Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions bridge/evm/contracts/SuiBridgeV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "./SuiBridge.sol";
import "./utils/BridgeUtilsV2.sol";

contract SuiBridgeV2 is SuiBridge {
/// @notice Allows the caller to provide signatures that enable the transfer of tokens to
/// the recipient address indicated within the message payload.
/// @dev `message.chainID` represents the sending chain ID. Receiving chain ID needs to match
/// this bridge's chain ID (this chain).
/// @param signatures The array of signatures.
/// @param message The BridgeUtils containing the transfer details.
function transferBridgedTokensWithSignaturesV2(
bytes[] memory signatures,
BridgeUtils.Message memory message
)
external
nonReentrant
verifyMessageAndSignatures(message, signatures, BridgeUtils.TOKEN_TRANSFER)
onlySupportedChain(message.chainID)
{
// verify that message has not been processed
require(!isTransferProcessed[message.nonce], "SuiBridge: Message already processed");
require(message.version == 2, "SuiBridge: Invalid message version");

IBridgeConfig config = committee.config();

BridgeUtilsV2.TokenTransferPayloadV2 memory tokenTransferPayload =
BridgeUtilsV2.decodeTokenTransferPayloadV2(message.payload);

// verify target chain ID is this chain ID
require(
tokenTransferPayload.targetChain == config.chainID(), "SuiBridge: Invalid target chain"
);

// convert amount to ERC20 token decimals
uint256 erc20AdjustedAmount = BridgeUtils.convertSuiToERC20Decimal(
IERC20Metadata(config.tokenAddressOf(tokenTransferPayload.tokenID)).decimals(),
config.tokenSuiDecimalOf(tokenTransferPayload.tokenID),
tokenTransferPayload.amount
);

_transferTokensFromVault(
message.chainID,
tokenTransferPayload.tokenID,
tokenTransferPayload.recipientAddress,
erc20AdjustedAmount,
tokenTransferPayload.timestamp
);

// mark message as processed
isTransferProcessed[message.nonce] = true;

emit TokensClaimed(
message.chainID,
message.nonce,
config.chainID(),
tokenTransferPayload.tokenID,
erc20AdjustedAmount,
tokenTransferPayload.senderAddress,
tokenTransferPayload.recipientAddress
);
}

/// @dev Transfers tokens from the vault to a target address.
/// @param sendingChainID The ID of the chain from which the tokens are being transferred.
/// @param tokenID The ID of the token being transferred.
/// @param recipientAddress The address to which the tokens are being transferred.
/// @param amount The amount of tokens being transferred.
function _transferTokensFromVault(
uint8 sendingChainID,
uint8 tokenID,
address recipientAddress,
uint256 amount,
uint256 transferTimeStamp
)
private
whenNotPaused
limitNotExceededV2(sendingChainID, tokenID, amount, transferTimeStamp)
{
address tokenAddress = committee.config().tokenAddressOf(tokenID);

// Check that the token address is supported
require(tokenAddress != address(0), "SuiBridge: Unsupported token");

// transfer eth if token type is eth
if (tokenID == BridgeUtils.ETH) {
vault.transferETH(payable(recipientAddress), amount);
} else {
// transfer tokens from vault to target address
vault.transferERC20(tokenAddress, recipientAddress, amount);
}
}

/* ========== MODIFIERS ========== */

/// @dev Requires the amount being transferred does not exceed the bridge limit in
/// the last 48 hours.
/// @param tokenID The ID of the token being transferred.
/// @param amount The amount of tokens being transferred.
modifier limitNotExceededV2(
uint8 chainID,
uint8 tokenID,
uint256 amount,
uint256 transferTimeStamp
) {
if (!BridgeUtilsV2.isMatureMessage(transferTimeStamp, block.timestamp)) {
require(
!limiter.willAmountExceedLimit(chainID, tokenID, amount),
"SuiBridge: Amount exceeds bridge limit"
);
// record the transfer in the limiter
limiter.recordBridgeTransfers(chainID, tokenID, amount);
}
_;
}
}
137 changes: 137 additions & 0 deletions bridge/evm/contracts/utils/BridgeUtilsV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

library BridgeUtilsV2 {
/* ========== STRUCTS ========== */
/// @dev A struct that represents a token transfer payload
/// @param senderAddressLength The length of the sender address in bytes
/// @param senderAddress The address of the sender on the source chain
/// @param targetChain The chain ID of the target chain
/// @param recipientAddressLength The length of the target address in bytes
/// @param recipientAddress The address of the recipient on the target chain
/// @param tokenID The ID of the token to be transferred
/// @param amount The amount of the token to be transferred
/// @param timestamp The timestamp of the message creation
struct TokenTransferPayloadV2 {
uint8 senderAddressLength;
bytes senderAddress;
uint8 targetChain;
uint8 recipientAddressLength;
address recipientAddress;
uint8 tokenID;
uint64 amount;
uint256 timestamp; // timestamp of the message creation
}

/* ========== CONSTANTS ========== */

function isMatureMessage(uint256 messageTimestamp, uint256 currentTimestamp)
internal
pure
returns (bool)
{
// The message is considered mature if the timestamp is greater than or equal to the current block timestamp
// minus 48 hours (48 * 3600 seconds).
return currentTimestamp > messageTimestamp + 48 * 3600;
}

/// @notice Decodes a token transfer payload from bytes to a TokenTransferPayload struct.
/// @dev The function will revert if the payload length is invalid.
/// TokenTransfer payload is 64 bytes.
/// byte 0 : sender address length
/// bytes 1-32 : sender address (as we only support Sui now, it has to be 32 bytes long)
/// bytes 33 : target chain id
/// byte 34 : target address length
/// bytes 35-54 : target address
/// byte 55 : token id
/// bytes 56-63 : amount
/// bytes 64-71 : message timestamp
/// @param _payload The payload to be decoded.
/// @return The decoded token transfer payload as a TokenTransferPayload struct.
function decodeTokenTransferPayloadV2(bytes memory _payload)
internal
pure
returns (TokenTransferPayloadV2 memory)
{
require(_payload.length == 71, "BridgeUtils: TokenTransferPayload must be 71 bytes");

uint8 senderAddressLength = uint8(_payload[0]);

require(
senderAddressLength == 32,
"BridgeUtils: Invalid sender address length, Sui address must be 32 bytes"
);

// used to offset already read bytes
uint8 offset = 1;

// extract sender address from payload bytes 1-32
bytes memory senderAddress = new bytes(senderAddressLength);
for (uint256 i; i < senderAddressLength; i++) {
senderAddress[i] = _payload[i + offset];
}

// move offset past the sender address length
offset += senderAddressLength;

// target chain is a single byte
uint8 targetChain = uint8(_payload[offset++]);

// target address length is a single byte
uint8 recipientAddressLength = uint8(_payload[offset++]);
require(
recipientAddressLength == 20,
"BridgeUtils: Invalid target address length, EVM address must be 20 bytes"
);

// extract target address from payload (35-54)
address recipientAddress;

// why `add(recipientAddressLength, offset)`?
// At this point, offset = 35, recipientAddressLength = 20. `mload(add(payload, 55))`
// reads the next 32 bytes from bytes 23 in paylod, because the first 32 bytes
// of payload stores its length. So in reality, bytes 23 - 54 is loaded. During
// casting to address (20 bytes), the least sigificiant bytes are retained, namely
// `recipientAddress` is bytes 35-54
assembly {
recipientAddress := mload(add(_payload, add(recipientAddressLength, offset)))
}

// move offset past the target address length
offset += recipientAddressLength;

// token id is a single byte
uint8 tokenID = uint8(_payload[offset++]);

// extract amount from payload
uint64 amount;
uint8 amountLength = 8; // uint64 = 8 bits

// Why `add(amountLength, offset)`?
// At this point, offset = 56, amountLength = 8. `mload(add(payload, 64))`
// reads the next 32 bytes from bytes 32 in paylod, because the first 32 bytes
// of payload stores its length. So in reality, bytes 32 - 63 is loaded. During
// casting to uint64 (8 bytes), the least sigificiant bytes are retained, namely
// `recipientAddress` is bytes 56-63
assembly {
amount := mload(add(_payload, add(amountLength, offset)))
}

uint256 message_timestamp;
// Extract timestamp from payload bytes 64-71
assembly {
message_timestamp := mload(add(_payload, 64))
}

return TokenTransferPayloadV2(
senderAddressLength,
senderAddress,
targetChain,
recipientAddressLength,
recipientAddress,
tokenID,
amount,
message_timestamp
);
}
}
10 changes: 9 additions & 1 deletion bridge/evm/test/BridgeBaseTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,15 @@ contract BridgeBaseTest is Test {
"BridgeConfig.sol",
abi.encodeCall(
BridgeConfig.initialize,
(address(committee), chainID, supportedTokens, tokenPrices, tokenIds, suiDecimals, supportedChains)
(
address(committee),
chainID,
supportedTokens,
tokenPrices,
tokenIds,
suiDecimals,
supportedChains
)
),
opts
);
Expand Down
Loading
Loading