diff --git a/.github/actions/diffs/action.yml b/.github/actions/diffs/action.yml index e8e601d9a1c37..29cbdcc4b6ea5 100644 --- a/.github/actions/diffs/action.yml +++ b/.github/actions/diffs/action.yml @@ -13,6 +13,9 @@ outputs: isMove: description: True when changes happened to the Move code value: "${{ steps.diff.outputs.isMove }}" + isSolidity: + description: True when changes happened to the Solidity code + value: "${{ steps.diff.outputs.isSolidity }}" isReleaseNotesEligible: description: True when changes happened in Release Notes eligible paths value: "${{ steps.diff.outputs.isReleaseNotesEligible }}" @@ -58,6 +61,8 @@ runs: - 'Cargo.toml' - 'examples/**' - 'sui_programmability/**' + isSolidity: + - 'bridge/evm/**' isReleaseNotesEligible: - 'consensus/**' - 'crates/**' diff --git a/.github/workflows/narwhal.yml b/.github/workflows/narwhal.yml index a2c2425fecfd3..2572676d4e526 100644 --- a/.github/workflows/narwhal.yml +++ b/.github/workflows/narwhal.yml @@ -91,6 +91,10 @@ jobs: uses: pierotofy/set-swap-space@master with: swap-size-gb: 256 + - name: Install Foundry + run: | + curl -L https://foundry.paradigm.xyz | { cat; echo '$FOUNDRY_BIN_DIR/foundryup'; } | bash + echo "$HOME/.config/.foundry/bin" >> $GITHUB_PATH - name: cargo test run: | cargo nextest run --profile ci diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 2c2a313da7408..74bc4dae82709 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -45,6 +45,7 @@ jobs: outputs: isRust: ${{ steps.diff.outputs.isRust }} isMove: ${{ steps.diff.outputs.isMove }} + isSolidity: ${{ steps.diff.outputs.isSolidity }} isReleaseNotesEligible: ${{ steps.diff.outputs.isReleaseNotesEligible }} isMoveAutoFormatter: ${{ steps.diff.outputs.isMoveAutoFormatter }} steps: @@ -108,7 +109,7 @@ jobs: test: needs: diff - if: needs.diff.outputs.isRust == 'true' + if: needs.diff.outputs.isRust == 'true' || needs.diff.outputs.isSolidity == 'true' timeout-minutes: 45 env: # Tests written with #[sim_test] are often flaky if run as #[tokio::test] - this var @@ -128,6 +129,10 @@ jobs: uses: pierotofy/set-swap-space@master with: swap-size-gb: 256 + - name: Install Foundry + run: | + curl -L https://foundry.paradigm.xyz | { cat; echo '$FOUNDRY_BIN_DIR/foundryup'; } | bash + echo "$HOME/.config/.foundry/bin" >> $GITHUB_PATH - name: cargo test run: | cargo nextest run --profile ci @@ -221,7 +226,7 @@ jobs: simtest: needs: diff - if: needs.diff.outputs.isRust == 'true' + if: needs.diff.outputs.isRust == 'true' || needs.diff.outputs.isSolidity == 'true' timeout-minutes: 45 runs-on: [ubuntu-ghcloud] env: @@ -465,3 +470,27 @@ jobs: env: POSTGRES_HOST: localhost POSTGRES_PORT: 5432 + + bridge-evm: + name: bridge-evm-tests + needs: diff + if: needs.diff.outputs.isSolidity == 'true' + runs-on: [ubuntu-latest] + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # Pin v4.1.1 + - name: Install Foundry + run: | + curl -L https://foundry.paradigm.xyz | { cat; echo '$FOUNDRY_BIN_DIR/foundryup'; } | bash + echo "$HOME/.config/.foundry/bin" >> $GITHUB_PATH + - name: Install Dependencies + working-directory: bridge/evm + run: | + forge install https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable@v5.0.1 https://github.com/foundry-rs/forge-std@v1.3.0 https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades --no-git --no-commit + - name: Check Bridge EVM Unit Tests + shell: bash + working-directory: bridge/evm + env: + INFURA_API_KEY: ${{ secrets.INFURA_API_KEY }} + run: | + forge clean + forge test --ffi \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 675bd11764935..36088bb7d46de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11729,6 +11729,7 @@ dependencies = [ "fastcrypto", "fastcrypto-zkp", "fs_extra", + "futures", "im", "inquire", "insta", @@ -11759,6 +11760,7 @@ dependencies = [ "shell-words", "shlex", "signature 1.6.4", + "sui-bridge", "sui-config", "sui-execution", "sui-genesis-builder", @@ -12108,6 +12110,7 @@ dependencies = [ "bcs", "bin-version", "clap", + "enum_dispatch", "ethers", "eyre", "fastcrypto", @@ -12128,6 +12131,7 @@ dependencies = [ "shared-crypto", "sui-common", "sui-config", + "sui-json-rpc-api", "sui-json-rpc-types", "sui-sdk", "sui-test-transaction-builder", @@ -12438,6 +12442,7 @@ dependencies = [ "serde_json", "shared-crypto", "sui", + "sui-bridge", "sui-config", "sui-core", "sui-framework", @@ -14168,6 +14173,7 @@ dependencies = [ "nonempty", "num-bigint 0.4.4", "num-traits", + "num_enum 0.6.1", "once_cell", "parking_lot 0.12.1", "prometheus", @@ -14620,12 +14626,15 @@ name = "test-cluster" version = "0.1.0" dependencies = [ "anyhow", + "bcs", + "fastcrypto", "fastcrypto-zkp", "futures", "jsonrpsee", "move-binary-format", "prometheus", "rand 0.8.5", + "sui-bridge", "sui-config", "sui-core", "sui-framework", diff --git a/Cargo.toml b/Cargo.toml index 242f8060ed4cc..7139e0a560ff5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -591,6 +591,7 @@ sui-analytics-indexer-derive = { path = "crates/sui-analytics-indexer-derive" } sui-archival = { path = "crates/sui-archival" } sui-authority-aggregation = { path = "crates/sui-authority-aggregation" } sui-benchmark = { path = "crates/sui-benchmark" } +sui-bridge = { path = "crates/sui-bridge" } sui-cluster-test = { path = "crates/sui-cluster-test" } sui-common = { path = "crates/sui-common" } sui-config = { path = "crates/sui-config" } diff --git a/bridge/evm/.gitignore b/bridge/evm/.gitignore index 8c2ebb24e47d9..b03db232ccd9e 100644 --- a/bridge/evm/.gitignore +++ b/bridge/evm/.gitignore @@ -4,10 +4,11 @@ cache* network_config.json .vscode cache/ -out/ +out*/ *.txt !remappings.txt lcov.info broadcast/**/31337 -lib/* \ No newline at end of file +lib/* + diff --git a/bridge/evm/contracts/BridgeCommittee.sol b/bridge/evm/contracts/BridgeCommittee.sol index 6dfe814a6acd0..349c9c8b905c9 100644 --- a/bridge/evm/contracts/BridgeCommittee.sol +++ b/bridge/evm/contracts/BridgeCommittee.sol @@ -18,32 +18,31 @@ contract BridgeCommittee is IBridgeCommittee, CommitteeUpgradeable { mapping(address committeeMember => bool isBlocklisted) public blocklist; IBridgeConfig public config; - /* ========== INITIALIZER ========== */ + /* ========== INITIALIZERS ========== */ /// @notice Initializes the contract with the provided parameters. /// @dev should be called directly after deployment (see OpenZeppelin upgradeable standards). - /// the provided arrays must have the same length and the total stake provided must equal 10000. - /// @param _config The address of the BridgeConfig contract. + /// the provided arrays must have the same length and the total stake provided must be greater than, + /// or equal to the provided minimum stake required. /// @param committee addresses of the committee members. /// @param stake amounts of the committee members. - function initialize( - address _config, - address[] memory committee, - uint16[] memory stake, - uint16 minStakeRequired - ) external initializer { + /// @param minStakeRequired minimum stake required for the committee. + function initialize(address[] memory committee, uint16[] memory stake, uint16 minStakeRequired) + external + initializer + { __CommitteeUpgradeable_init(address(this)); __UUPSUpgradeable_init(); uint256 _committeeLength = committee.length; + require(_committeeLength < 256, "BridgeCommittee: Committee length must be less than 256"); + require( _committeeLength == stake.length, "BridgeCommittee: Committee and stake arrays must be of the same length" ); - config = IBridgeConfig(_config); - uint16 totalStake; for (uint16 i; i < _committeeLength; i++) { require( @@ -57,19 +56,28 @@ contract BridgeCommittee is IBridgeCommittee, CommitteeUpgradeable { require(totalStake >= minStakeRequired, "BridgeCommittee: total stake is less than minimum"); // 10000 == 100% } + /// @notice Initializes the contract with the provided parameters. + /// @dev This function should be called directly after config deployment. The config contract address + /// provided should be verified before bridging any assets. + /// @param _config The address of the BridgeConfig contract. + function initializeConfig(address _config) external { + require(address(config) == address(0), "BridgeCommittee: Config already initialized"); + config = IBridgeConfig(_config); + } + /* ========== EXTERNAL FUNCTIONS ========== */ /// @notice Verifies the provided signatures for the given message by aggregating and validating the /// stake of each signer against the required stake of the given message type. /// @dev The function will revert if the total stake of the signers is less than the required stake. /// @param signatures The array of signatures to be verified. - /// @param message The `BridgeMessage.Message` to be verified. - function verifySignatures(bytes[] memory signatures, BridgeMessage.Message memory message) + /// @param message The `BridgeUtils.Message` to be verified. + function verifySignatures(bytes[] memory signatures, BridgeUtils.Message memory message) external view override { - uint32 requiredStake = BridgeMessage.requiredStake(message); + uint32 requiredStake = BridgeUtils.requiredStake(message); uint16 approvalStake; address signer; @@ -81,19 +89,15 @@ contract BridgeCommittee is IBridgeCommittee, CommitteeUpgradeable { // recover the signer from the signature (bytes32 r, bytes32 s, uint8 v) = splitSignature(signature); - (signer,,) = ECDSA.tryRecover(BridgeMessage.computeHash(message), v, r, s); + (signer,,) = ECDSA.tryRecover(BridgeUtils.computeHash(message), v, r, s); - // skip if signer is block listed or has no stake - if (blocklist[signer] || committeeStake[signer] == 0) continue; + require(!blocklist[signer], "BridgeCommittee: Signer is blocklisted"); + require(committeeStake[signer] > 0, "BridgeCommittee: Signer has no stake"); uint8 index = committeeIndex[signer]; uint256 mask = 1 << index; - if (bitmap & mask == 0) { - bitmap |= mask; - } else { - // skip if duplicate signature - continue; - } + require(bitmap & mask == 0, "BridgeCommittee: Duplicate signature provided"); + bitmap |= mask; approvalStake += committeeStake[signer]; } @@ -103,18 +107,18 @@ contract BridgeCommittee is IBridgeCommittee, CommitteeUpgradeable { /// @notice Updates the blocklist status of the provided addresses if provided signatures are valid. /// @param signatures The array of signatures to validate the message. - /// @param message BridgeMessage containing the update blocklist payload. + /// @param message BridgeUtils containing the update blocklist payload. function updateBlocklistWithSignatures( bytes[] memory signatures, - BridgeMessage.Message memory message + BridgeUtils.Message memory message ) external nonReentrant - verifyMessageAndSignatures(message, signatures, BridgeMessage.BLOCKLIST) + verifyMessageAndSignatures(message, signatures, BridgeUtils.BLOCKLIST) { // decode the blocklist payload (bool isBlocklisted, address[] memory _blocklist) = - BridgeMessage.decodeBlocklistPayload(message.payload); + BridgeUtils.decodeBlocklistPayload(message.payload); // update the blocklist _updateBlocklist(_blocklist, isBlocklisted); diff --git a/bridge/evm/contracts/BridgeConfig.sol b/bridge/evm/contracts/BridgeConfig.sol new file mode 100644 index 0000000000000..4cad5e3aaa615 --- /dev/null +++ b/bridge/evm/contracts/BridgeConfig.sol @@ -0,0 +1,188 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import "./utils/CommitteeUpgradeable.sol"; +import "./interfaces/IBridgeConfig.sol"; + +/// @title BridgeConfig +/// @notice This contract manages a registry of supported tokens and supported chain IDs for the SuiBridge. +/// It also provides functions to convert token amounts to Sui decimal adjusted amounts and vice versa. +contract BridgeConfig is IBridgeConfig, CommitteeUpgradeable { + /* ========== STATE VARIABLES ========== */ + + uint8 public chainID; + mapping(uint8 tokenID => Token) public supportedTokens; + // price in USD (8 decimal precision) (e.g. 1 ETH = 2000 USD => 2000_00000000) + mapping(uint8 tokenID => uint64 tokenPrice) public tokenPrices; + mapping(uint8 chainId => bool isSupported) public supportedChains; + + /// @notice Constructor function for the BridgeConfig contract. + /// @dev the provided arrays must have the same length. + /// @param _committee The address of the BridgeCommittee contract. + /// @param _chainID The ID of the chain this contract is deployed on. + /// @param _supportedTokens The addresses of the supported tokens. + /// @param _tokenPrices An array of token prices (with 8 decimal precision). + /// @param _supportedChains array of supported chain IDs. + function initialize( + address _committee, + uint8 _chainID, + address[] memory _supportedTokens, + uint64[] memory _tokenPrices, + uint8[] memory _supportedChains + ) external initializer { + __CommitteeUpgradeable_init(_committee); + require(_supportedTokens[0] == address(0), "BridgeConfig: Must reserve first token for SUI"); + require(_supportedTokens.length == 5, "BridgeConfig: Invalid supported token addresses"); + require( + _supportedTokens.length == _tokenPrices.length, "BridgeConfig: Invalid token prices" + ); + + uint8[] memory _suiDecimals = new uint8[](5); + _suiDecimals[0] = 9; // SUI + _suiDecimals[1] = 8; // wBTC + _suiDecimals[2] = 8; // wETH + _suiDecimals[3] = 6; // USDC + _suiDecimals[4] = 6; // USDT + + for (uint8 i; i < _supportedTokens.length; i++) { + supportedTokens[i] = Token(_supportedTokens[i], _suiDecimals[i], true); + } + + for (uint8 i; i < _supportedChains.length; i++) { + require(_supportedChains[i] != _chainID, "BridgeConfig: Cannot support self"); + supportedChains[_supportedChains[i]] = true; + } + + for (uint8 i; i < _tokenPrices.length; i++) { + tokenPrices[i] = _tokenPrices[i]; + } + + chainID = _chainID; + } + + /* ========== VIEW FUNCTIONS ========== */ + + /// @notice Returns the address of the token with the given ID. + /// @param tokenID The ID of the token. + /// @return address of the provided token. + function tokenAddressOf(uint8 tokenID) public view override returns (address) { + return supportedTokens[tokenID].tokenAddress; + } + + /// @notice Returns the sui decimal places of the token with the given ID. + /// @param tokenID The ID of the token. + /// @return amount of sui decimal places of the provided token. + function tokenSuiDecimalOf(uint8 tokenID) public view override returns (uint8) { + return supportedTokens[tokenID].suiDecimal; + } + + /// @notice Returns the price of the token with the given ID. + /// @param tokenID The ID of the token. + /// @return price of the provided token. + function tokenPriceOf(uint8 tokenID) public view override returns (uint64) { + return tokenPrices[tokenID]; + } + + /// @notice Returns whether a token is supported in SuiBridge with the given ID. + /// @param tokenID The ID of the token. + /// @return true if the token is supported, false otherwise. + function isTokenSupported(uint8 tokenID) public view override returns (bool) { + return supportedTokens[tokenID].tokenAddress != address(0); + } + + /// @notice Returns whether a chain is supported in SuiBridge with the given ID. + /// @param chainId The ID of the chain. + /// @return true if the chain is supported, false otherwise. + function isChainSupported(uint8 chainId) public view override returns (bool) { + return supportedChains[chainId]; + } + + /* ========== MUTATIVE FUNCTIONS ========== */ + + /// @notice Updates the token price with the provided message if the provided signatures are valid. + /// @param signatures array of signatures to validate the message. + /// @param message BridgeMessage containing the update token price payload. + function updateTokenPriceWithSignatures( + bytes[] memory signatures, + BridgeUtils.Message memory message + ) + external + nonReentrant + verifyMessageAndSignatures(message, signatures, BridgeUtils.UPDATE_TOKEN_PRICE) + { + // decode the update token payload + (uint8 tokenID, uint64 price) = BridgeUtils.decodeUpdateTokenPricePayload(message.payload); + + _updateTokenPrice(tokenID, price); + } + + function addTokensWithSignatures(bytes[] memory signatures, BridgeUtils.Message memory message) + external + nonReentrant + verifyMessageAndSignatures(message, signatures, BridgeUtils.ADD_EVM_TOKENS) + { + // decode the update token payload + ( + bool native, + uint8[] memory tokenIDs, + address[] memory tokenAddresses, + uint8[] memory suiDecimals, + uint64[] memory _tokenPrices + ) = BridgeUtils.decodeAddTokensPayload(message.payload); + + // update the token + for (uint8 i; i < tokenIDs.length; i++) { + _addToken(tokenIDs[i], tokenAddresses[i], suiDecimals[i], _tokenPrices[i], native); + } + } + + /* ========== PRIVATE FUNCTIONS ========== */ + + /// @notice Updates the price of the token with the provided ID. + /// @param tokenID The ID of the token to update. + /// @param tokenPrice The price of the token. + function _updateTokenPrice(uint8 tokenID, uint64 tokenPrice) private { + require(isTokenSupported(tokenID), "BridgeConfig: Unsupported token"); + require(tokenPrice > 0, "BridgeConfig: Invalid token price"); + + tokenPrices[tokenID] = tokenPrice; + + emit TokenPriceUpdated(tokenID, tokenPrice); + } + + /// @notice Updates the token with the provided ID. + /// @param tokenID The ID of the token to update. + /// @param tokenAddress The address of the token. + /// @param suiDecimal The decimal places of the token. + /// @param tokenPrice The price of the token. + /// @param native Whether the token is native to the chain. + function _addToken( + uint8 tokenID, + address tokenAddress, + uint8 suiDecimal, + uint64 tokenPrice, + bool native + ) private { + require(tokenAddress != address(0), "BridgeConfig: Invalid token address"); + require(suiDecimal > 0, "BridgeConfig: Invalid Sui decimal"); + require(tokenPrice > 0, "BridgeConfig: Invalid token price"); + + uint8 erc20Decimals = IERC20Metadata(tokenAddress).decimals(); + require(erc20Decimals >= suiDecimal, "BridgeConfig: Invalid Sui decimal"); + + supportedTokens[tokenID] = Token(tokenAddress, suiDecimal, native); + tokenPrices[tokenID] = tokenPrice; + + emit TokenAdded(tokenID, tokenAddress, suiDecimal, tokenPrice); + } + + /* ========== MODIFIERS ========== */ + + /// @notice Requires the given token to be supported. + /// @param tokenID The ID of the token to check. + modifier tokenSupported(uint8 tokenID) { + require(isTokenSupported(tokenID), "BridgeConfig: Unsupported token"); + _; + } +} diff --git a/bridge/evm/contracts/BridgeLimiter.sol b/bridge/evm/contracts/BridgeLimiter.sol index 3f1089b2b496e..f38bf6a3dc5e9 100644 --- a/bridge/evm/contracts/BridgeLimiter.sol +++ b/bridge/evm/contracts/BridgeLimiter.sol @@ -11,16 +11,14 @@ import "./utils/CommitteeUpgradeable.sol"; /// @notice A contract that limits the amount of tokens that can be bridged from a given chain within /// a rolling 24-hour window. This is accomplished by storing the amount bridged from a given chain in USD /// within a given hourly timestamp. It also provides functions to update the token prices and the total -/// limit of the given chainID measured in USD with a 4 decimal precision. +/// limit of the given chainID measured in USD with 8 decimal precision. /// The contract is intended to be used and owned by the SuiBridge contract. contract BridgeLimiter is IBridgeLimiter, CommitteeUpgradeable, OwnableUpgradeable { /* ========== STATE VARIABLES ========== */ mapping(uint256 chainHourTimestamp => uint256 totalAmountBridged) public chainHourlyTransferAmount; - // price in USD (4 decimal precision) (e.g. 1 ETH = 2000 USD => 20000000) - mapping(uint8 tokenID => uint256 tokenPrice) public tokenPrices; - // total limit in USD (4 decimal precision) (e.g. 10000000 => 1000 USD) + // total limit in USD (8 decimal precision) (e.g. 1000_00000000 => 1000 USD) mapping(uint8 chainID => uint64 totalLimit) public chainLimits; mapping(uint8 chainID => uint32 oldestHourTimestamp) public oldestChainTimestamp; @@ -30,24 +28,19 @@ contract BridgeLimiter is IBridgeLimiter, CommitteeUpgradeable, OwnableUpgradeab /// @dev this function should be called directly after deployment (see OpenZeppelin upgradeable /// standards). /// @param _committee The address of the BridgeCommittee contract. - /// @param _tokenPrices An array of token prices (with 4 decimal precision). /// @param chainIDs An array of chain IDs to limit. - /// @param _totalLimits The total limit for the bridge (4 decimal precision). - function initialize( - address _committee, - uint256[] memory _tokenPrices, - uint8[] memory chainIDs, - uint64[] memory _totalLimits - ) external initializer { + /// @param _totalLimits The total limit for the bridge (8 decimal precision). + function initialize(address _committee, uint8[] memory chainIDs, uint64[] memory _totalLimits) + external + initializer + { require( chainIDs.length == _totalLimits.length, "BridgeLimiter: invalid chainIDs and totalLimits length" ); __CommitteeUpgradeable_init(_committee); __Ownable_init(msg.sender); - for (uint8 i; i < _tokenPrices.length; i++) { - tokenPrices[i] = _tokenPrices[i]; - } + for (uint8 i; i < chainIDs.length; i++) { require( committee.config().isChainSupported(chainIDs[i]), @@ -97,17 +90,18 @@ contract BridgeLimiter is IBridgeLimiter, CommitteeUpgradeable, OwnableUpgradeab return total; } - /// @notice Calculates the given token amount in USD (4 decimal precision). + /// @notice Calculates the given token amount in USD (8 decimal precision). /// @param tokenID The ID of the token. /// @param amount The amount of tokens. - /// @return amount in USD (4 decimal precision). + /// @return amount in USD (8 decimal precision). function calculateAmountInUSD(uint8 tokenID, uint256 amount) public view returns (uint256) { // get the token address - address tokenAddress = committee.config().getTokenAddress(tokenID); + address tokenAddress = committee.config().tokenAddressOf(tokenID); // get the decimals uint8 decimals = IERC20Metadata(tokenAddress).decimals(); - return amount * tokenPrices[tokenID] / (10 ** decimals); + // calculate amount in USD + return amount * committee.config().tokenPriceOf(tokenID) / (10 ** decimals); } /// @notice Returns the current hour timestamp. @@ -163,40 +157,20 @@ contract BridgeLimiter is IBridgeLimiter, CommitteeUpgradeable, OwnableUpgradeab emit HourlyTransferAmountUpdated(_currentHour, usdAmount); } - /// @notice Updates the token price with the provided message if the provided signatures are valid. - /// @param signatures array of signatures to validate the message. - /// @param message BridgeMessage containing the update token price payload. - function updateTokenPriceWithSignatures( - bytes[] memory signatures, - BridgeMessage.Message memory message - ) - external - nonReentrant - verifyMessageAndSignatures(message, signatures, BridgeMessage.UPDATE_TOKEN_PRICE) - { - // decode the update token payload - (uint8 tokenID, uint64 price) = BridgeMessage.decodeUpdateTokenPricePayload(message.payload); - - // update the token price - tokenPrices[tokenID] = price; - - emit AssetPriceUpdated(tokenID, price); - } - /// @notice Updates the total limit with the provided message if the provided signatures are valid. /// @param signatures array of signatures to validate the message. - /// @param message The BridgeMessage containing the update limit payload. + /// @param message The BridgeUtils containing the update limit payload. function updateLimitWithSignatures( bytes[] memory signatures, - BridgeMessage.Message memory message + BridgeUtils.Message memory message ) external nonReentrant - verifyMessageAndSignatures(message, signatures, BridgeMessage.UPDATE_BRIDGE_LIMIT) + verifyMessageAndSignatures(message, signatures, BridgeUtils.UPDATE_BRIDGE_LIMIT) { // decode the update limit payload (uint8 sourceChainID, uint64 newLimit) = - BridgeMessage.decodeUpdateLimitPayload(message.payload); + BridgeUtils.decodeUpdateLimitPayload(message.payload); require( committee.config().isChainSupported(sourceChainID), diff --git a/bridge/evm/contracts/BridgeVault.sol b/bridge/evm/contracts/BridgeVault.sol index 0eae772b53967..8cd78f8b94b53 100644 --- a/bridge/evm/contracts/BridgeVault.sol +++ b/bridge/evm/contracts/BridgeVault.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.20; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "./interfaces/IBridgeVault.sol"; import "./interfaces/IWETH9.sol"; @@ -37,14 +38,8 @@ contract BridgeVault is Ownable, IBridgeVault { override onlyOwner { - // Get the token contract instance - IERC20 token = IERC20(tokenAddress); - // Transfer the tokens from the contract to the target address - bool success = token.transfer(recipientAddress, amount); - - // Check that the transfer was successful - require(success, "BridgeVault: Transfer failed"); + SafeERC20.safeTransfer(IERC20(tokenAddress), recipientAddress, amount); } /// @notice Unwraps stored wrapped ETH and transfers the newly withdrawn ETH to the provided target @@ -61,10 +56,15 @@ contract BridgeVault is Ownable, IBridgeVault { wETH.withdraw(amount); // Transfer the unwrapped ETH to the target address - recipientAddress.transfer(amount); + (bool success,) = recipientAddress.call{value: amount}(""); + require(success, "ETH transfer failed"); } - /// @notice Enables the contract to receive ETH. - /// @dev This function is required to receive ETH when unwrapping WETH. - receive() external payable {} + /// @notice Wraps as eth sent to this contract. + /// @dev skip if sender is wETH contract to avoid infinite loop. + receive() external payable { + if (msg.sender != address(wETH)) { + wETH.deposit{value: msg.value}(); + } + } } diff --git a/bridge/evm/contracts/SuiBridge.sol b/bridge/evm/contracts/SuiBridge.sol index e9fac67fae03f..24b862855aaef 100644 --- a/bridge/evm/contracts/SuiBridge.sol +++ b/bridge/evm/contracts/SuiBridge.sol @@ -3,6 +3,8 @@ pragma solidity ^0.8.20; import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/interfaces/IERC20Metadata.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "./utils/CommitteeUpgradeable.sol"; import "./interfaces/ISuiBridge.sol"; import "./interfaces/IBridgeVault.sol"; @@ -23,7 +25,6 @@ contract SuiBridge is ISuiBridge, CommitteeUpgradeable, PausableUpgradeable { mapping(uint64 nonce => bool isProcessed) public isTransferProcessed; IBridgeVault public vault; IBridgeLimiter public limiter; - IWETH9 public wETH; /* ========== INITIALIZER ========== */ @@ -32,8 +33,7 @@ contract SuiBridge is ISuiBridge, CommitteeUpgradeable, PausableUpgradeable { /// @param _committee The address of the committee contract. /// @param _vault The address of the bridge vault contract. /// @param _limiter The address of the bridge limiter contract. - /// @param _wETH The address of the WETH9 contract. - function initialize(address _committee, address _vault, address _limiter, address _wETH) + function initialize(address _committee, address _vault, address _limiter) external initializer { @@ -41,7 +41,6 @@ contract SuiBridge is ISuiBridge, CommitteeUpgradeable, PausableUpgradeable { __Pausable_init(); vault = IBridgeVault(_vault); limiter = IBridgeLimiter(_limiter); - wETH = IWETH9(_wETH); } /* ========== EXTERNAL FUNCTIONS ========== */ @@ -51,31 +50,34 @@ contract SuiBridge is ISuiBridge, CommitteeUpgradeable, PausableUpgradeable { /// @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 BridgeMessage containing the transfer details. + /// @param message The BridgeUtils containing the transfer details. function transferBridgedTokensWithSignatures( bytes[] memory signatures, - BridgeMessage.Message memory message + BridgeUtils.Message memory message ) external nonReentrant - verifyMessageAndSignatures(message, signatures, BridgeMessage.TOKEN_TRANSFER) + verifyMessageAndSignatures(message, signatures, BridgeUtils.TOKEN_TRANSFER) onlySupportedChain(message.chainID) { // verify that message has not been processed require(!isTransferProcessed[message.nonce], "SuiBridge: Message already processed"); - BridgeMessage.TokenTransferPayload memory tokenTransferPayload = - BridgeMessage.decodeTokenTransferPayload(message.payload); + IBridgeConfig config = committee.config(); + + BridgeUtils.TokenTransferPayload memory tokenTransferPayload = + BridgeUtils.decodeTokenTransferPayload(message.payload); // verify target chain ID is this chain ID require( - tokenTransferPayload.targetChain == committee.config().chainID(), - "SuiBridge: Invalid target chain" + tokenTransferPayload.targetChain == config.chainID(), "SuiBridge: Invalid target chain" ); // convert amount to ERC20 token decimals - uint256 erc20AdjustedAmount = committee.config().convertSuiToERC20Decimal( - tokenTransferPayload.tokenID, tokenTransferPayload.amount + uint256 erc20AdjustedAmount = BridgeUtils.convertSuiToERC20Decimal( + IERC20Metadata(config.tokenAddressOf(tokenTransferPayload.tokenID)).decimals(), + config.tokenSuiDecimalOf(tokenTransferPayload.tokenID), + tokenTransferPayload.amount ); _transferTokensFromVault( @@ -91,7 +93,7 @@ contract SuiBridge is ISuiBridge, CommitteeUpgradeable, PausableUpgradeable { emit TokensClaimed( message.chainID, message.nonce, - committee.config().chainID(), + config.chainID(), tokenTransferPayload.tokenID, erc20AdjustedAmount, tokenTransferPayload.senderAddress, @@ -103,17 +105,17 @@ contract SuiBridge is ISuiBridge, CommitteeUpgradeable, PausableUpgradeable { /// @dev If the given operation is to freeze and the bridge is already frozen, the operation /// will revert. /// @param signatures The array of signatures to verify. - /// @param message The BridgeMessage containing the details of the operation. + /// @param message The BridgeUtils containing the details of the operation. function executeEmergencyOpWithSignatures( bytes[] memory signatures, - BridgeMessage.Message memory message + BridgeUtils.Message memory message ) external nonReentrant - verifyMessageAndSignatures(message, signatures, BridgeMessage.EMERGENCY_OP) + verifyMessageAndSignatures(message, signatures, BridgeUtils.EMERGENCY_OP) { // decode the emergency op message - bool isFreezing = BridgeMessage.decodeEmergencyOpPayload(message.payload); + bool isFreezing = BridgeUtils.decodeEmergencyOpPayload(message.payload); if (isFreezing) _pause(); else _unpause(); @@ -134,9 +136,11 @@ contract SuiBridge is ISuiBridge, CommitteeUpgradeable, PausableUpgradeable { bytes memory recipientAddress, uint8 destinationChainID ) external whenNotPaused nonReentrant onlySupportedChain(destinationChainID) { - require(committee.config().isTokenSupported(tokenID), "SuiBridge: Unsupported token"); + IBridgeConfig config = committee.config(); + + require(config.isTokenSupported(tokenID), "SuiBridge: Unsupported token"); - address tokenAddress = committee.config().getTokenAddress(tokenID); + address tokenAddress = config.tokenAddressOf(tokenID); // check that the bridge contract has allowance to transfer the tokens require( @@ -145,14 +149,16 @@ contract SuiBridge is ISuiBridge, CommitteeUpgradeable, PausableUpgradeable { ); // Transfer the tokens from the contract to the vault - IERC20(tokenAddress).transferFrom(msg.sender, address(vault), amount); + SafeERC20.safeTransferFrom(IERC20(tokenAddress), msg.sender, address(vault), amount); - // Adjust the amount to emit. - uint64 suiAdjustedAmount = committee.config().convertERC20ToSuiDecimal(tokenID, amount); + // Adjust the amount + uint64 suiAdjustedAmount = BridgeUtils.convertERC20ToSuiDecimal( + IERC20Metadata(tokenAddress).decimals(), config.tokenSuiDecimalOf(tokenID), amount + ); emit TokensDeposited( - committee.config().chainID(), - nonces[BridgeMessage.TOKEN_TRANSFER], + config.chainID(), + nonces[BridgeUtils.TOKEN_TRANSFER], destinationChainID, tokenID, suiAdjustedAmount, @@ -161,7 +167,7 @@ contract SuiBridge is ISuiBridge, CommitteeUpgradeable, PausableUpgradeable { ); // increment token transfer nonce - nonces[BridgeMessage.TOKEN_TRANSFER]++; + nonces[BridgeUtils.TOKEN_TRANSFER]++; } /// @notice Enables the caller to deposit Eth to be bridged to a given destination chain. @@ -177,28 +183,32 @@ contract SuiBridge is ISuiBridge, CommitteeUpgradeable, PausableUpgradeable { { uint256 amount = msg.value; - // Wrap ETH - wETH.deposit{value: amount}(); - - // Transfer the wrapped ETH back to caller - wETH.transfer(address(vault), amount); + // Transfer the unwrapped ETH to the target address + (bool success,) = payable(address(vault)).call{value: amount}(""); + require(success, "SuiBridge: Failed to transfer ETH to vault"); // Adjust the amount to emit. - uint64 suiAdjustedAmount = - committee.config().convertERC20ToSuiDecimal(BridgeMessage.ETH, amount); + IBridgeConfig config = committee.config(); + + // Adjust the amount + uint64 suiAdjustedAmount = BridgeUtils.convertERC20ToSuiDecimal( + IERC20Metadata(config.tokenAddressOf(BridgeUtils.ETH)).decimals(), + config.tokenSuiDecimalOf(BridgeUtils.ETH), + amount + ); emit TokensDeposited( - committee.config().chainID(), - nonces[BridgeMessage.TOKEN_TRANSFER], + config.chainID(), + nonces[BridgeUtils.TOKEN_TRANSFER], destinationChainID, - BridgeMessage.ETH, + BridgeUtils.ETH, suiAdjustedAmount, msg.sender, recipientAddress ); // increment token transfer nonce - nonces[BridgeMessage.TOKEN_TRANSFER]++; + nonces[BridgeUtils.TOKEN_TRANSFER]++; } /* ========== INTERNAL FUNCTIONS ========== */ @@ -214,13 +224,13 @@ contract SuiBridge is ISuiBridge, CommitteeUpgradeable, PausableUpgradeable { address recipientAddress, uint256 amount ) private whenNotPaused limitNotExceeded(sendingChainID, tokenID, amount) { - address tokenAddress = committee.config().getTokenAddress(tokenID); + 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 == BridgeMessage.ETH) { + if (tokenID == BridgeUtils.ETH) { vault.transferETH(payable(recipientAddress), amount); } else { // transfer tokens from vault to target address diff --git a/bridge/evm/contracts/interfaces/IBridgeCommittee.sol b/bridge/evm/contracts/interfaces/IBridgeCommittee.sol index 7ece51ab6f84e..e1d2d5d73d2cd 100644 --- a/bridge/evm/contracts/interfaces/IBridgeCommittee.sol +++ b/bridge/evm/contracts/interfaces/IBridgeCommittee.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import "../utils/BridgeMessage.sol"; +import "../utils/BridgeUtils.sol"; import "./IBridgeConfig.sol"; /// @title IBridgeCommittee @@ -11,8 +11,8 @@ interface IBridgeCommittee { /// stake of each signer against the required stake of the given message type. /// @dev The function will revert if the total stake of the signers is less than the required stake. /// @param signatures The array of signatures to be verified. - /// @param message The `BridgeMessage.Message` to be verified. - function verifySignatures(bytes[] memory signatures, BridgeMessage.Message memory message) + /// @param message The `BridgeUtils.Message` to be verified. + function verifySignatures(bytes[] memory signatures, BridgeUtils.Message memory message) external view; diff --git a/bridge/evm/contracts/interfaces/IBridgeConfig.sol b/bridge/evm/contracts/interfaces/IBridgeConfig.sol index 290c29ade07b4..1b16f9b7e791e 100644 --- a/bridge/evm/contracts/interfaces/IBridgeConfig.sol +++ b/bridge/evm/contracts/interfaces/IBridgeConfig.sol @@ -10,6 +10,7 @@ interface IBridgeConfig { struct Token { address tokenAddress; uint8 suiDecimal; + bool native; } /* ========== VIEW FUNCTIONS ========== */ @@ -17,30 +18,17 @@ interface IBridgeConfig { /// @notice Returns the address of the token with the given ID. /// @param tokenID The ID of the token. /// @return address of the provided token. - function getTokenAddress(uint8 tokenID) external view returns (address); + function tokenAddressOf(uint8 tokenID) external view returns (address); /// @notice Returns the sui decimal places of the token with the given ID. /// @param tokenID The ID of the token. /// @return amount of sui decimal places of the provided token. - function getSuiDecimal(uint8 tokenID) external view returns (uint8); - - /// @notice Converts the provided token amount to the Sui decimal adjusted amount. - /// @param tokenID The ID of the token to convert. - /// @param amount The ERC20 amount of the tokens to convert to Sui. - /// @return Sui converted amount. - function convertERC20ToSuiDecimal(uint8 tokenID, uint256 amount) - external - view - returns (uint64); - - /// @notice Converts the provided token amount to the ERC20 decimal adjusted amount. - /// @param tokenID The ID of the token to convert. - /// @param amount The Sui amount of the tokens to convert to ERC20 amount. - /// @return ERC20 converted amount. - function convertSuiToERC20Decimal(uint8 tokenID, uint64 amount) - external - view - returns (uint256); + function tokenSuiDecimalOf(uint8 tokenID) external view returns (uint8); + + /// @notice Returns the price of the token with the given ID. + /// @param tokenID The ID of the token. + /// @return price of the provided token. + function tokenPriceOf(uint8 tokenID) external view returns (uint64); /// @notice Returns the supported status of the token with the given ID. /// @param tokenID The ID of the token. @@ -54,4 +42,7 @@ interface IBridgeConfig { /// @notice Returns the chain ID of the bridge. function chainID() external view returns (uint8); + + event TokenAdded(uint8 tokenID, address tokenAddress, uint8 suiDecimal, uint64 tokenPrice); + event TokenPriceUpdated(uint8 tokenID, uint64 tokenPrice); } diff --git a/bridge/evm/contracts/interfaces/IBridgeLimiter.sol b/bridge/evm/contracts/interfaces/IBridgeLimiter.sol index b9b2fc98906c9..20b3e4000f320 100644 --- a/bridge/evm/contracts/interfaces/IBridgeLimiter.sol +++ b/bridge/evm/contracts/interfaces/IBridgeLimiter.sol @@ -28,11 +28,6 @@ interface IBridgeLimiter { /// @param amount The amount in USD transferred. event HourlyTransferAmountUpdated(uint32 hourUpdated, uint256 amount); - /// @dev Emitted when the asset price is updated. - /// @param tokenId The ID of the token. - /// @param price The price of the token in USD with 4 decimal places (e.g. 10000 -> $1) - event AssetPriceUpdated(uint8 tokenId, uint64 price); - /// @dev Emitted when the total limit is updated. /// @param sourceChainID The ID of the source chain. /// @param newLimit The new limit in USD with 4 decimal places (e.g. 10000 -> $1) diff --git a/bridge/evm/contracts/utils/BridgeConfig.sol b/bridge/evm/contracts/utils/BridgeConfig.sol deleted file mode 100644 index 4d071be33a39a..0000000000000 --- a/bridge/evm/contracts/utils/BridgeConfig.sol +++ /dev/null @@ -1,145 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; -import "@openzeppelin/contracts/access/Ownable.sol"; -import "../interfaces/IBridgeConfig.sol"; - -/// @title BridgeConfig -/// @notice This contract manages a registry of supported tokens and supported chain IDs for the SuiBridge. -/// It also provides functions to convert token amounts to Sui decimal adjusted amounts and vice versa. -contract BridgeConfig is IBridgeConfig { - /* ========== STATE VARIABLES ========== */ - - uint8 public chainID; - mapping(uint8 tokenID => Token) public supportedTokens; - mapping(uint8 chainId => bool isSupported) public supportedChains; - - /// @notice Constructor function for the BridgeConfig contract. - /// @dev the provided arrays must have the same length. - /// @param _chainID The ID of the chain this contract is deployed on. - /// @param _supportedTokens The addresses of the supported tokens. - /// @param _supportedChains array of supported chain IDs. - constructor( - uint8 _chainID, - address[] memory _supportedTokens, - uint8[] memory _supportedChains - ) { - require(_supportedTokens.length == 4, "BridgeConfig: Invalid supported token addresses"); - - uint8[] memory _suiDecimals = new uint8[](5); - _suiDecimals[0] = 9; // SUI - _suiDecimals[1] = 8; // wBTC - _suiDecimals[2] = 8; // wETH - _suiDecimals[3] = 6; // USDC - _suiDecimals[4] = 6; // USDT - - // Add SUI as the first supported token - supportedTokens[0] = Token(address(0), _suiDecimals[0]); - - for (uint8 i; i < _supportedTokens.length; i++) { - supportedTokens[i + 1] = Token(_supportedTokens[i], _suiDecimals[i + 1]); - } - - for (uint8 i; i < _supportedChains.length; i++) { - require(_supportedChains[i] != _chainID, "BridgeConfig: Cannot support self"); - supportedChains[_supportedChains[i]] = true; - } - - chainID = _chainID; - } - - /* ========== VIEW FUNCTIONS ========== */ - - /// @notice Returns the address of the token with the given ID. - /// @param tokenID The ID of the token. - /// @return address of the provided token. - function getTokenAddress(uint8 tokenID) public view override returns (address) { - return supportedTokens[tokenID].tokenAddress; - } - - /// @notice Returns the sui decimal places of the token with the given ID. - /// @param tokenID The ID of the token. - /// @return amount of sui decimal places of the provided token. - function getSuiDecimal(uint8 tokenID) public view override returns (uint8) { - return supportedTokens[tokenID].suiDecimal; - } - - /// @notice Returns whether a token is supported in SuiBridge with the given ID. - /// @param tokenID The ID of the token. - /// @return true if the token is supported, false otherwise. - function isTokenSupported(uint8 tokenID) public view override returns (bool) { - return supportedTokens[tokenID].tokenAddress != address(0); - } - - /// @notice Returns whether a chain is supported in SuiBridge with the given ID. - /// @param chainId The ID of the chain. - /// @return true if the chain is supported, false otherwise. - function isChainSupported(uint8 chainId) public view override returns (bool) { - return supportedChains[chainId]; - } - - /// @notice Converts the provided token amount to the Sui decimal adjusted amount. - /// @param tokenID The ID of the token to convert. - /// @param amount The ERC20 amount of the tokens to convert to Sui. - /// @return Sui converted amount. - function convertERC20ToSuiDecimal(uint8 tokenID, uint256 amount) - public - view - override - returns (uint64) - { - uint8 ethDecimal = IERC20Metadata(getTokenAddress(tokenID)).decimals(); - uint8 suiDecimal = getSuiDecimal(tokenID); - - if (ethDecimal == suiDecimal) { - // Ensure converted amount fits within uint64 - require(amount <= type(uint64).max, "BridgeConfig: Amount too large for uint64"); - return uint64(amount); - } - - require(ethDecimal > suiDecimal, "BridgeConfig: Invalid Sui decimal"); - - // Difference in decimal places - uint256 factor = 10 ** (ethDecimal - suiDecimal); - amount = amount / factor; - - // Ensure the converted amount fits within uint64 - require(amount <= type(uint64).max, "BridgeConfig: Amount too large for uint64"); - - return uint64(amount); - } - - /// @notice Converts the provided Sui decimal adjusted amount to the ERC20 token amount. - /// @param tokenID The ID of the token to convert. - /// @param amount The Sui amount of the tokens to convert to ERC20. - /// @return ERC20 converted amount. - function convertSuiToERC20Decimal(uint8 tokenID, uint64 amount) - public - view - override - returns (uint256) - { - uint8 ethDecimal = IERC20Metadata(getTokenAddress(tokenID)).decimals(); - uint8 suiDecimal = getSuiDecimal(tokenID); - - if (suiDecimal == ethDecimal) { - return uint256(amount); - } - - require(ethDecimal > suiDecimal, "BridgeConfig: Invalid Sui decimal"); - - // Difference in decimal places - uint256 factor = 10 ** (ethDecimal - suiDecimal); - return uint256(amount * factor); - } - - /* ========== MODIFIERS ========== */ - - /// @notice Requires the given token to be supported. - /// @param tokenID The ID of the token to check. - modifier tokenSupported(uint8 tokenID) { - require(isTokenSupported(tokenID), "BridgeConfig: Unsupported token"); - _; - } -} diff --git a/bridge/evm/contracts/utils/BridgeMessage.sol b/bridge/evm/contracts/utils/BridgeUtils.sol similarity index 68% rename from bridge/evm/contracts/utils/BridgeMessage.sol rename to bridge/evm/contracts/utils/BridgeUtils.sol index 95f875ba76a4e..d6ade868f5954 100644 --- a/bridge/evm/contracts/utils/BridgeMessage.sol +++ b/bridge/evm/contracts/utils/BridgeUtils.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -/// @title BridgeMessage +/// @title BridgeUtils /// @notice This library defines the message format and constants for the Sui native bridge. It also /// provides functions to encode and decode bridge messages and their payloads. /// @dev This library only utilizes internal functions to enable upgradeability via the OpenZeppelin /// UUPS proxy pattern (external libraries are not supported). -library BridgeMessage { +library BridgeUtils { /* ========== STRUCTS ========== */ /// @dev A struct that represents a bridge message @@ -50,6 +50,7 @@ library BridgeMessage { uint8 public constant UPDATE_BRIDGE_LIMIT = 3; uint8 public constant UPDATE_TOKEN_PRICE = 4; uint8 public constant UPGRADE = 5; + uint8 public constant ADD_EVM_TOKENS = 7; // Message type stake requirements uint32 public constant TRANSFER_STAKE_REQUIRED = 3334; @@ -58,7 +59,8 @@ library BridgeMessage { uint32 public constant UPGRADE_STAKE_REQUIRED = 5001; uint16 public constant BLOCKLIST_STAKE_REQUIRED = 5001; uint32 public constant BRIDGE_LIMIT_STAKE_REQUIRED = 5001; - uint32 public constant TOKEN_PRICE_STAKE_REQUIRED = 5001; + uint32 public constant UPDATE_TOKEN_PRICE_STAKE_REQUIRED = 5001; + uint32 public constant ADD_EVM_TOKENS_STAKE_REQUIRED = 5001; // token Ids uint8 public constant SUI = 0; @@ -105,14 +107,65 @@ library BridgeMessage { } else if (_message.messageType == UPDATE_BRIDGE_LIMIT) { return BRIDGE_LIMIT_STAKE_REQUIRED; } else if (_message.messageType == UPDATE_TOKEN_PRICE) { - return TOKEN_PRICE_STAKE_REQUIRED; + return UPDATE_TOKEN_PRICE_STAKE_REQUIRED; } else if (_message.messageType == UPGRADE) { return UPGRADE_STAKE_REQUIRED; + } else if (_message.messageType == ADD_EVM_TOKENS) { + return ADD_EVM_TOKENS_STAKE_REQUIRED; } else { - revert("BridgeMessage: Invalid message type"); + revert("BridgeUtils: Invalid message type"); } } + /// @notice Converts the provided token amount to the Sui decimal adjusted amount. + /// @param erc20Decimal The erc20 decimal value for the token. + /// @param suiDecimal The sui decimal value for the token. + /// @param amount The ERC20 amount of the tokens to convert to Sui. + /// @return Sui converted amount. + function convertERC20ToSuiDecimal(uint8 erc20Decimal, uint8 suiDecimal, uint256 amount) + internal + pure + returns (uint64) + { + if (erc20Decimal == suiDecimal) { + // Ensure converted amount fits within uint64 + require(amount <= type(uint64).max, "BridgeUtils: Amount too large for uint64"); + return uint64(amount); + } + + require(erc20Decimal > suiDecimal, "BridgeUtils: Invalid Sui decimal"); + + // Difference in decimal places + uint256 factor = 10 ** (erc20Decimal - suiDecimal); + amount = amount / factor; + + // Ensure the converted amount fits within uint64 + require(amount <= type(uint64).max, "BridgeUtils: Amount too large for uint64"); + + return uint64(amount); + } + + /// @notice Converts the provided Sui decimal adjusted amount to the ERC20 token amount. + /// @param erc20Decimal The erc20 decimal value for the token. + /// @param suiDecimal The sui decimal value for the token. + /// @param amount The Sui amount of the tokens to convert to ERC20. + /// @return ERC20 converted amount. + function convertSuiToERC20Decimal(uint8 erc20Decimal, uint8 suiDecimal, uint64 amount) + internal + pure + returns (uint256) + { + if (suiDecimal == erc20Decimal) { + return uint256(amount); + } + + require(erc20Decimal > suiDecimal, "BridgeUtils: Invalid Sui decimal"); + + // Difference in decimal places + uint256 factor = 10 ** (erc20Decimal - suiDecimal); + return uint256(amount * factor); + } + /// @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. @@ -128,15 +181,15 @@ library BridgeMessage { function decodeTokenTransferPayload(bytes memory _payload) internal pure - returns (BridgeMessage.TokenTransferPayload memory) + returns (TokenTransferPayload memory) { - require(_payload.length == 64, "BridgeMessage: TokenTransferPayload must be 64 bytes"); + require(_payload.length == 64, "BridgeUtils: TokenTransferPayload must be 64 bytes"); uint8 senderAddressLength = uint8(_payload[0]); require( senderAddressLength == 32, - "BridgeMessage: Invalid sender address length, Sui address must be 32 bytes" + "BridgeUtils: Invalid sender address length, Sui address must be 32 bytes" ); // used to offset already read bytes @@ -158,7 +211,7 @@ library BridgeMessage { uint8 recipientAddressLength = uint8(_payload[offset++]); require( recipientAddressLength == 20, - "BridgeMessage: Invalid target address length, EVM address must be 20 bytes" + "BridgeUtils: Invalid target address length, EVM address must be 20 bytes" ); // extract target address from payload (35-54) @@ -222,7 +275,7 @@ library BridgeMessage { uint8 membersLength = uint8(_payload[1]); address[] memory members = new address[](membersLength); uint8 offset = 2; - require((_payload.length - offset) % 20 == 0, "BridgeMessage: Invalid payload length"); + require((_payload.length - offset) % 20 == 0, "BridgeUtils: Invalid payload length"); for (uint8 i; i < membersLength; i++) { // Calculate the starting index for each address offset += i * 20; @@ -246,9 +299,9 @@ library BridgeMessage { /// @param _payload The payload to be decoded. /// @return The emergency operation type. function decodeEmergencyOpPayload(bytes memory _payload) internal pure returns (bool) { - require(_payload.length == 1, "BridgeMessage: Invalid payload length"); + require(_payload.length == 1, "BridgeUtils: Invalid payload length"); uint8 emergencyOpCode = uint8(_payload[0]); - require(emergencyOpCode <= 1, "BridgeMessage: Invalid op code"); + require(emergencyOpCode <= 1, "BridgeUtils: Invalid op code"); return emergencyOpCode == 0; } @@ -265,7 +318,7 @@ library BridgeMessage { pure returns (uint8 senderChainID, uint64 newLimit) { - require(_payload.length == 9, "BridgeMessage: Invalid payload length"); + require(_payload.length == 9, "BridgeUtils: Invalid payload length"); senderChainID = uint8(_payload[0]); // Extracts the uint64 value by loading 32 bytes starting just after the first byte. @@ -275,6 +328,24 @@ library BridgeMessage { } } + /// @notice Decodes an upgrade payload from bytes to a proxy address, an implementation address, + /// and call data. + /// @dev The function will revert if the payload length is invalid. The payload is expected to be + /// abi encoded. + /// @param _payload The payload to be decoded. + /// @return proxy the address of the proxy to be upgraded. + /// @return implementation the address of the new implementation contract. + /// @return callData the call data to be used in the upgrade. + function decodeUpgradePayload(bytes memory _payload) + internal + pure + returns (address, address, bytes memory) + { + (address proxy, address implementation, bytes memory callData) = + abi.decode(_payload, (address, address, bytes)); + return (proxy, implementation, callData); + } + /// @notice Decodes an update token price payload from bytes to a token ID and a new price. /// @dev The function will revert if the payload length is invalid. /// Update token price payload is 9 bytes. @@ -298,21 +369,78 @@ library BridgeMessage { } } - /// @notice Decodes an upgrade payload from bytes to a proxy address, an implementation address, - /// and call data. - /// @dev The function will revert if the payload length is invalid. The payload is expected to be - /// abi encoded. + /// @notice Decodes an add token payload from bytes to a token ID, a token address, and a token price. + /// @dev The function will revert if the payload length is invalid. + /// Add token payload is 5 + 2n + 20n + 8n bytes (assuming all arrays are of length n). + /// byte 0 : is native + /// byte 1 : number of token IDs + /// byte 2 -> n : token IDs + /// byte n + 1 : number of addresses + /// bytes n + 2 -> m : addresses + /// byte m + 1 : number of sui decimals + /// bytes m + 2 -> i : sui decimals + /// byte i + 1 : number of prices + /// bytes i + 2 -> j : prices (uint64) /// @param _payload The payload to be decoded. - /// @return proxy the address of the proxy to be upgraded. - /// @return implementation the address of the new implementation contract. - /// @return callData the call data to be used in the upgrade. - function decodeUpgradePayload(bytes memory _payload) + /// @return native whether the token is native to the chain. + /// @return tokenIDs the token ID to be added. + /// @return tokenAddresses the address of the token to be added. + /// @return suiDecimals the Sui decimal places of the tokens to be added. + /// @return tokenPrices the price of the tokens to be added. + function decodeAddTokensPayload(bytes memory _payload) internal pure - returns (address, address, bytes memory) + returns ( + bool native, + uint8[] memory tokenIDs, + address[] memory tokenAddresses, + uint8[] memory suiDecimals, + uint64[] memory tokenPrices + ) { - (address proxy, address implementation, bytes memory callData) = - abi.decode(_payload, (address, address, bytes)); - return (proxy, implementation, callData); + native = _payload[0] != bytes1(0); + + uint8 tokenCount = uint8(_payload[1]); + + // Calculate the starting index for each token ID + uint8 offset = 2; + tokenIDs = new uint8[](tokenCount); + for (uint8 i; i < tokenCount; i++) { + tokenIDs[i] = uint8(_payload[offset++]); + } + + uint8 addressCount = uint8(_payload[offset++]); + tokenAddresses = new address[](addressCount); + for (uint8 i; i < addressCount; i++) { + // Calculate the starting index for each address + address tokenAddress; + // Extract each address + assembly { + tokenAddress := mload(add(add(_payload, 20), offset)) + } + offset += 20; + // Store the extracted address + tokenAddresses[i] = tokenAddress; + } + + uint8 decimalCount = uint8(_payload[offset++]); + suiDecimals = new uint8[](decimalCount); + for (uint8 i; i < decimalCount; i++) { + suiDecimals[i] = uint8(_payload[offset++]); + } + + uint8 priceCount = uint8(_payload[offset++]); + tokenPrices = new uint64[](priceCount); + for (uint8 i; i < priceCount; i++) { + // Calculate the starting index for each price + uint64 tokenPrice; + // Extract each price + assembly { + tokenPrice := shr(192, mload(add(add(_payload, 0x20), offset))) + } + offset += 8; + // Store the extracted price + tokenPrices[i] = tokenPrice; + } } } diff --git a/bridge/evm/contracts/utils/CommitteeUpgradeable.sol b/bridge/evm/contracts/utils/CommitteeUpgradeable.sol index 19a1e4448d134..4e415174d5335 100644 --- a/bridge/evm/contracts/utils/CommitteeUpgradeable.sol +++ b/bridge/evm/contracts/utils/CommitteeUpgradeable.sol @@ -34,15 +34,15 @@ abstract contract CommitteeUpgradeable is /// @notice Enables the upgrade of the inheriting contract by verifying the provided signatures. /// @dev The function will revert if the provided signatures or message is invalid. /// @param signatures The array of signatures to be verified. - /// @param message The BridgeMessage to be verified. - function upgradeWithSignatures(bytes[] memory signatures, BridgeMessage.Message memory message) + /// @param message The BridgeUtils to be verified. + function upgradeWithSignatures(bytes[] memory signatures, BridgeUtils.Message memory message) external nonReentrant - verifyMessageAndSignatures(message, signatures, BridgeMessage.UPGRADE) + verifyMessageAndSignatures(message, signatures, BridgeUtils.UPGRADE) { // decode the upgrade payload (address proxy, address implementation, bytes memory callData) = - BridgeMessage.decodeUpgradePayload(message.payload); + BridgeUtils.decodeUpgradePayload(message.payload); // verify proxy address require(proxy == address(this), "CommitteeUpgradeable: Invalid proxy address"); @@ -51,8 +51,6 @@ abstract contract CommitteeUpgradeable is _upgradeAuthorized = true; // upgrade contract upgradeToAndCall(implementation, callData); // Upgraded event emitted with new implementation address - // reset upgrade authorization - _upgradeAuthorized = false; } /* ========== INTERNAL FUNCTIONS ========== */ @@ -60,7 +58,8 @@ abstract contract CommitteeUpgradeable is /// @notice Authorizes the upgrade of the inheriting contract. /// @dev The _upgradeAuthorized state variable can only be set with the upgradeWithSignatures /// function, meaning that the upgrade can only be authorized by the committee. - function _authorizeUpgrade(address) internal view override { + function _authorizeUpgrade(address) internal override { require(_upgradeAuthorized, "CommitteeUpgradeable: Unauthorized upgrade"); + _upgradeAuthorized = false; } } diff --git a/bridge/evm/contracts/utils/MessageVerifier.sol b/bridge/evm/contracts/utils/MessageVerifier.sol index 5cf05e630b8f8..cfc1ff13db53a 100644 --- a/bridge/evm/contracts/utils/MessageVerifier.sol +++ b/bridge/evm/contracts/utils/MessageVerifier.sol @@ -27,11 +27,11 @@ abstract contract MessageVerifier is Initializable { /// @notice Verifies the provided message and signatures using the BridgeCommittee contract. /// @dev The function will revert if the message type does not match the expected type, /// if the signatures are invalid, or if the message nonce is invalid. - /// @param message The BridgeMessage to be verified. + /// @param message The BridgeUtils to be verified. /// @param signatures The array of signatures to be verified. /// @param messageType The expected message type of the provided message. modifier verifyMessageAndSignatures( - BridgeMessage.Message memory message, + BridgeUtils.Message memory message, bytes[] memory signatures, uint8 messageType ) { @@ -40,7 +40,7 @@ abstract contract MessageVerifier is Initializable { // verify signatures committee.verifySignatures(signatures, message); // increment message type nonce - if (messageType != BridgeMessage.TOKEN_TRANSFER) { + if (messageType != BridgeUtils.TOKEN_TRANSFER) { // verify chain ID require( message.chainID == committee.config().chainID(), "MessageVerifier: Invalid chain ID" diff --git a/bridge/evm/deploy_configs/11155111.json b/bridge/evm/deploy_configs/11155111.json index 315801c0c8e54..a63a804b0e843 100644 --- a/bridge/evm/deploy_configs/11155111.json +++ b/bridge/evm/deploy_configs/11155111.json @@ -1,11 +1,11 @@ { "committeeMemberStake": [2500, 2500, 2500, 2500], - "committeeMembers": ["0x68b43fd906c0b8f024a18c56e06744f7c6157c65", "0xacaef39832cb995c4e049437a3e2ec6a7bad1ab5", "0x8061f127910e8ef56f16a2c411220bad25d61444", "0x508f3f1ff45f4ca3d8e86cdcc91445f00acc59fc"], + "committeeMembers": ["0xf04c72634fc11f7078fc8d1e1260d105a6d9c555", "0x6b1b0fb6bb0a217a0fa8a6a880d886437fbfb9a7", "0x278b75716cfdd84612efb78f8ba99240826dca00", "0xcb5c7457b31509f3451d931dd633115451acc0b0"], "minCommitteeStakeRequired": 10000, "sourceChainId": 11, - "supportedChainIDs": [1, 2, 3], - "supportedChainLimitsInDollars": [100000000000, 100000000000, 100000000000], - "supportedTokens": ["0x0112D7B36726B3077b72DDb457A9f9c94D9cd71c", "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14", "0x80bF6fb931C8eB99Ab32aeD543ACCFd168fd2a47", "0xaA8E23Fb1079EA71e0a56F48a2aA51851D8433D0"], - "tokenPrices": [16200, 620000000, 43000000, 10000, 10000], + "supportedChainIDs": [1, 2], + "supportedChainLimitsInDollars": [100000000000, 100000000000], + "supportedTokens": ["0x0000000000000000000000000000000000000000", "0x0112D7B36726B3077b72DDb457A9f9c94D9cd71c", "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14", "0x80bF6fb931C8eB99Ab32aeD543ACCFd168fd2a47", "0xaA8E23Fb1079EA71e0a56F48a2aA51851D8433D0"], + "tokenPrices": [162000000, 6200000000000, 430000000000, 100000000, 100000000], "wETHAddress": "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14" } diff --git a/bridge/evm/deploy_configs/31337.json b/bridge/evm/deploy_configs/31337.json index d4dcebdeae9f1..265995c7ccf1e 100644 --- a/bridge/evm/deploy_configs/31337.json +++ b/bridge/evm/deploy_configs/31337.json @@ -3,9 +3,8 @@ "committeeMembers": ["0x68b43fd906c0b8f024a18c56e06744f7c6157c65", "0xacaef39832cb995c4e049437a3e2ec6a7bad1ab5", "0x8061f127910e8ef56f16a2c411220bad25d61444", "0x508f3f1ff45f4ca3d8e86cdcc91445f00acc59fc"], "minCommitteeStakeRequired": 10000, "sourceChainId": 0, - "supportedChainIDs": [1], - "supportedChainLimitsInDollars": [1000000], + "supportedChainIDs": [1, 2, 3], + "supportedChainLimitsInDollars": [1000000000000000, 1000000000000000, 1000000000000000], "supportedTokens": [], - "tokenPrices": [12800, 432518900, 25969600, 10000], - "wETHAddress": "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14" + "tokenPrices": [128000000, 4325189000000, 259696000000, 100000000, 100000000] } \ No newline at end of file diff --git a/bridge/evm/deploy_configs/example.json b/bridge/evm/deploy_configs/example.json index 7d5b55507032d..88cf3de5a74be 100644 --- a/bridge/evm/deploy_configs/example.json +++ b/bridge/evm/deploy_configs/example.json @@ -5,7 +5,7 @@ "sourceChainId": 0, "supportedChainIDs": [1], "supportedChainLimitsInDollars": [1000000], - "supportedTokens": ["0x000", "0x000", "0x000", "0x000"], - "tokenPrices": [12800, 432518900, 25969600, 10000], - "wETHAddress": "0x000" + "supportedTokens": ["0x00", "0x00", "0x00", "0x00"], + "tokenPrices": [128000000, 4325189000000, 259696000000, 100000000], + "wETHAddress": "0x00" } \ No newline at end of file diff --git a/bridge/evm/foundry.toml b/bridge/evm/foundry.toml index ce3a874b48adf..b2a3ebfec2a6d 100644 --- a/bridge/evm/foundry.toml +++ b/bridge/evm/foundry.toml @@ -1,13 +1,13 @@ [profile.default] src = 'contracts' test = 'test' -no_match_test = "testMock" +no_match_test = "testSkip" out = 'out' libs = ['lib'] solc = "0.8.20" build_info = true extra_output = ["storageLayout"] -fs_permissions = [{ access = "read", path = "./"}] +fs_permissions = [{ access = "read", path = "/"}] gas_reports = ["SuiBridge"] [fmt] line_length = 100 diff --git a/bridge/evm/script/deploy_bridge.s.sol b/bridge/evm/script/deploy_bridge.s.sol index 7a899d5becda7..4df43f5384a52 100644 --- a/bridge/evm/script/deploy_bridge.s.sol +++ b/bridge/evm/script/deploy_bridge.s.sol @@ -4,10 +4,11 @@ pragma solidity ^0.8.20; import "forge-std/Script.sol"; // import "openzeppelin-foundry-upgrades/Upgrades.sol"; import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol"; +import "openzeppelin-foundry-upgrades/Options.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; import "../contracts/BridgeCommittee.sol"; import "../contracts/BridgeVault.sol"; -import "../contracts/utils/BridgeConfig.sol"; +import "../contracts/BridgeConfig.sol"; import "../contracts/BridgeLimiter.sol"; import "../contracts/SuiBridge.sol"; import "../test/mocks/MockTokens.sol"; @@ -16,98 +17,141 @@ contract DeployBridge is Script { function run() external { uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); vm.startBroadcast(deployerPrivateKey); - string memory chainID = Strings.toString(block.chainid); + bytes32 chainIDHash = keccak256(abi.encode(chainID)); + bool isLocal = chainIDHash != keccak256(abi.encode("11155111")) + && chainIDHash != keccak256(abi.encode("1")); string memory root = vm.projectRoot(); string memory path = string.concat(root, "/deploy_configs/", chainID, ".json"); + // If this is local deployment, we override the path if OVERRIDE_CONFIG_PATH is set. + // This is useful in integration tests where config path is not fixed. + if (isLocal) { + path = vm.envOr("OVERRIDE_CONFIG_PATH", path); + } + + console.log("config path: ", path); string memory json = vm.readFile(path); bytes memory bytesJson = vm.parseJson(json); - DeployConfig memory config = abi.decode(bytesJson, (DeployConfig)); - - // TODO: validate config values before deploying + DeployConfig memory deployConfig = abi.decode(bytesJson, (DeployConfig)); // if deploying to local network, deploy mock tokens - if (keccak256(abi.encode(chainID)) == keccak256(abi.encode("31337"))) { + if (isLocal) { + console.log("Deploying mock tokens for local network"); // deploy WETH - config.WETH = address(new WETH()); + deployConfig.WETH = address(new WETH()); // deploy mock tokens MockWBTC wBTC = new MockWBTC(); MockUSDC USDC = new MockUSDC(); MockUSDT USDT = new MockUSDT(); - // update config with mock addresses - config.supportedTokens = new address[](4); + // update deployConfig with mock addresses + deployConfig.supportedTokens = new address[](5); // In BridgeConfig.sol `supportedTokens is shifted by one // and the first token is SUI. - config.supportedTokens[0] = address(wBTC); - config.supportedTokens[1] = config.WETH; - config.supportedTokens[2] = address(USDC); - config.supportedTokens[3] = address(USDT); + deployConfig.supportedTokens[0] = address(0); + deployConfig.supportedTokens[1] = address(wBTC); + deployConfig.supportedTokens[2] = deployConfig.WETH; + deployConfig.supportedTokens[3] = address(USDC); + deployConfig.supportedTokens[4] = address(USDT); } + // TODO: validate config values before deploying + // convert supported chains from uint256 to uint8[] - uint8[] memory supportedChainIDs = new uint8[](config.supportedChainIDs.length); - for (uint256 i; i < config.supportedChainIDs.length; i++) { - supportedChainIDs[i] = uint8(config.supportedChainIDs[i]); + uint8[] memory supportedChainIDs = new uint8[](deployConfig.supportedChainIDs.length); + for (uint256 i; i < deployConfig.supportedChainIDs.length; i++) { + supportedChainIDs[i] = uint8(deployConfig.supportedChainIDs[i]); } // deploy bridge config + // price of Sui (id = 0) should not be included in tokenPrices + require( + deployConfig.supportedTokens.length == deployConfig.tokenPrices.length, + "supportedTokens.length + 1 != tokenPrices.length" + ); - BridgeConfig bridgeConfig = - new BridgeConfig(uint8(config.sourceChainId), config.supportedTokens, supportedChainIDs); - - // deploy Bridge Committee + // deploy Bridge Committee =================================================================== // convert committeeMembers stake from uint256 to uint16[] - uint16[] memory committeeMemberStake = new uint16[](config.committeeMemberStake.length); - for (uint256 i; i < config.committeeMemberStake.length; i++) { - committeeMemberStake[i] = uint16(config.committeeMemberStake[i]); + uint16[] memory committeeMemberStake = + new uint16[](deployConfig.committeeMemberStake.length); + for (uint256 i; i < deployConfig.committeeMemberStake.length; i++) { + committeeMemberStake[i] = uint16(deployConfig.committeeMemberStake[i]); } + Options memory opts; + opts.unsafeSkipAllChecks = true; + address bridgeCommittee = Upgrades.deployUUPSProxy( "BridgeCommittee.sol", abi.encodeCall( BridgeCommittee.initialize, ( - address(bridgeConfig), - config.committeeMembers, + deployConfig.committeeMembers, committeeMemberStake, - uint16(config.minCommitteeStakeRequired) + uint16(deployConfig.minCommitteeStakeRequired) ) - ) + ), + opts ); - // deploy vault + // deploy bridge config ===================================================================== - BridgeVault vault = new BridgeVault(config.WETH); + // convert token prices from uint256 to uint64 + uint64[] memory tokenPrices = new uint64[](deployConfig.tokenPrices.length); + for (uint256 i; i < deployConfig.tokenPrices.length; i++) { + tokenPrices[i] = uint64(deployConfig.tokenPrices[i]); + } + + address bridgeConfig = Upgrades.deployUUPSProxy( + "BridgeConfig.sol", + abi.encodeCall( + BridgeConfig.initialize, + ( + address(bridgeCommittee), + uint8(deployConfig.sourceChainId), + deployConfig.supportedTokens, + tokenPrices, + supportedChainIDs + ) + ), + opts + ); + + // initialize config in the bridge committee + BridgeCommittee(bridgeCommittee).initializeConfig(address(bridgeConfig)); + + // deploy vault ============================================================================= - // deploy limiter + BridgeVault vault = new BridgeVault(deployConfig.WETH); + + // deploy limiter =========================================================================== // convert chain limits from uint256 to uint64[] - uint64[] memory chainLimits = new uint64[](config.supportedChainLimitsInDollars.length); - for (uint256 i; i < config.supportedChainLimitsInDollars.length; i++) { - chainLimits[i] = uint64(config.supportedChainLimitsInDollars[i]); + uint64[] memory chainLimits = + new uint64[](deployConfig.supportedChainLimitsInDollars.length); + for (uint256 i; i < deployConfig.supportedChainLimitsInDollars.length; i++) { + chainLimits[i] = uint64(deployConfig.supportedChainLimitsInDollars[i]); } address limiter = Upgrades.deployUUPSProxy( "BridgeLimiter.sol", abi.encodeCall( - BridgeLimiter.initialize, - (bridgeCommittee, config.tokenPrices, supportedChainIDs, chainLimits) - ) + BridgeLimiter.initialize, (bridgeCommittee, supportedChainIDs, chainLimits) + ), + opts ); uint8[] memory _destinationChains = new uint8[](1); _destinationChains[0] = 1; - // deploy Sui Bridge + // deploy Sui Bridge ======================================================================== address suiBridge = Upgrades.deployUUPSProxy( "SuiBridge.sol", - abi.encodeCall( - SuiBridge.initialize, (bridgeCommittee, address(vault), limiter, config.WETH) - ) + abi.encodeCall(SuiBridge.initialize, (bridgeCommittee, address(vault), limiter)), + opts ); // transfer vault ownership to bridge @@ -117,21 +161,21 @@ contract DeployBridge is Script { instance.transferOwnership(suiBridge); // print deployed addresses for post deployment setup - console.log("[Deployed] BridgeConfig:", address(bridgeConfig)); + console.log("[Deployed] BridgeConfig:", bridgeConfig); console.log("[Deployed] SuiBridge:", suiBridge); console.log("[Deployed] BridgeLimiter:", limiter); console.log("[Deployed] BridgeCommittee:", bridgeCommittee); console.log("[Deployed] BridgeVault:", address(vault)); - console.log("[Deployed] BTC:", bridgeConfig.getTokenAddress(1)); - console.log("[Deployed] ETH:", bridgeConfig.getTokenAddress(2)); - console.log("[Deployed] USDC:", bridgeConfig.getTokenAddress(3)); - console.log("[Deployed] USDT:", bridgeConfig.getTokenAddress(4)); + console.log("[Deployed] BTC:", BridgeConfig(bridgeConfig).tokenAddressOf(1)); + console.log("[Deployed] ETH:", BridgeConfig(bridgeConfig).tokenAddressOf(2)); + console.log("[Deployed] USDC:", BridgeConfig(bridgeConfig).tokenAddressOf(3)); + console.log("[Deployed] USDT:", BridgeConfig(bridgeConfig).tokenAddressOf(4)); vm.stopBroadcast(); } // used to ignore for forge coverage - function test() public {} + function testSkip() public {} } /// check the following for guidelines on updating deploy_configs and references: diff --git a/bridge/evm/test/BridgeBaseTest.t.sol b/bridge/evm/test/BridgeBaseTest.t.sol index 875e9c4da2797..4232e5e30326c 100644 --- a/bridge/evm/test/BridgeBaseTest.t.sol +++ b/bridge/evm/test/BridgeBaseTest.t.sol @@ -6,7 +6,7 @@ import "../contracts/BridgeCommittee.sol"; import "../contracts/BridgeVault.sol"; import "../contracts/BridgeLimiter.sol"; import "../contracts/SuiBridge.sol"; -import "../contracts/utils/BridgeConfig.sol"; +import "../contracts/BridgeConfig.sol"; contract BridgeBaseTest is Test { address committeeMemberA; @@ -27,20 +27,28 @@ contract BridgeBaseTest is Test { address deployer; + // token addresses on mainnet address wETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; address USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; address USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; address wBTC = 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599; + address wBTCWhale = 0x6daB3bCbFb336b29d06B9C793AEF7eaA57888922; address USDCWhale = 0x51eDF02152EBfb338e03E30d65C15fBf06cc9ECC; + address USDTWhale = 0xa7C0D36c4698981FAb42a7d8c783674c6Fe2592d; - uint256 SUI_PRICE = 12800; - uint256 BTC_PRICE = 432518900; - uint256 ETH_PRICE = 25969600; - uint256 USDC_PRICE = 10000; + uint64 USD_VALUE_MULTIPLIER = 100000000; // 8 DP accuracy + + uint64 SUI_PRICE = 1_28000000; + uint64 BTC_PRICE = 43251_89000000; + uint64 ETH_PRICE = 2596_96000000; + uint64 USDC_PRICE = 1_00000000; + uint64[] tokenPrices; + address[] supportedTokens; + uint8[] supportedChains; uint8 public chainID = 1; - uint64 totalLimit = 10000000000; + uint64 totalLimit = 1_000_000 * USD_VALUE_MULTIPLIER; uint16 minStakeRequired = 10000; BridgeCommittee public committee; @@ -48,9 +56,6 @@ contract BridgeBaseTest is Test { BridgeVault public vault; BridgeLimiter public limiter; BridgeConfig public config; - address[] public supportedTokens; - uint8[] public supportedChains; - uint256[] public tokenPrices; function setUpBridgeTest() public { vm.createSelectFork( @@ -73,17 +78,9 @@ contract BridgeBaseTest is Test { vm.deal(bridgerB, 1 ether); deployer = address(1); vm.startPrank(deployer); - address[] memory _supportedTokens = new address[](4); - _supportedTokens[0] = wBTC; - _supportedTokens[1] = wETH; - _supportedTokens[2] = USDC; - _supportedTokens[3] = USDT; - uint8[] memory _supportedChains = new uint8[](1); - _supportedChains[0] = 0; - config = new BridgeConfig(chainID, _supportedTokens, _supportedChains); - supportedTokens = _supportedTokens; - supportedChains = _supportedChains; + // deploy committee ===================================================================== + committee = new BridgeCommittee(); address[] memory _committee = new address[](5); uint16[] memory _stake = new uint16[](5); _committee[0] = committeeMemberA; @@ -96,72 +93,53 @@ contract BridgeBaseTest is Test { _stake[2] = 1000; _stake[3] = 2002; _stake[4] = 4998; - committee = new BridgeCommittee(); - // Test fail initialize: committee and stake arrays must be of the same length - address[] memory _committeeNotSameLength = new address[](5); - _committeeNotSameLength[0] = committeeMemberA; - _committeeNotSameLength[1] = committeeMemberB; - _committeeNotSameLength[2] = committeeMemberC; - _committeeNotSameLength[3] = committeeMemberD; - _committeeNotSameLength[4] = committeeMemberE; - - uint16[] memory _stakeNotSameLength = new uint16[](4); - _stakeNotSameLength[0] = 1000; - _stakeNotSameLength[1] = 1000; - _stakeNotSameLength[2] = 1000; - _stakeNotSameLength[3] = 2002; - - vm.expectRevert( - bytes("BridgeCommittee: Committee and stake arrays must be of the same length") + committee.initialize(_committee, _stake, minStakeRequired); + + // deploy config ===================================================================== + config = new BridgeConfig(); + supportedTokens = new address[](5); + supportedTokens[0] = address(0); + supportedTokens[1] = wBTC; + supportedTokens[2] = wETH; + supportedTokens[3] = USDC; + supportedTokens[4] = USDT; + supportedChains = new uint8[](1); + supportedChains[0] = 0; + tokenPrices = new uint64[](5); + tokenPrices[0] = SUI_PRICE; + tokenPrices[1] = BTC_PRICE; + tokenPrices[2] = ETH_PRICE; + tokenPrices[3] = USDC_PRICE; + tokenPrices[4] = USDC_PRICE; + + config.initialize( + address(committee), chainID, supportedTokens, tokenPrices, supportedChains ); - committee.initialize( - address(config), _committeeNotSameLength, _stakeNotSameLength, minStakeRequired - ); + // initialize config in the bridge committee + committee.initializeConfig(address(config)); - // Test fail initialize: Committee Duplicate Committee Member - address[] memory _committeeDuplicateCommitteeMember = new address[](5); - _committeeDuplicateCommitteeMember[0] = committeeMemberA; - _committeeDuplicateCommitteeMember[1] = committeeMemberB; - _committeeDuplicateCommitteeMember[2] = committeeMemberC; - _committeeDuplicateCommitteeMember[3] = committeeMemberD; - _committeeDuplicateCommitteeMember[4] = committeeMemberA; - - uint16[] memory _stakeDuplicateCommitteeMember = new uint16[](5); - _stakeDuplicateCommitteeMember[0] = 1000; - _stakeDuplicateCommitteeMember[1] = 1000; - _stakeDuplicateCommitteeMember[2] = 1000; - _stakeDuplicateCommitteeMember[3] = 2002; - _stakeDuplicateCommitteeMember[4] = 1000; - - vm.expectRevert(bytes("BridgeCommittee: Duplicate committee member")); - committee.initialize( - address(config), - _committeeDuplicateCommitteeMember, - _stakeDuplicateCommitteeMember, - minStakeRequired - ); + // deploy vault ===================================================================== - committee.initialize(address(config), _committee, _stake, minStakeRequired); vault = new BridgeVault(wETH); - uint256[] memory _tokenPrices = new uint256[](4); - _tokenPrices[0] = SUI_PRICE; - _tokenPrices[1] = BTC_PRICE; - _tokenPrices[2] = ETH_PRICE; - _tokenPrices[3] = USDC_PRICE; - tokenPrices = _tokenPrices; + + // deploy limiter ===================================================================== + limiter = new BridgeLimiter(); uint64[] memory chainLimits = new uint64[](1); chainLimits[0] = totalLimit; - limiter.initialize(address(committee), _tokenPrices, _supportedChains, chainLimits); + limiter.initialize(address(committee), supportedChains, chainLimits); + + // deploy bridge ===================================================================== + bridge = new SuiBridge(); - bridge.initialize(address(committee), address(vault), address(limiter), wETH); + bridge.initialize(address(committee), address(vault), address(limiter)); vault.transferOwnership(address(bridge)); limiter.transferOwnership(address(bridge)); } - function testMock() public {} + function testSkip() public {} // Helper function to get the signature components from an address function getSignature(bytes32 digest, uint256 privateKey) public pure returns (bytes memory) { diff --git a/bridge/evm/test/BridgeCommitteeTest.t.sol b/bridge/evm/test/BridgeCommitteeTest.t.sol index c2308304fb005..2357743f18397 100644 --- a/bridge/evm/test/BridgeCommitteeTest.t.sol +++ b/bridge/evm/test/BridgeCommitteeTest.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.20; import "./BridgeBaseTest.t.sol"; -import "../contracts/utils/BridgeMessage.sol"; +import "../contracts/utils/BridgeUtils.sol"; contract BridgeCommitteeTest is BridgeBaseTest { // This function is called before each unit test @@ -37,17 +37,77 @@ contract BridgeCommitteeTest is BridgeBaseTest { assertEq(committee.nonces(4), 0); } + function testBridgeCommitteeInitializationLength() public { + BridgeCommittee _committee = new BridgeCommittee(); + address[] memory _committeeMembers = new address[](256); + + for (uint160 i = 0; i < 256; i++) { + _committeeMembers[i] = address(i); + } + + vm.expectRevert(bytes("BridgeCommittee: Committee length must be less than 256")); + _committee.initialize(_committeeMembers, new uint16[](256), minStakeRequired); + } + + function testBridgeCommitteeInitializeConfig() public { + vm.expectRevert(bytes("BridgeCommittee: Config already initialized")); + // Initialize the committee with the config contract + committee.initializeConfig(address(101)); + } + + function testBridgeFailInitialization() public { + // Test fail initialize: Committee Duplicate Committee Member + BridgeCommittee _committee = new BridgeCommittee(); + address[] memory _committeeDuplicateCommitteeMember = new address[](5); + _committeeDuplicateCommitteeMember[0] = committeeMemberA; + _committeeDuplicateCommitteeMember[1] = committeeMemberB; + _committeeDuplicateCommitteeMember[2] = committeeMemberC; + _committeeDuplicateCommitteeMember[3] = committeeMemberD; + _committeeDuplicateCommitteeMember[4] = committeeMemberA; + + uint16[] memory _stakeDuplicateCommitteeMember = new uint16[](5); + _stakeDuplicateCommitteeMember[0] = 1000; + _stakeDuplicateCommitteeMember[1] = 1000; + _stakeDuplicateCommitteeMember[2] = 1000; + _stakeDuplicateCommitteeMember[3] = 2002; + _stakeDuplicateCommitteeMember[4] = 1000; + + vm.expectRevert(bytes("BridgeCommittee: Duplicate committee member")); + _committee.initialize( + _committeeDuplicateCommitteeMember, _stakeDuplicateCommitteeMember, minStakeRequired + ); + + address[] memory _committeeNotSameLength = new address[](5); + _committeeNotSameLength[0] = committeeMemberA; + _committeeNotSameLength[1] = committeeMemberB; + _committeeNotSameLength[2] = committeeMemberC; + _committeeNotSameLength[3] = committeeMemberD; + _committeeNotSameLength[4] = committeeMemberE; + + uint16[] memory _stakeNotSameLength = new uint16[](4); + _stakeNotSameLength[0] = 1000; + _stakeNotSameLength[1] = 1000; + _stakeNotSameLength[2] = 1000; + _stakeNotSameLength[3] = 2002; + + vm.expectRevert( + bytes("BridgeCommittee: Committee and stake arrays must be of the same length") + ); + + _committee.initialize(_committeeNotSameLength, _stakeNotSameLength, minStakeRequired); + } + function testVerifySignaturesWithValidSignatures() public { // Create a message - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.TOKEN_TRANSFER, + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.TOKEN_TRANSFER, version: 1, nonce: 1, chainID: chainID, payload: "0x0" }); - bytes memory messageBytes = BridgeMessage.encodeMessage(message); + bytes memory messageBytes = BridgeUtils.encodeMessage(message); bytes32 messageHash = keccak256(messageBytes); @@ -65,15 +125,15 @@ contract BridgeCommitteeTest is BridgeBaseTest { function testVerifySignaturesWithInvalidSignatures() public { // Create a message - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.TOKEN_TRANSFER, + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.TOKEN_TRANSFER, version: 1, nonce: 1, chainID: chainID, payload: "0x0" }); - bytes memory messageBytes = BridgeMessage.encodeMessage(message); + bytes memory messageBytes = BridgeUtils.encodeMessage(message); bytes32 messageHash = keccak256(messageBytes); @@ -91,15 +151,15 @@ contract BridgeCommitteeTest is BridgeBaseTest { function testVerifySignaturesDuplicateSignature() public { // Create a message - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.TOKEN_TRANSFER, + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.TOKEN_TRANSFER, version: 1, nonce: 1, chainID: chainID, payload: "0x0" }); - bytes memory messageBytes = BridgeMessage.encodeMessage(message); + bytes memory messageBytes = BridgeUtils.encodeMessage(message); bytes32 messageHash = keccak256(messageBytes); bytes[] memory signatures = new bytes[](4); @@ -111,7 +171,7 @@ contract BridgeCommitteeTest is BridgeBaseTest { signatures[3] = getSignature(messageHash, committeeMemberPkC); // Call the verifySignatures function and expect it to revert - vm.expectRevert(bytes("BridgeCommittee: Insufficient stake amount")); + vm.expectRevert(bytes("BridgeCommittee: Duplicate signature provided")); committee.verifySignatures(signatures, message); } @@ -122,14 +182,14 @@ contract BridgeCommitteeTest is BridgeBaseTest { bytes memory payload = abi.encode(uint8(0), _blocklist); // Create a message with wrong nonce - BridgeMessage.Message memory messageWrongNonce = BridgeMessage.Message({ - messageType: BridgeMessage.BLOCKLIST, + BridgeUtils.Message memory messageWrongNonce = BridgeUtils.Message({ + messageType: BridgeUtils.BLOCKLIST, version: 1, nonce: 0, chainID: chainID, payload: payload }); - bytes memory messageBytes = BridgeMessage.encodeMessage(messageWrongNonce); + bytes memory messageBytes = BridgeUtils.encodeMessage(messageWrongNonce); bytes32 messageHash = keccak256(messageBytes); bytes[] memory signatures = new bytes[](4); @@ -149,14 +209,14 @@ contract BridgeCommitteeTest is BridgeBaseTest { bytes memory payload = abi.encode(uint8(0), _blocklist); // Create a message with wrong messageType - BridgeMessage.Message memory messageWrongMessageType = BridgeMessage.Message({ - messageType: BridgeMessage.TOKEN_TRANSFER, + BridgeUtils.Message memory messageWrongMessageType = BridgeUtils.Message({ + messageType: BridgeUtils.TOKEN_TRANSFER, version: 1, nonce: 0, chainID: chainID, payload: payload }); - bytes memory messageBytes = BridgeMessage.encodeMessage(messageWrongMessageType); + bytes memory messageBytes = BridgeUtils.encodeMessage(messageWrongMessageType); bytes32 messageHash = keccak256(messageBytes); bytes[] memory signatures = new bytes[](4); @@ -176,14 +236,14 @@ contract BridgeCommitteeTest is BridgeBaseTest { bytes memory payload = abi.encode(uint8(0), _blocklist); // Create a message - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.BLOCKLIST, + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.BLOCKLIST, version: 1, nonce: 0, chainID: chainID, payload: payload }); - bytes memory messageBytes = BridgeMessage.encodeMessage(message); + bytes memory messageBytes = BridgeUtils.encodeMessage(message); bytes32 messageHash = keccak256(messageBytes); bytes[] memory signatures = new bytes[](4); @@ -201,15 +261,15 @@ contract BridgeCommitteeTest is BridgeBaseTest { payload = abi.encodePacked(payload, committeeMemberA); // Create a message - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.BLOCKLIST, + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.BLOCKLIST, version: 1, nonce: 0, chainID: chainID, payload: payload }); - bytes memory messageBytes = BridgeMessage.encodeMessage(message); + bytes memory messageBytes = BridgeUtils.encodeMessage(message); bytes32 messageHash = keccak256(messageBytes); bytes[] memory signatures = new bytes[](4); @@ -223,17 +283,17 @@ contract BridgeCommitteeTest is BridgeBaseTest { assertTrue(committee.blocklist(committeeMemberA)); - // verify CommitteeMemberA's signature is no longer valid - vm.expectRevert(bytes("BridgeCommittee: Insufficient stake amount")); // update message message.nonce = 1; // reconstruct signatures - messageBytes = BridgeMessage.encodeMessage(message); + messageBytes = BridgeUtils.encodeMessage(message); messageHash = keccak256(messageBytes); signatures[0] = getSignature(messageHash, committeeMemberPkA); signatures[1] = getSignature(messageHash, committeeMemberPkB); signatures[2] = getSignature(messageHash, committeeMemberPkC); signatures[3] = getSignature(messageHash, committeeMemberPkD); + // verify CommitteeMemberA's signature is no longer valid + vm.expectRevert(bytes("BridgeCommittee: Signer is blocklisted")); // re-verify signatures committee.verifySignatures(signatures, message); } @@ -243,15 +303,15 @@ contract BridgeCommitteeTest is BridgeBaseTest { bytes memory payload = abi.encode(committeeMemberA); // Create a message - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.UPGRADE, + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.UPGRADE, version: 1, nonce: 0, chainID: chainID, payload: payload }); - bytes memory messageBytes = BridgeMessage.encodeMessage(message); + bytes memory messageBytes = BridgeUtils.encodeMessage(message); bytes32 messageHash = keccak256(messageBytes); bytes[] memory signatures = new bytes[](4); @@ -263,7 +323,7 @@ contract BridgeCommitteeTest is BridgeBaseTest { signatures[2] = getSignature(messageHash, committeeMemberPkC); signatures[3] = getSignature(messageHash, committeeMemberPkF); - vm.expectRevert(bytes("BridgeCommittee: Insufficient stake amount")); + vm.expectRevert(bytes("BridgeCommittee: Signer has no stake")); committee.verifySignatures(signatures, message); } @@ -277,15 +337,15 @@ contract BridgeCommitteeTest is BridgeBaseTest { payload = abi.encodePacked(payload, committeeMemberA); // Create a message - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.BLOCKLIST, + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.BLOCKLIST, version: 1, nonce: 1, chainID: chainID, payload: payload }); - bytes memory messageBytes = BridgeMessage.encodeMessage(message); + bytes memory messageBytes = BridgeUtils.encodeMessage(message); bytes32 messageHash = keccak256(messageBytes); bytes[] memory signatures = new bytes[](4); @@ -314,19 +374,19 @@ contract BridgeCommitteeTest is BridgeBaseTest { _stake[2] = 2500; _stake[3] = 2500; committee = new BridgeCommittee(); - committee.initialize(address(config), _committee, _stake, minStakeRequired); + committee.initialize(_committee, _stake, minStakeRequired); bytes memory payload = hex"010268b43fd906c0b8f024a18c56e06744f7c6157c65acaef39832cb995c4e049437a3e2ec6a7bad1ab5"; // Create blocklist message - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.BLOCKLIST, + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.BLOCKLIST, version: 1, nonce: 68, chainID: 2, payload: payload }); - bytes memory encodedMessage = BridgeMessage.encodeMessage(message); + bytes memory encodedMessage = BridgeUtils.encodeMessage(message); bytes memory expectedEncodedMessage = hex"5355495f4252494447455f4d4553534147450101000000000000004402010268b43fd906c0b8f024a18c56e06744f7c6157c65acaef39832cb995c4e049437a3e2ec6a7bad1ab5"; @@ -338,7 +398,6 @@ contract BridgeCommitteeTest is BridgeBaseTest { address[] memory _committee = new address[](4); uint16[] memory _stake = new uint16[](4); uint8 chainID = 11; - config = new BridgeConfig(chainID, supportedTokens, supportedChains); _committee[0] = 0x68B43fD906C0B8F024a18C56e06744F7c6157c65; _committee[1] = 0xaCAEf39832CB995c4E049437A3E2eC6a7bad1Ab5; _committee[2] = 0x8061f127910e8eF56F16a2C411220BaD25D61444; @@ -348,20 +407,26 @@ contract BridgeCommitteeTest is BridgeBaseTest { _stake[2] = 2500; _stake[3] = 2500; committee = new BridgeCommittee(); - committee.initialize(address(config), _committee, _stake, minStakeRequired); + committee.initialize(_committee, _stake, minStakeRequired); + config = new BridgeConfig(); + config.initialize( + address(committee), chainID, supportedTokens, tokenPrices, supportedChains + ); + + committee.initializeConfig(address(config)); + assertEq(committee.blocklist(0x68B43fD906C0B8F024a18C56e06744F7c6157c65), false); // blocklist 1 member 02321ede33d2c2d7a8a152f275a1484edef2098f034121a602cb7d767d38680aa4 ("0x68B43fD906C0B8F024a18C56e06744F7c6157c65") - bytes memory payload = - hex"000168b43fd906c0b8f024a18c56e06744f7c6157c65"; - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.BLOCKLIST, + bytes memory payload = hex"000168b43fd906c0b8f024a18c56e06744f7c6157c65"; + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.BLOCKLIST, version: 1, nonce: 0, chainID: chainID, payload: payload }); - bytes memory encodedMessage = BridgeMessage.encodeMessage(message); + bytes memory encodedMessage = BridgeUtils.encodeMessage(message); bytes memory expectedEncodedMessage = hex"5355495f4252494447455f4d455353414745010100000000000000000b000168b43fd906c0b8f024a18c56e06744f7c6157c65"; @@ -384,14 +449,14 @@ contract BridgeCommitteeTest is BridgeBaseTest { // unblocklist 1 member 02321ede33d2c2d7a8a152f275a1484edef2098f034121a602cb7d767d38680aa4 ("0x68B43fD906C0B8F024a18C56e06744F7c6157c65") payload = hex"010168b43fd906c0b8f024a18c56e06744f7c6157c65"; - message = BridgeMessage.Message({ - messageType: BridgeMessage.BLOCKLIST, + message = BridgeUtils.Message({ + messageType: BridgeUtils.BLOCKLIST, version: 1, nonce: 1, chainID: chainID, payload: payload }); - encodedMessage = BridgeMessage.encodeMessage(message); + encodedMessage = BridgeUtils.encodeMessage(message); expectedEncodedMessage = hex"5355495f4252494447455f4d455353414745010100000000000000010b010168b43fd906c0b8f024a18c56e06744f7c6157c65"; @@ -407,7 +472,7 @@ contract BridgeCommitteeTest is BridgeBaseTest { signatures[2] = hex"62b36dab0d2c10f74d84b5f9838435c396cca1f3c4939eb4df82d1c72430e7ec2a030a980a9514beaeda6dffdc5e177b7edbd18543979f488d8fd09dba753a5500"; - vm.expectRevert(bytes("BridgeCommittee: Insufficient stake amount")); + vm.expectRevert(bytes("BridgeCommittee: Signer is blocklisted")); committee.verifySignatures(signatures, message); // use sig from a unblocklisted validator diff --git a/bridge/evm/test/BridgeConfigTest.t.sol b/bridge/evm/test/BridgeConfigTest.t.sol index db3a164ffe1d6..dcc8ee46dab85 100644 --- a/bridge/evm/test/BridgeConfigTest.t.sol +++ b/bridge/evm/test/BridgeConfigTest.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import "./mocks/MockTokens.sol"; import "./BridgeBaseTest.t.sol"; +import "./mocks/MockTokens.sol"; contract BridgeConfigTest is BridgeBaseTest { function setUp() public { @@ -10,118 +10,493 @@ contract BridgeConfigTest is BridgeBaseTest { } function testBridgeConfigInitialization() public { - assertTrue(config.getTokenAddress(1) == wBTC); - assertTrue(config.getTokenAddress(2) == wETH); - assertTrue(config.getTokenAddress(3) == USDC); - assertTrue(config.getTokenAddress(4) == USDT); - assertEq(config.getSuiDecimal(0), 9); - assertEq(config.getSuiDecimal(1), 8); - assertEq(config.getSuiDecimal(2), 8); - assertEq(config.getSuiDecimal(3), 6); - assertEq(config.getSuiDecimal(4), 6); + assertTrue(config.tokenAddressOf(1) == wBTC); + assertTrue(config.tokenAddressOf(2) == wETH); + assertTrue(config.tokenAddressOf(3) == USDC); + assertTrue(config.tokenAddressOf(4) == USDT); + assertEq(config.tokenSuiDecimalOf(0), 9); + assertEq(config.tokenSuiDecimalOf(1), 8); + assertEq(config.tokenSuiDecimalOf(2), 8); + assertEq(config.tokenSuiDecimalOf(3), 6); + assertEq(config.tokenSuiDecimalOf(4), 6); + assertEq(config.tokenPriceOf(0), SUI_PRICE); + assertEq(config.tokenPriceOf(1), BTC_PRICE); + assertEq(config.tokenPriceOf(2), ETH_PRICE); + assertEq(config.tokenPriceOf(3), USDC_PRICE); + assertEq(config.tokenPriceOf(4), USDC_PRICE); assertEq(config.chainID(), chainID); assertTrue(config.supportedChains(0)); } function testGetAddress() public { - assertEq(config.getTokenAddress(1), wBTC); + assertEq(config.tokenAddressOf(1), wBTC); } - function testconvertERC20ToSuiDecimalAmountTooLargeForUint64() public { - vm.expectRevert(bytes("BridgeConfig: Amount too large for uint64")); - config.convertERC20ToSuiDecimal(BridgeMessage.ETH, type(uint256).max); + function testIsTokenSupported() public { + assertTrue(config.isTokenSupported(1)); + assertTrue(!config.isTokenSupported(0)); } - function testconvertERC20ToSuiDecimalInvalidSuiDecimal() public { - vm.startPrank(address(bridge)); - address smallUSDC = address(new MockSmallUSDC()); - address[] memory _supportedTokens = new address[](4); - _supportedTokens[0] = wBTC; - _supportedTokens[1] = wETH; - _supportedTokens[2] = smallUSDC; - _supportedTokens[3] = USDT; - uint8[] memory _supportedDestinationChains = new uint8[](1); - _supportedDestinationChains[0] = 0; - BridgeConfig newBridgeConfig = - new BridgeConfig(chainID, _supportedTokens, _supportedDestinationChains); - vm.expectRevert(bytes("BridgeConfig: Invalid Sui decimal")); - newBridgeConfig.convertERC20ToSuiDecimal(3, 100); + function testTokenSuiDecimalOf() public { + assertEq(config.tokenSuiDecimalOf(1), 8); } - function testconvertSuiToERC20DecimalInvalidSuiDecimal() public { - vm.startPrank(address(bridge)); - address smallUSDC = address(new MockSmallUSDC()); - address[] memory _supportedTokens = new address[](4); - _supportedTokens[0] = wBTC; - _supportedTokens[1] = wETH; - _supportedTokens[2] = smallUSDC; - _supportedTokens[3] = USDT; - uint8[] memory _supportedDestinationChains = new uint8[](1); - _supportedDestinationChains[0] = 0; - BridgeConfig newBridgeConfig = - new BridgeConfig(chainID, _supportedTokens, _supportedDestinationChains); + function testAddTokensWithSignatures() public { + MockUSDC _newToken = new MockUSDC(); + + // Create update tokens payload + bool _isNative = true; + uint8 _numTokenIDs = 1; + uint8 tokenID1 = 10; + uint8 _numAddresses = 1; + address address1 = address(_newToken); + uint8 _numSuiDecimals = 1; + uint8 suiDecimal1 = 6; + uint8 _numPrices = 1; + uint64 price1 = 100_000 * USD_VALUE_MULTIPLIER; + + bytes memory payload = abi.encodePacked( + _isNative, + _numTokenIDs, + tokenID1, + _numAddresses, + address1, + _numSuiDecimals, + suiDecimal1, + _numPrices, + price1 + ); + + // Create transfer message + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.ADD_EVM_TOKENS, + version: 1, + nonce: 0, + chainID: 1, + payload: payload + }); + + bytes memory encodedMessage = BridgeUtils.encodeMessage(message); + + bytes32 messageHash = keccak256(encodedMessage); + + bytes[] memory signatures = new bytes[](4); + + signatures[0] = getSignature(messageHash, committeeMemberPkA); + signatures[1] = getSignature(messageHash, committeeMemberPkB); + signatures[2] = getSignature(messageHash, committeeMemberPkC); + signatures[3] = getSignature(messageHash, committeeMemberPkD); + + // test token ID 10 is not supported + assertFalse(config.isTokenSupported(10)); + config.addTokensWithSignatures(signatures, message); + assertTrue(config.isTokenSupported(10)); + assertEq(config.tokenAddressOf(10), address1); + assertEq(config.tokenSuiDecimalOf(10), 6); + assertEq(config.tokenPriceOf(10), 100_000 * USD_VALUE_MULTIPLIER); + } + + function testAddTokensAddressFailure() public { + MockUSDC _newToken = new MockUSDC(); + + // Create update tokens payload + bool _isNative = true; + uint8 _numTokenIDs = 1; + uint8 tokenID1 = 10; + uint8 _numAddresses = 1; + address address1 = address(0); + uint8 _numSuiDecimals = 1; + uint8 suiDecimal1 = 6; + uint8 _numPrices = 1; + uint64 price1 = 100_000 * USD_VALUE_MULTIPLIER; + + bytes memory payload = abi.encodePacked( + _isNative, + _numTokenIDs, + tokenID1, + _numAddresses, + address1, + _numSuiDecimals, + suiDecimal1, + _numPrices, + price1 + ); + + // Create Add evm token message + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.ADD_EVM_TOKENS, + version: 1, + nonce: 0, + chainID: 1, + payload: payload + }); + + bytes memory encodedMessage = BridgeUtils.encodeMessage(message); + + bytes32 messageHash = keccak256(encodedMessage); + + bytes[] memory signatures = new bytes[](4); + + signatures[0] = getSignature(messageHash, committeeMemberPkA); + signatures[1] = getSignature(messageHash, committeeMemberPkB); + signatures[2] = getSignature(messageHash, committeeMemberPkC); + signatures[3] = getSignature(messageHash, committeeMemberPkD); + + // address should fail because the address supplied in the message is 0 + vm.expectRevert(bytes("BridgeConfig: Invalid token address")); + config.addTokensWithSignatures(signatures, message); + } + + function testAddTokensSuiDecimalFailure() public { + MockUSDC _newToken = new MockUSDC(); + + // Create add tokens payload + bool _isNative = true; + uint8 _numTokenIDs = 1; + uint8 tokenID1 = 10; + uint8 _numAddresses = 1; + address address1 = address(_newToken); + uint8 _numSuiDecimals = 1; + uint8 suiDecimal1 = 10; + uint8 _numPrices = 1; + uint64 price1 = 100_000 * USD_VALUE_MULTIPLIER; + + bytes memory payload = abi.encodePacked( + _isNative, + _numTokenIDs, + tokenID1, + _numAddresses, + address1, + _numSuiDecimals, + suiDecimal1, + _numPrices, + price1 + ); + + // Create transfer message + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.ADD_EVM_TOKENS, + version: 1, + nonce: 0, + chainID: 1, + payload: payload + }); + + bytes memory encodedMessage = BridgeUtils.encodeMessage(message); + + bytes32 messageHash = keccak256(encodedMessage); + + bytes[] memory signatures = new bytes[](4); + + signatures[0] = getSignature(messageHash, committeeMemberPkA); + signatures[1] = getSignature(messageHash, committeeMemberPkB); + signatures[2] = getSignature(messageHash, committeeMemberPkC); + signatures[3] = getSignature(messageHash, committeeMemberPkD); + + // add token shoudl fail because the sui decimal is greater than the eth decimal vm.expectRevert(bytes("BridgeConfig: Invalid Sui decimal")); - newBridgeConfig.convertSuiToERC20Decimal(3, 100); + config.addTokensWithSignatures(signatures, message); } - function testIsTokenSupported() public { - assertTrue(config.isTokenSupported(1)); - assertTrue(!config.isTokenSupported(0)); + function testAddTokensPriceFailure() public { + MockUSDC _newToken = new MockUSDC(); + + // Create update tokens payload + bool _isNative = true; + uint8 _numTokenIDs = 1; + uint8 tokenID1 = 10; + uint8 _numAddresses = 1; + address address1 = address(_newToken); + uint8 _numSuiDecimals = 1; + uint8 suiDecimal1 = 10; + uint8 _numPrices = 1; + uint64 price1 = 0; + + bytes memory payload = abi.encodePacked( + _isNative, + _numTokenIDs, + tokenID1, + _numAddresses, + address1, + _numSuiDecimals, + suiDecimal1, + _numPrices, + price1 + ); + + // Create transfer message + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.ADD_EVM_TOKENS, + version: 1, + nonce: 0, + chainID: 1, + payload: payload + }); + + bytes memory encodedMessage = BridgeUtils.encodeMessage(message); + + bytes32 messageHash = keccak256(encodedMessage); + + bytes[] memory signatures = new bytes[](4); + + signatures[0] = getSignature(messageHash, committeeMemberPkA); + signatures[1] = getSignature(messageHash, committeeMemberPkB); + signatures[2] = getSignature(messageHash, committeeMemberPkC); + signatures[3] = getSignature(messageHash, committeeMemberPkD); + + vm.expectRevert(bytes("BridgeConfig: Invalid token price")); + config.addTokensWithSignatures(signatures, message); } - function testGetSuiDecimal() public { - assertEq(config.getSuiDecimal(1), 8); + function testUpdateTokenPriceWithSignatures() public { + // Create update tokens payload + uint8 tokenID = BridgeUtils.ETH; + uint64 price = 100_000 * USD_VALUE_MULTIPLIER; + + bytes memory payload = abi.encodePacked(tokenID, price); + + // Create transfer message + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.UPDATE_TOKEN_PRICE, + version: 1, + nonce: 0, + chainID: 1, + payload: payload + }); + + bytes memory encodedMessage = BridgeUtils.encodeMessage(message); + + bytes32 messageHash = keccak256(encodedMessage); + + bytes[] memory signatures = new bytes[](4); + + signatures[0] = getSignature(messageHash, committeeMemberPkA); + signatures[1] = getSignature(messageHash, committeeMemberPkB); + signatures[2] = getSignature(messageHash, committeeMemberPkC); + signatures[3] = getSignature(messageHash, committeeMemberPkD); + + // test ETH price + assertEq(config.tokenPriceOf(BridgeUtils.ETH), ETH_PRICE); + config.updateTokenPriceWithSignatures(signatures, message); + assertEq(config.tokenPriceOf(BridgeUtils.ETH), 100_000 * USD_VALUE_MULTIPLIER); } - function testconvertERC20ToSuiDecimal() public { - // ETH - assertEq(IERC20Metadata(wETH).decimals(), 18); - uint256 ethAmount = 10 ether; - uint64 suiAmount = config.convertERC20ToSuiDecimal(BridgeMessage.ETH, ethAmount); - assertEq(suiAmount, 10_000_000_00); // 10 * 10 ^ 8 - - // USDC - assertEq(IERC20Metadata(USDC).decimals(), 6); - ethAmount = 50_000_000; // 50 USDC - suiAmount = config.convertERC20ToSuiDecimal(BridgeMessage.USDC, ethAmount); - assertEq(suiAmount, ethAmount); - - // USDT - assertEq(IERC20Metadata(USDT).decimals(), 6); - ethAmount = 60_000_000; // 60 USDT - suiAmount = config.convertERC20ToSuiDecimal(BridgeMessage.USDT, ethAmount); - assertEq(suiAmount, ethAmount); - - // BTC - assertEq(IERC20Metadata(wBTC).decimals(), 8); - ethAmount = 2_00_000_000; // 2 BTC - suiAmount = config.convertERC20ToSuiDecimal(BridgeMessage.BTC, ethAmount); - assertEq(suiAmount, ethAmount); + // An e2e update token price regression test covering message ser/de + function testUpdateTokenPricesRegressionTest() public { + address[] memory _committee = new address[](4); + uint16[] memory _stake = new uint16[](4); + _committee[0] = 0x68B43fD906C0B8F024a18C56e06744F7c6157c65; + _committee[1] = 0xaCAEf39832CB995c4E049437A3E2eC6a7bad1Ab5; + _committee[2] = 0x8061f127910e8eF56F16a2C411220BaD25D61444; + _committee[3] = 0x508F3F1ff45F4ca3D8e86CDCC91445F00aCC59fC; + _stake[0] = 2500; + _stake[1] = 2500; + _stake[2] = 2500; + _stake[3] = 2500; + committee = new BridgeCommittee(); + committee.initialize(_committee, _stake, minStakeRequired); + committee.initializeConfig(address(config)); + vault = new BridgeVault(wETH); + + uint64[] memory totalLimits = new uint64[](1); + totalLimits[0] = 1000000; + uint8[] memory _supportedDestinationChains = new uint8[](1); + _supportedDestinationChains[0] = 0; + skip(2 days); + limiter = new BridgeLimiter(); + limiter.initialize(address(committee), _supportedDestinationChains, totalLimits); + bridge = new SuiBridge(); + bridge.initialize(address(committee), address(vault), address(limiter)); + vault.transferOwnership(address(bridge)); + limiter.transferOwnership(address(bridge)); + + bytes memory payload = hex"01000000003b9aca00"; + + // Create update token price message + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.UPDATE_TOKEN_PRICE, + version: 1, + nonce: 266, + chainID: 3, + payload: payload + }); + bytes memory encodedMessage = BridgeUtils.encodeMessage(message); + bytes memory expectedEncodedMessage = + hex"5355495f4252494447455f4d4553534147450401000000000000010a0301000000003b9aca00"; + + assertEq(encodedMessage, expectedEncodedMessage); + } + + // An e2e update token price regression test covering message ser/de and signature verification + function testUpdateTokenPriceRegressionTestWithSigVerficiation() public { + address[] memory _committee = new address[](4); + _committee[0] = 0x68B43fD906C0B8F024a18C56e06744F7c6157c65; + _committee[1] = 0xaCAEf39832CB995c4E049437A3E2eC6a7bad1Ab5; + _committee[2] = 0x8061f127910e8eF56F16a2C411220BaD25D61444; + _committee[3] = 0x508F3F1ff45F4ca3D8e86CDCC91445F00aCC59fC; + uint8 sendingChainID = 1; + uint8[] memory _supportedChains = new uint8[](1); + _supportedChains[0] = sendingChainID; + uint8 chainID = 11; + uint16[] memory _stake = new uint16[](4); + _stake[0] = 2500; + _stake[1] = 2500; + _stake[2] = 2500; + _stake[3] = 2500; + committee = new BridgeCommittee(); + committee.initialize(_committee, _stake, minStakeRequired); + uint8[] memory _supportedDestinationChains = new uint8[](1); + _supportedDestinationChains[0] = sendingChainID; + + config = new BridgeConfig(); + config.initialize( + address(committee), chainID, supportedTokens, tokenPrices, _supportedChains + ); + + committee.initializeConfig(address(config)); + + vault = new BridgeVault(wETH); + skip(2 days); + + uint64[] memory totalLimits = new uint64[](1); + totalLimits[0] = 1000000; + + limiter = new BridgeLimiter(); + limiter.initialize(address(committee), _supportedDestinationChains, totalLimits); + bridge = new SuiBridge(); + bridge.initialize(address(committee), address(vault), address(limiter)); + vault.transferOwnership(address(bridge)); + limiter.transferOwnership(address(bridge)); + + // BTC -> 600_000_000 ($60k) + bytes memory payload = hex"010000000023c34600"; + + // Create update token price message + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.UPDATE_TOKEN_PRICE, + version: 1, + nonce: 0, + chainID: chainID, + payload: payload + }); + bytes memory encodedMessage = BridgeUtils.encodeMessage(message); + bytes memory expectedEncodedMessage = + hex"5355495f4252494447455f4d455353414745040100000000000000000b010000000023c34600"; + + assertEq(encodedMessage, expectedEncodedMessage); + + bytes[] memory signatures = new bytes[](3); + + signatures[0] = + hex"eb81068c2214c01bf5d89e6bd748c0d184ae68f74d365174657053af916dcd335960737eb724560a3481bb77b7df4169d8305a034143e1c749fd9f9bcda6cc1601"; + signatures[1] = + hex"116ad7d7bb705374328f85613020777d636fa092f98aa59a1d58f12f36d96f0e7aacfeb8ff356289da8d0d75278ccad8c19ec878db0b836f96ab544e91de1fed01"; + signatures[2] = + hex"b0229b50b0fe3fd4cdb05b31c7689d99e3181f9f11069cb457d73112985865ff504d9a9959c367d02b18b2d78312a012f194798499198410880351ab0a241a0c00"; + + committee.verifySignatures(signatures, message); + + config.updateTokenPriceWithSignatures(signatures, message); + assertEq(config.tokenPrices(BridgeUtils.BTC), 600_000_000); } - function testconvertSuiToERC20Decimal() public { - // ETH - assertEq(IERC20Metadata(wETH).decimals(), 18); - uint64 suiAmount = 11_000_000_00; // 11 eth - uint256 ethAmount = config.convertSuiToERC20Decimal(BridgeMessage.ETH, suiAmount); - assertEq(ethAmount, 11 ether); - - // USDC - assertEq(IERC20Metadata(USDC).decimals(), 6); - suiAmount = 50_000_000; // 50 USDC - ethAmount = config.convertSuiToERC20Decimal(BridgeMessage.USDC, suiAmount); - assertEq(suiAmount, ethAmount); - - // USDT - assertEq(IERC20Metadata(USDT).decimals(), 6); - suiAmount = 50_000_000; // 50 USDT - ethAmount = config.convertSuiToERC20Decimal(BridgeMessage.USDT, suiAmount); - assertEq(suiAmount, ethAmount); - - // BTC - assertEq(IERC20Metadata(wBTC).decimals(), 8); - suiAmount = 3_000_000_00; // 3 BTC - ethAmount = config.convertSuiToERC20Decimal(BridgeMessage.BTC, suiAmount); - assertEq(suiAmount, ethAmount); + function testAddTokensRegressionTest() public { + address[] memory _committee = new address[](4); + uint16[] memory _stake = new uint16[](4); + _committee[0] = 0x68B43fD906C0B8F024a18C56e06744F7c6157c65; + _committee[1] = 0xaCAEf39832CB995c4E049437A3E2eC6a7bad1Ab5; + _committee[2] = 0x8061f127910e8eF56F16a2C411220BaD25D61444; + _committee[3] = 0x508F3F1ff45F4ca3D8e86CDCC91445F00aCC59fC; + _stake[0] = 2500; + _stake[1] = 2500; + _stake[2] = 2500; + _stake[3] = 2500; + committee = new BridgeCommittee(); + committee.initialize(_committee, _stake, minStakeRequired); + uint8[] memory _supportedDestinationChains = new uint8[](1); + _supportedDestinationChains[0] = 0; + config = new BridgeConfig(); + config.initialize( + address(committee), 12, supportedTokens, tokenPrices, _supportedDestinationChains + ); + committee.initializeConfig(address(config)); + vault = new BridgeVault(wETH); + + uint64[] memory totalLimits = new uint64[](1); + totalLimits[0] = 1000000; + + skip(2 days); + limiter = new BridgeLimiter(); + limiter.initialize(address(committee), _supportedDestinationChains, totalLimits); + bridge = new SuiBridge(); + bridge.initialize(address(committee), address(vault), address(limiter)); + vault.transferOwnership(address(bridge)); + limiter.transferOwnership(address(bridge)); + + bytes memory payload = + hex"0103636465036b175474e89094c44da98b954eedeac495271d0fae7ab96520de3a18e5e111b5eaab095312d7fe84c18360217d8f7ab5e7c516566761ea12ce7f9d720305060703000000003b9aca00000000007735940000000000b2d05e00"; + + ( + bool native, + uint8[] memory tokenIDs, + address[] memory tokenAddresses, + uint8[] memory suiDecimals, + uint64[] memory tokenPrices + ) = BridgeUtils.decodeAddTokensPayload(payload); + + assertEq(native, true); + assertEq(tokenIDs.length, 3); + assertEq(tokenIDs[0], 99); + assertEq(tokenIDs[1], 100); + assertEq(tokenIDs[2], 101); + + assertEq(tokenAddresses.length, 3); + assertEq(tokenAddresses[0], 0x6B175474E89094C44Da98b954EedeAC495271d0F); // dai + assertEq(tokenAddresses[1], 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84); // lido + assertEq(tokenAddresses[2], 0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72); // ENS + + assertEq(suiDecimals.length, 3); + assertEq(suiDecimals[0], 5); + assertEq(suiDecimals[1], 6); + assertEq(suiDecimals[2], 7); + + assertEq(tokenPrices.length, 3); + assertEq(tokenPrices[0], 1_000_000_000); + assertEq(tokenPrices[1], 2_000_000_000); + assertEq(tokenPrices[2], 3_000_000_000); + + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.ADD_EVM_TOKENS, + version: 1, + nonce: 0, + chainID: 12, + payload: payload + }); + bytes memory encodedMessage = BridgeUtils.encodeMessage(message); + bytes memory expectedEncodedMessage = + hex"5355495f4252494447455f4d455353414745070100000000000000000c0103636465036b175474e89094c44da98b954eedeac495271d0fae7ab96520de3a18e5e111b5eaab095312d7fe84c18360217d8f7ab5e7c516566761ea12ce7f9d720305060703000000003b9aca00000000007735940000000000b2d05e00"; + + assertEq(encodedMessage, expectedEncodedMessage); + + bytes[] memory signatures = new bytes[](3); + + signatures[0] = + hex"98b064aa172d0a66142f2fc45d9cd3255fb096cb92e0fcc9be4688b425aad6b53251c9044de4475e64e85b38b32cd3c813a8010281b00811d40fce9b3b372f2200"; + signatures[1] = + hex"275037d70185c835b0d1ee70a118d1cc5da90db2468fab1fa24517eeec3055d814f0ca65db7e6274dbda92d33c9df914db7ada4901a283ec1d3e8c126827923600"; + signatures[2] = + hex"ebb6669c8fb4b000fd41dde6e464c44c009ddcb47c05e7e5ea3deba71b21bd28156b23b6e7813a0603c57553ce484771c142ba6c981c4753035655e89006c0ee01"; + + config.addTokensWithSignatures(signatures, message); + + assertEq(config.tokenPriceOf(99), 1_000_000_000); + assertEq(config.tokenPriceOf(100), 2_000_000_000); + assertEq(config.tokenPriceOf(101), 3_000_000_000); + assertEq(config.tokenSuiDecimalOf(99), 5); + assertEq(config.tokenSuiDecimalOf(100), 6); + assertEq(config.tokenSuiDecimalOf(101), 7); + assertEq(config.tokenAddressOf(99), 0x6B175474E89094C44Da98b954EedeAC495271d0F); + assertEq(config.tokenAddressOf(100), 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84); + assertEq(config.tokenAddressOf(101), 0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72); } } diff --git a/bridge/evm/test/BridgeGasTest.t.sol b/bridge/evm/test/BridgeGasTest.t.sol new file mode 100644 index 0000000000000..ca4a69a0f6cc8 --- /dev/null +++ b/bridge/evm/test/BridgeGasTest.t.sol @@ -0,0 +1,283 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "./BridgeBaseTest.t.sol"; + +contract BridgeGasTest is BridgeBaseTest { + // This function is called before each unit test + function setUp() public { + setUpBridgeTest(); + } + + // Uncomment to run these tests (must run tests with --via-ir flag) + + // function testTransferBridgedTokensWith7Signatures() public { + // // define committee with 50 members + // address[] memory _committee = new address[](56); + // uint256[] memory pks = new uint256[](56); + // uint16[] memory _stake = new uint16[](56); + // for (uint256 i = 0; i < 56; i++) { + // string memory name = string(abi.encodePacked("committeeMember", i)); + // (address member, uint256 pk) = makeAddrAndKey(name); + // _committee[i] = member; + // pks[i] = pk; + // // 1 member with 2500 stake + // if (i == 55) { + // _stake[i] = 2500; + // // 50 members with 100 stake (total: 5000) + // } else if (i < 50) { + // _stake[i] = 100; + // // 5 members with 500 stake (total: 2500) + // } else { + // _stake[i] = 500; + // } + // } + // committee = new BridgeCommittee(); + // committee.initialize(_committee, _stake, minStakeRequired); + // committee.initializeConfig(address(config)); + // uint256[] memory tokenPrices = new uint256[](4); + // tokenPrices[0] = 10000; // SUI PRICE + // tokenPrices[1] = 10000; // BTC PRICE + // tokenPrices[2] = 10000; // ETH PRICE + // tokenPrices[3] = 10000; // USDC PRICE + // uint64[] memory totalLimits = new uint64[](1); + // totalLimits[0] = 1000000; + // skip(2 days); + // SuiBridge _bridge = new SuiBridge(); + // _bridge.initialize(address(committee), address(vault), address(limiter), wETH); + // changePrank(address(bridge)); + // limiter.transferOwnership(address(_bridge)); + // vault.transferOwnership(address(_bridge)); + // bridge = _bridge; + + // // Fill vault with WETH + // changePrank(deployer); + // IWETH9(wETH).deposit{value: 10 ether}(); + // IERC20(wETH).transfer(address(vault), 10 ether); + + // // transfer bridged tokens with 7 signatures + // // Create transfer payload + // uint8 senderAddressLength = 32; + // bytes memory senderAddress = abi.encode(0); + // uint8 targetChain = chainID; + // uint8 recipientAddressLength = 20; + // address recipientAddress = bridgerA; + // uint8 tokenID = BridgeUtils.ETH; + // uint64 amount = 100000000; // 1 ether in sui decimals + // bytes memory payload = abi.encodePacked( + // senderAddressLength, + // senderAddress, + // targetChain, + // recipientAddressLength, + // recipientAddress, + // tokenID, + // amount + // ); + + // // Create transfer message + // BridgeUtils.Message memory message = BridgeUtils.Message({ + // messageType: BridgeUtils.TOKEN_TRANSFER, + // version: 1, + // nonce: 1, + // chainID: 0, + // payload: payload + // }); + + // bytes memory encodedMessage = BridgeUtils.encodeMessage(message); + + // bytes32 messageHash = keccak256(encodedMessage); + + // bytes[] memory signatures = new bytes[](7); + + // uint8 index; + // for (uint256 i = 50; i < 55; i++) { + // signatures[index++] = getSignature(messageHash, pks[i]); + // } + // signatures[5] = getSignature(messageHash, pks[55]); + // signatures[6] = getSignature(messageHash, pks[0]); + + // bridge.transferBridgedTokensWithSignatures(signatures, message); + // } + + // function testTransferBridgedTokensWith26Signatures() public { + // // define committee with 50 members + // address[] memory _committee = new address[](56); + // uint256[] memory pks = new uint256[](56); + // uint16[] memory _stake = new uint16[](56); + // for (uint256 i = 0; i < 56; i++) { + // string memory name = string(abi.encodePacked("committeeMember", i)); + // (address member, uint256 pk) = makeAddrAndKey(name); + // _committee[i] = member; + // pks[i] = pk; + // // 1 member with 2500 stake + // if (i == 55) { + // _stake[i] = 2500; + // // 50 members with 100 stake (total: 5000) + // } else if (i < 50) { + // _stake[i] = 100; + // // 5 members with 500 stake (total: 2500) + // } else { + // _stake[i] = 500; + // } + // } + // committee = new BridgeCommittee(); + // committee.initialize(_committee, _stake, minStakeRequired); + // committee.initializeConfig(address(config)); + // uint256[] memory tokenPrices = new uint256[](4); + // tokenPrices[0] = 10000; // SUI PRICE + // tokenPrices[1] = 10000; // BTC PRICE + // tokenPrices[2] = 10000; // ETH PRICE + // tokenPrices[3] = 10000; // USDC PRICE + // uint64[] memory totalLimits = new uint64[](1); + // totalLimits[0] = 1000000; + // skip(2 days); + // SuiBridge _bridge = new SuiBridge(); + // _bridge.initialize(address(committee), address(vault), address(limiter), wETH); + // changePrank(address(bridge)); + // limiter.transferOwnership(address(_bridge)); + // vault.transferOwnership(address(_bridge)); + // bridge = _bridge; + + // // Fill vault with WETH + // changePrank(deployer); + // IWETH9(wETH).deposit{value: 10 ether}(); + // IERC20(wETH).transfer(address(vault), 10 ether); + + // // transfer bridged tokens with 26 signatures + + // // Create transfer payload + // uint8 senderAddressLength = 32; + // bytes memory senderAddress = abi.encode(0); + // uint8 targetChain = chainID; + // uint8 recipientAddressLength = 20; + // address recipientAddress = bridgerA; + // uint8 tokenID = BridgeUtils.ETH; + // uint64 amount = 100000000; // 1 ether in sui decimals + // bytes memory payload = abi.encodePacked( + // senderAddressLength, + // senderAddress, + // targetChain, + // recipientAddressLength, + // recipientAddress, + // tokenID, + // amount + // ); + + // // Create transfer message + // BridgeUtils.Message memory message = BridgeUtils.Message({ + // messageType: BridgeUtils.TOKEN_TRANSFER, + // version: 1, + // nonce: 2, + // chainID: 0, + // payload: payload + // }); + + // bytes memory encodedMessage = BridgeUtils.encodeMessage(message); + + // bytes32 messageHash = keccak256(encodedMessage); + + // bytes[] memory signatures = new bytes[](25); + + // uint256 index = 0; + // // add 5 committee members with 100 stake + // for (uint256 i = 50; i < 55; i++) { + // signatures[index++] = getSignature(messageHash, pks[i]); + // } + // // add last committee member with 2500 stake + // signatures[5] = getSignature(messageHash, pks[55]); + + // // add 20 committee members with 100 stake + // for (uint256 i = 0; i < 20; i++) { + // signatures[index++] = getSignature(messageHash, pks[i]); + // } + + // bridge.transferBridgedTokensWithSignatures(signatures, message); + // } + + // function testTransferBridgedTokensWith56Signatures() public { + // // define committee with 50 members + // address[] memory _committee = new address[](56); + // uint256[] memory pks = new uint256[](56); + // uint16[] memory _stake = new uint16[](56); + // for (uint256 i = 0; i < 56; i++) { + // string memory name = string(abi.encodePacked("committeeMember", i)); + // (address member, uint256 pk) = makeAddrAndKey(name); + // _committee[i] = member; + // pks[i] = pk; + // // 1 member with 2500 stake + // if (i == 55) { + // _stake[i] = 2500; + // // 50 members with 100 stake (total: 5000) + // } else if (i < 50) { + // _stake[i] = 100; + // // 5 members with 500 stake (total: 2500) + // } else { + // _stake[i] = 500; + // } + // } + // committee = new BridgeCommittee(); + // committee.initialize(_committee, _stake, minStakeRequired); + // committee.initializeConfig(address(config)); + // uint256[] memory tokenPrices = new uint256[](4); + // tokenPrices[0] = 10000; // SUI PRICE + // tokenPrices[1] = 10000; // BTC PRICE + // tokenPrices[2] = 10000; // ETH PRICE + // tokenPrices[3] = 10000; // USDC PRICE + // uint64[] memory totalLimits = new uint64[](1); + // totalLimits[0] = 1000000; + // skip(2 days); + // SuiBridge _bridge = new SuiBridge(); + // _bridge.initialize(address(committee), address(vault), address(limiter), wETH); + // changePrank(address(bridge)); + // limiter.transferOwnership(address(_bridge)); + // vault.transferOwnership(address(_bridge)); + // bridge = _bridge; + + // // Fill vault with WETH + // changePrank(deployer); + // IWETH9(wETH).deposit{value: 10 ether}(); + // IERC20(wETH).transfer(address(vault), 10 ether); + + // // transfer bridged tokens with 56 signatures + + // // Create transfer payload + // uint8 senderAddressLength = 32; + // bytes memory senderAddress = abi.encode(0); + // uint8 targetChain = chainID; + // uint8 recipientAddressLength = 20; + // address recipientAddress = bridgerA; + // uint8 tokenID = BridgeUtils.ETH; + // uint64 amount = 100000000; // 1 ether in sui decimals + // bytes memory payload = abi.encodePacked( + // senderAddressLength, + // senderAddress, + // targetChain, + // recipientAddressLength, + // recipientAddress, + // tokenID, + // amount + // ); + + // // Create transfer message + // BridgeUtils.Message memory message = BridgeUtils.Message({ + // messageType: BridgeUtils.TOKEN_TRANSFER, + // version: 1, + // nonce: 3, + // chainID: 0, + // payload: payload + // }); + + // bytes memory encodedMessage = BridgeUtils.encodeMessage(message); + + // bytes32 messageHash = keccak256(encodedMessage); + + // bytes[] memory signatures = new bytes[](56); + + // // get all signatures + // for (uint256 i = 0; i < 56; i++) { + // signatures[i] = getSignature(messageHash, pks[i]); + // } + + // bridge.transferBridgedTokensWithSignatures(signatures, message); + // } +} diff --git a/bridge/evm/test/BridgeLimiterTest.t.sol b/bridge/evm/test/BridgeLimiterTest.t.sol index 7c942327638f7..7d96880ef4229 100644 --- a/bridge/evm/test/BridgeLimiterTest.t.sol +++ b/bridge/evm/test/BridgeLimiterTest.t.sol @@ -15,17 +15,13 @@ contract BridgeLimiterTest is BridgeBaseTest { } function testBridgeLimiterInitialization() public { - assertEq(limiter.tokenPrices(0), SUI_PRICE); - assertEq(limiter.tokenPrices(1), BTC_PRICE); - assertEq(limiter.tokenPrices(2), ETH_PRICE); - assertEq(limiter.tokenPrices(3), USDC_PRICE); assertEq(limiter.oldestChainTimestamp(supportedChainID), uint32(block.timestamp / 1 hours)); assertEq(limiter.chainLimits(supportedChainID), totalLimit); } function testCalculateAmountInUSD() public { uint8 tokenID = 1; // wBTC - uint256 wBTCAmount = 100000000; // wBTC has 8 decimals + uint256 wBTCAmount = 1_00000000; // wBTC has 8 decimals uint256 actual = limiter.calculateAmountInUSD(tokenID, wBTCAmount); assertEq(actual, BTC_PRICE); tokenID = 2; @@ -33,7 +29,7 @@ contract BridgeLimiterTest is BridgeBaseTest { actual = limiter.calculateAmountInUSD(tokenID, ethAmount); assertEq(actual, ETH_PRICE); tokenID = 3; - uint256 usdcAmount = 1000000; // USDC has 6 decimals + uint256 usdcAmount = 1_000000; // USDC has 6 decimals actual = limiter.calculateAmountInUSD(tokenID, usdcAmount); assertEq(actual, USDC_PRICE); } @@ -41,19 +37,19 @@ contract BridgeLimiterTest is BridgeBaseTest { function testCalculateWindowLimit() public { changePrank(address(bridge)); uint8 tokenID = 3; - uint256 amount = 1000000; // USDC has 6 decimals + uint256 amount = 1_000000; // USDC has 6 decimals limiter.recordBridgeTransfers(supportedChainID, tokenID, amount); skip(1 hours); limiter.recordBridgeTransfers(supportedChainID, tokenID, 2 * amount); skip(1 hours); uint256 actual = limiter.calculateWindowAmount(supportedChainID); - assertEq(actual, 30000); + assertEq(actual, 3 * USD_VALUE_MULTIPLIER); skip(22 hours); actual = limiter.calculateWindowAmount(supportedChainID); - assertEq(actual, 20000); + assertEq(actual, 2 * USD_VALUE_MULTIPLIER); skip(59 minutes); actual = limiter.calculateWindowAmount(supportedChainID); - assertEq(actual, 20000); + assertEq(actual, 2 * USD_VALUE_MULTIPLIER); skip(1 minutes); actual = limiter.calculateWindowAmount(supportedChainID); assertEq(actual, 0); @@ -72,7 +68,7 @@ contract BridgeLimiterTest is BridgeBaseTest { function testRecordBridgeTransfer() public { changePrank(address(bridge)); uint8 tokenID = 1; - uint256 amount = 100000000; // wBTC has 8 decimals + uint256 amount = 1_00000000; // wBTC has 8 decimals limiter.recordBridgeTransfers(supportedChainID, tokenID, amount); tokenID = 2; amount = 1 ether; @@ -88,7 +84,7 @@ contract BridgeLimiterTest is BridgeBaseTest { function testrecordBridgeTransfersGarbageCollection() public { changePrank(address(bridge)); uint8 tokenID = 1; - uint256 amount = 100000000; // wBTC has 8 decimals + uint256 amount = 1_00000000; // wBTC has 8 decimals uint32 hourToDelete = uint32(block.timestamp / 1 hours); limiter.recordBridgeTransfers(supportedChainID, tokenID, amount); uint256 keyToDelete = limiter.getChainHourTimestampKey(supportedChainID, hourToDelete); @@ -100,49 +96,21 @@ contract BridgeLimiterTest is BridgeBaseTest { assertEq(deleteAmount, 0); } - function testUpdateTokenPriceWithSignatures() public { - changePrank(address(bridge)); - bytes memory payload = abi.encodePacked(uint8(1), uint64(100000000)); - // Create a sample BridgeMessage - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.UPDATE_TOKEN_PRICE, - version: 1, - nonce: 0, - chainID: chainID, - payload: payload - }); - - bytes memory messageBytes = BridgeMessage.encodeMessage(message); - bytes32 messageHash = keccak256(messageBytes); - - bytes[] memory signatures = new bytes[](4); - signatures[0] = getSignature(messageHash, committeeMemberPkA); - signatures[1] = getSignature(messageHash, committeeMemberPkB); - signatures[2] = getSignature(messageHash, committeeMemberPkC); - signatures[3] = getSignature(messageHash, committeeMemberPkD); - - // Call the updateTokenPriceWithSignatures function - limiter.updateTokenPriceWithSignatures(signatures, message); - - // Assert that the token price has been updated correctly - assertEq(limiter.tokenPrices(1), 100000000); - } - function testUpdateLimitWithSignatures() public { changePrank(address(bridge)); uint8 sourceChainID = 0; - uint64 newLimit = 1000000000; + uint64 newLimit = 10 * USD_VALUE_MULTIPLIER; bytes memory payload = abi.encodePacked(sourceChainID, newLimit); - // Create a sample BridgeMessage - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.UPDATE_BRIDGE_LIMIT, + // Create a sample BridgeUtils + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.UPDATE_BRIDGE_LIMIT, version: 1, nonce: 0, chainID: chainID, payload: payload }); - bytes memory messageBytes = BridgeMessage.encodeMessage(message); + bytes memory messageBytes = BridgeUtils.encodeMessage(message); bytes32 messageHash = keccak256(messageBytes); bytes[] memory signatures = new bytes[](4); @@ -156,21 +124,11 @@ contract BridgeLimiterTest is BridgeBaseTest { // Call the updateLimitWithSignatures function limiter.updateLimitWithSignatures(signatures, message); - assertEq(limiter.chainLimits(supportedChainID), 1000000000); + assertEq(limiter.chainLimits(supportedChainID), 10 * USD_VALUE_MULTIPLIER); } function testMultipleChainLimits() public { - // deploy new config contract with 2 supported chains - address[] memory _supportedTokens = new address[](4); - _supportedTokens[0] = wBTC; - _supportedTokens[1] = wETH; - _supportedTokens[2] = USDC; - _supportedTokens[3] = USDT; - uint8[] memory supportedChains = new uint8[](2); - supportedChains[0] = 11; - supportedChains[1] = 12; - config = new BridgeConfig(chainID, _supportedTokens, supportedChains); - // deploy new committee with new config contract + // deploy new committee address[] memory _committee = new address[](5); uint16[] memory _stake = new uint16[](5); _committee[0] = committeeMemberA; @@ -184,34 +142,53 @@ contract BridgeLimiterTest is BridgeBaseTest { _stake[3] = 2002; _stake[4] = 4998; committee = new BridgeCommittee(); - committee.initialize(address(config), _committee, _stake, minStakeRequired); + committee.initialize(_committee, _stake, minStakeRequired); + // deploy new config contract with 2 supported chains + address[] memory _supportedTokens = new address[](5); + _supportedTokens[0] = address(0); // SUI + _supportedTokens[1] = wBTC; + _supportedTokens[2] = wETH; + _supportedTokens[3] = USDC; + _supportedTokens[4] = USDT; + uint8[] memory supportedChains = new uint8[](2); + supportedChains[0] = 11; + supportedChains[1] = 12; + config = new BridgeConfig(); + config.initialize( + address(committee), chainID, _supportedTokens, tokenPrices, supportedChains + ); + committee.initializeConfig(address(config)); // deploy new limiter with 2 supported chains uint64[] memory totalLimits = new uint64[](2); - totalLimits[0] = 10000000000; - totalLimits[1] = 20000000000; - uint256[] memory tokenPrices = new uint256[](4); - tokenPrices[0] = SUI_PRICE; - tokenPrices[1] = BTC_PRICE; - tokenPrices[2] = ETH_PRICE; - tokenPrices[3] = USDC_PRICE; + totalLimits[0] = 1_000_000 * USD_VALUE_MULTIPLIER; + totalLimits[1] = 2_000_000 * USD_VALUE_MULTIPLIER; limiter = new BridgeLimiter(); - limiter.initialize(address(committee), tokenPrices, supportedChains, totalLimits); + limiter.initialize(address(committee), supportedChains, totalLimits); // check if the limits are set correctly - assertEq(limiter.chainLimits(11), 10000000000); - assertEq(limiter.chainLimits(12), 20000000000); + assertEq(limiter.chainLimits(11), 1_000_000 * USD_VALUE_MULTIPLIER); + assertEq(limiter.chainLimits(12), 2_000_000 * USD_VALUE_MULTIPLIER); // check if the oldestChainTimestamp is set correctly assertEq(limiter.oldestChainTimestamp(11), uint32(block.timestamp / 1 hours)); assertEq(limiter.oldestChainTimestamp(12), uint32(block.timestamp / 1 hours)); // check that limits are checked correctly uint8 tokenID = 3; - uint256 amount = 999999 * 1000000; // USDC has 6 decimals - assertFalse(limiter.willAmountExceedLimit(11, tokenID, amount)); + uint256 amount = 999_999 * 1000000; // USDC has 6 decimals + assertFalse( + limiter.willAmountExceedLimit(11, tokenID, amount), "limit should not be exceeded" + ); limiter.recordBridgeTransfers(11, tokenID, amount); - assertTrue(limiter.willAmountExceedLimit(11, tokenID, 2000000)); - assertFalse(limiter.willAmountExceedLimit(11, tokenID, 1000000)); - assertEq(limiter.calculateWindowAmount(11), 9999990000); - assertEq(limiter.calculateWindowAmount(12), 0); + + assertTrue(limiter.willAmountExceedLimit(11, tokenID, 2000000), "limit should be exceeded"); + assertFalse( + limiter.willAmountExceedLimit(11, tokenID, 1000000), "limit should not be exceeded" + ); + assertEq( + limiter.calculateWindowAmount(11), + 999999 * USD_VALUE_MULTIPLIER, + "window amount should be correct" + ); + assertEq(limiter.calculateWindowAmount(12), 0, "window amount should be correct"); // check that transfers are recorded correctly amount = 1100000 * 1000000; // USDC has 6 decimals limiter.recordBridgeTransfers(12, tokenID, amount); @@ -219,16 +196,31 @@ contract BridgeLimiterTest is BridgeBaseTest { limiter.chainHourlyTransferAmount( limiter.getChainHourTimestampKey(12, uint32(block.timestamp / 1 hours)) ), - 11000000000 + 1_100_000 * USD_VALUE_MULTIPLIER, + "transfer amount should be correct" + ); + assertEq( + limiter.calculateWindowAmount(11), + 999999 * USD_VALUE_MULTIPLIER, + "window amount should be correct" + ); + assertEq( + limiter.calculateWindowAmount(12), + 1100000 * USD_VALUE_MULTIPLIER, + "window amount should be correct" ); - assertEq(limiter.calculateWindowAmount(11), 9999990000); - assertEq(limiter.calculateWindowAmount(12), 11000000000); } // An e2e update limit regression test covering message ser/de function testUpdateLimitRegressionTest() public { address[] memory _committee = new address[](4); uint16[] memory _stake = new uint16[](4); + uint8 chainID = 11; + uint8[] memory _supportedChains = new uint8[](1); + uint8 sendingChainID = 1; + _supportedChains[0] = sendingChainID; + uint8[] memory _supportedDestinationChains = new uint8[](1); + _supportedDestinationChains[0] = sendingChainID; _committee[0] = 0x68B43fD906C0B8F024a18C56e06744F7c6157c65; _committee[1] = 0xaCAEf39832CB995c4E049437A3E2eC6a7bad1Ab5; _committee[2] = 0x8061f127910e8eF56F16a2C411220BaD25D61444; @@ -238,50 +230,53 @@ contract BridgeLimiterTest is BridgeBaseTest { _stake[2] = 2500; _stake[3] = 2500; committee = new BridgeCommittee(); - committee.initialize(address(config), _committee, _stake, minStakeRequired); - vault = new BridgeVault(wETH); - uint256[] memory tokenPrices = new uint256[](4); + committee.initialize(_committee, _stake, minStakeRequired); + + // deploy config + tokenPrices = new uint64[](5); tokenPrices[0] = 10000; // SUI PRICE tokenPrices[1] = 10000; // BTC PRICE tokenPrices[2] = 10000; // ETH PRICE tokenPrices[3] = 10000; // USDC PRICE + tokenPrices[4] = 10000; // USDT PRICE + config = new BridgeConfig(); + config.initialize( + address(committee), chainID, supportedTokens, tokenPrices, _supportedChains + ); + + // initialize config in the bridge committee + committee.initializeConfig(address(config)); + + vault = new BridgeVault(wETH); + + skip(2 days); uint64[] memory totalLimits = new uint64[](1); totalLimits[0] = 1000000; - uint8[] memory _supportedDestinationChains = new uint8[](1); - _supportedDestinationChains[0] = 0; - skip(2 days); + limiter = new BridgeLimiter(); - limiter.initialize( - address(committee), tokenPrices, _supportedDestinationChains, totalLimits - ); + limiter.initialize(address(committee), _supportedDestinationChains, totalLimits); bridge = new SuiBridge(); - bridge.initialize(address(committee), address(vault), address(limiter), wETH); + bridge.initialize(address(committee), address(vault), address(limiter)); vault.transferOwnership(address(bridge)); limiter.transferOwnership(address(bridge)); - // Fill vault with WETH - changePrank(deployer); - IWETH9(wETH).deposit{value: 10 ether}(); - IERC20(wETH).transfer(address(vault), 10 ether); - bytes memory payload = hex"0c00000002540be400"; // Create update bridge limit message - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.UPDATE_BRIDGE_LIMIT, + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.UPDATE_BRIDGE_LIMIT, version: 1, nonce: 15, - chainID: 3, + chainID: 2, payload: payload }); - bytes memory encodedMessage = BridgeMessage.encodeMessage(message); + bytes memory encodedMessage = BridgeUtils.encodeMessage(message); bytes memory expectedEncodedMessage = - hex"5355495f4252494447455f4d4553534147450301000000000000000f030c00000002540be400"; + hex"5355495f4252494447455f4d4553534147450301000000000000000f020c00000002540be400"; assertEq(encodedMessage, expectedEncodedMessage); } - // An e2e update limit regression test covering message ser/de and signature verification function testUpdateLimitRegressionTestWithSigVerficiation() public { address[] memory _committee = new address[](4); @@ -292,7 +287,6 @@ contract BridgeLimiterTest is BridgeBaseTest { _supportedChains[0] = sendingChainID; uint8[] memory _supportedDestinationChains = new uint8[](1); _supportedDestinationChains[0] = sendingChainID; - config = new BridgeConfig(chainID, supportedTokens, _supportedChains); _committee[0] = 0x68B43fD906C0B8F024a18C56e06744F7c6157c65; _committee[1] = 0xaCAEf39832CB995c4E049437A3E2eC6a7bad1Ab5; _committee[2] = 0x8061f127910e8eF56F16a2C411220BaD25D61444; @@ -302,25 +296,34 @@ contract BridgeLimiterTest is BridgeBaseTest { _stake[2] = 2500; _stake[3] = 2500; committee = new BridgeCommittee(); - committee.initialize(address(config), _committee, _stake, minStakeRequired); - vault = new BridgeVault(wETH); - uint256[] memory tokenPrices = new uint256[](4); + committee.initialize(_committee, _stake, minStakeRequired); + + // deploy config + tokenPrices = new uint64[](5); tokenPrices[0] = 10000; // SUI PRICE tokenPrices[1] = 10000; // BTC PRICE tokenPrices[2] = 10000; // ETH PRICE tokenPrices[3] = 10000; // USDC PRICE + tokenPrices[4] = 10000; // USDT PRICE + config = new BridgeConfig(); + config.initialize( + address(committee), chainID, supportedTokens, tokenPrices, _supportedChains + ); + + // initialize config in the bridge committee + committee.initializeConfig(address(config)); + + vault = new BridgeVault(wETH); skip(2 days); uint64[] memory totalLimits = new uint64[](1); - totalLimits[0] = 1000000; + totalLimits[0] = 100 * USD_VALUE_MULTIPLIER; limiter = new BridgeLimiter(); - limiter.initialize( - address(committee), tokenPrices, _supportedDestinationChains, totalLimits - ); + limiter.initialize(address(committee), _supportedDestinationChains, totalLimits); bridge = new SuiBridge(); - bridge.initialize(address(committee), address(vault), address(limiter), wETH); + bridge.initialize(address(committee), address(vault), address(limiter)); vault.transferOwnership(address(bridge)); limiter.transferOwnership(address(bridge)); @@ -329,18 +332,18 @@ contract BridgeLimiterTest is BridgeBaseTest { changePrank(deployer); IWETH9(wETH).deposit{value: 10 ether}(); IERC20(wETH).transfer(address(vault), 10 ether); - // sending chain: 01 (sendingChainID), new limit: 9_990_000_000_000 + // sending chain: 01 (sendingChainID), new limit: 99_900 * USD_VALUE_MULTIPLIER bytes memory payload = hex"0100000915fa66bc00"; // Create update bridge limit message - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.UPDATE_BRIDGE_LIMIT, + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.UPDATE_BRIDGE_LIMIT, version: 1, nonce: 0, chainID: chainID, payload: payload }); - bytes memory encodedMessage = BridgeMessage.encodeMessage(message); + bytes memory encodedMessage = BridgeUtils.encodeMessage(message); bytes memory expectedEncodedMessage = hex"5355495f4252494447455f4d455353414745030100000000000000000b0100000915fa66bc00"; @@ -358,140 +361,6 @@ contract BridgeLimiterTest is BridgeBaseTest { committee.verifySignatures(signatures, message); limiter.updateLimitWithSignatures(signatures, message); - assertEq(limiter.chainLimits(sendingChainID), 9_990_000_000_000); - } - - // An e2e update token price regression test covering message ser/de - function testUpdateTokenPriceRegressionTest() public { - address[] memory _committee = new address[](4); - uint16[] memory _stake = new uint16[](4); - _committee[0] = 0x68B43fD906C0B8F024a18C56e06744F7c6157c65; - _committee[1] = 0xaCAEf39832CB995c4E049437A3E2eC6a7bad1Ab5; - _committee[2] = 0x8061f127910e8eF56F16a2C411220BaD25D61444; - _committee[3] = 0x508F3F1ff45F4ca3D8e86CDCC91445F00aCC59fC; - _stake[0] = 2500; - _stake[1] = 2500; - _stake[2] = 2500; - _stake[3] = 2500; - committee = new BridgeCommittee(); - committee.initialize(address(config), _committee, _stake, minStakeRequired); - vault = new BridgeVault(wETH); - uint256[] memory tokenPrices = new uint256[](4); - tokenPrices[0] = 10000; // SUI PRICE - tokenPrices[1] = 10000; // BTC PRICE - tokenPrices[2] = 10000; // ETH PRICE - tokenPrices[3] = 10000; // USDC PRICE - uint64[] memory totalLimits = new uint64[](1); - totalLimits[0] = 1000000; - uint8[] memory _supportedDestinationChains = new uint8[](1); - _supportedDestinationChains[0] = 0; - skip(2 days); - limiter = new BridgeLimiter(); - limiter.initialize( - address(committee), tokenPrices, _supportedDestinationChains, totalLimits - ); - bridge = new SuiBridge(); - bridge.initialize(address(committee), address(vault), address(limiter), wETH); - vault.transferOwnership(address(bridge)); - limiter.transferOwnership(address(bridge)); - - // Fill vault with WETH - changePrank(deployer); - IWETH9(wETH).deposit{value: 10 ether}(); - IERC20(wETH).transfer(address(vault), 10 ether); - - bytes memory payload = hex"01000000003b9aca00"; - - // Create update token price message - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.UPDATE_TOKEN_PRICE, - version: 1, - nonce: 266, - chainID: 3, - payload: payload - }); - bytes memory encodedMessage = BridgeMessage.encodeMessage(message); - bytes memory expectedEncodedMessage = - hex"5355495f4252494447455f4d4553534147450401000000000000010a0301000000003b9aca00"; - - assertEq(encodedMessage, expectedEncodedMessage); - } - - // An e2e update asset price regression test covering message ser/de and signature verification - function testUpdateAssetPriceRegressionTestWithSigVerficiation() public { - address[] memory _committee = new address[](4); - uint16[] memory _stake = new uint16[](4); - uint8 chainID = 11; - uint8[] memory _supportedChains = new uint8[](1); - uint8 sendingChainID = 1; - _supportedChains[0] = sendingChainID; - uint8[] memory _supportedDestinationChains = new uint8[](1); - _supportedDestinationChains[0] = sendingChainID; - config = new BridgeConfig(chainID, supportedTokens, _supportedChains); - _committee[0] = 0x68B43fD906C0B8F024a18C56e06744F7c6157c65; - _committee[1] = 0xaCAEf39832CB995c4E049437A3E2eC6a7bad1Ab5; - _committee[2] = 0x8061f127910e8eF56F16a2C411220BaD25D61444; - _committee[3] = 0x508F3F1ff45F4ca3D8e86CDCC91445F00aCC59fC; - _stake[0] = 2500; - _stake[1] = 2500; - _stake[2] = 2500; - _stake[3] = 2500; - committee = new BridgeCommittee(); - committee.initialize(address(config), _committee, _stake, minStakeRequired); - vault = new BridgeVault(wETH); - uint256[] memory tokenPrices = new uint256[](4); - tokenPrices[0] = 10000; // SUI PRICE - tokenPrices[1] = 10000; // BTC PRICE - tokenPrices[2] = 10000; // ETH PRICE - tokenPrices[3] = 10000; // USDC PRICE - - skip(2 days); - - uint64[] memory totalLimits = new uint64[](1); - totalLimits[0] = 1000000; - - limiter = new BridgeLimiter(); - limiter.initialize( - address(committee), tokenPrices, _supportedDestinationChains, totalLimits - ); - bridge = new SuiBridge(); - bridge.initialize(address(committee), address(vault), address(limiter), wETH); - vault.transferOwnership(address(bridge)); - limiter.transferOwnership(address(bridge)); - - // Fill vault with WETH - changePrank(deployer); - IWETH9(wETH).deposit{value: 10 ether}(); - IERC20(wETH).transfer(address(vault), 10 ether); - // BTC -> 600_000_000 ($60k) - bytes memory payload = hex"010000000023c34600"; - - // Create update token price message - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.UPDATE_TOKEN_PRICE, - version: 1, - nonce: 0, - chainID: chainID, - payload: payload - }); - bytes memory encodedMessage = BridgeMessage.encodeMessage(message); - bytes memory expectedEncodedMessage = - hex"5355495f4252494447455f4d455353414745040100000000000000000b010000000023c34600"; - - assertEq(encodedMessage, expectedEncodedMessage); - - bytes[] memory signatures = new bytes[](3); - - signatures[0] = - hex"eb81068c2214c01bf5d89e6bd748c0d184ae68f74d365174657053af916dcd335960737eb724560a3481bb77b7df4169d8305a034143e1c749fd9f9bcda6cc1601"; - signatures[1] = - hex"116ad7d7bb705374328f85613020777d636fa092f98aa59a1d58f12f36d96f0e7aacfeb8ff356289da8d0d75278ccad8c19ec878db0b836f96ab544e91de1fed01"; - signatures[2] = - hex"b0229b50b0fe3fd4cdb05b31c7689d99e3181f9f11069cb457d73112985865ff504d9a9959c367d02b18b2d78312a012f194798499198410880351ab0a241a0c00"; - - committee.verifySignatures(signatures, message); - - limiter.updateTokenPriceWithSignatures(signatures, message); - assertEq(limiter.tokenPrices(BridgeMessage.BTC), 600_000_000); + assertEq(limiter.chainLimits(sendingChainID), 99_900 * USD_VALUE_MULTIPLIER); } } diff --git a/bridge/evm/test/BridgeMessageTest.t.sol b/bridge/evm/test/BridgeUtilsTest.t.sol similarity index 61% rename from bridge/evm/test/BridgeMessageTest.t.sol rename to bridge/evm/test/BridgeUtilsTest.t.sol index e5dc3df512630..f2bbad881d633 100644 --- a/bridge/evm/test/BridgeMessageTest.t.sol +++ b/bridge/evm/test/BridgeUtilsTest.t.sol @@ -4,7 +4,111 @@ pragma solidity ^0.8.20; import "./BridgeBaseTest.t.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; -contract BridgeMessageTest is BridgeBaseTest { +contract BridgeUtilsTest is BridgeBaseTest { + // This function is called before each unit test + function setUp() public { + setUpBridgeTest(); + } + + function testConvertERC20ToSuiDecimalAmountTooLargeForUint64() public { + vm.expectRevert(bytes("BridgeUtils: Amount too large for uint64")); + BridgeUtils.convertERC20ToSuiDecimal(18, 8, type(uint256).max); + } + + function testConvertERC20ToSuiDecimalInvalidSuiDecimal() public { + vm.expectRevert(bytes("BridgeUtils: Invalid Sui decimal")); + BridgeUtils.convertERC20ToSuiDecimal(10, 11, 100); + } + + function testconvertSuiToERC20DecimalInvalidSuiDecimal() public { + vm.expectRevert(bytes("BridgeUtils: Invalid Sui decimal")); + BridgeUtils.convertSuiToERC20Decimal(10, 11, 100); + } + + function testConvertERC20ToSuiDecimal() public { + // ETH + assertEq(IERC20Metadata(wETH).decimals(), 18); + uint256 ethAmount = 10 ether; + uint64 suiAmount = BridgeUtils.convertERC20ToSuiDecimal( + IERC20Metadata(config.tokenAddressOf(BridgeUtils.ETH)).decimals(), + config.tokenSuiDecimalOf(BridgeUtils.ETH), + ethAmount + ); + assertEq(suiAmount, 10_000_000_00); // 10 * 10 ^ 8 + + // USDC + assertEq(IERC20Metadata(USDC).decimals(), 6); + ethAmount = 50_000_000; // 50 USDC + suiAmount = BridgeUtils.convertERC20ToSuiDecimal( + IERC20Metadata(config.tokenAddressOf(BridgeUtils.USDC)).decimals(), + config.tokenSuiDecimalOf(BridgeUtils.USDC), + ethAmount + ); + assertEq(suiAmount, ethAmount); + + // USDT + assertEq(IERC20Metadata(USDT).decimals(), 6); + ethAmount = 60_000_000; // 60 USDT + suiAmount = BridgeUtils.convertERC20ToSuiDecimal( + IERC20Metadata(config.tokenAddressOf(BridgeUtils.USDT)).decimals(), + config.tokenSuiDecimalOf(BridgeUtils.USDT), + ethAmount + ); + assertEq(suiAmount, ethAmount); + + // BTC + assertEq(IERC20Metadata(wBTC).decimals(), 8); + ethAmount = 2_00_000_000; // 2 BTC + suiAmount = BridgeUtils.convertERC20ToSuiDecimal( + IERC20Metadata(config.tokenAddressOf(BridgeUtils.BTC)).decimals(), + config.tokenSuiDecimalOf(BridgeUtils.BTC), + ethAmount + ); + assertEq(suiAmount, ethAmount); + } + + function testconvertSuiToERC20Decimal() public { + // ETH + assertEq(IERC20Metadata(wETH).decimals(), 18); + uint64 suiAmount = 11_000_000_00; // 11 eth + uint256 ethAmount = BridgeUtils.convertSuiToERC20Decimal( + IERC20Metadata(config.tokenAddressOf(BridgeUtils.ETH)).decimals(), + config.tokenSuiDecimalOf(BridgeUtils.ETH), + suiAmount + ); + assertEq(ethAmount, 11 ether); + + // USDC + assertEq(IERC20Metadata(USDC).decimals(), 6); + suiAmount = 50_000_000; // 50 USDC + ethAmount = BridgeUtils.convertSuiToERC20Decimal( + IERC20Metadata(config.tokenAddressOf(BridgeUtils.USDC)).decimals(), + config.tokenSuiDecimalOf(BridgeUtils.USDC), + suiAmount + ); + assertEq(suiAmount, ethAmount); + + // USDT + assertEq(IERC20Metadata(USDT).decimals(), 6); + suiAmount = 50_000_000; // 50 USDT + ethAmount = BridgeUtils.convertSuiToERC20Decimal( + IERC20Metadata(config.tokenAddressOf(BridgeUtils.USDT)).decimals(), + config.tokenSuiDecimalOf(BridgeUtils.USDT), + suiAmount + ); + assertEq(suiAmount, ethAmount); + + // BTC + assertEq(IERC20Metadata(wBTC).decimals(), 8); + suiAmount = 3_000_000_00; // 3 BTC + ethAmount = BridgeUtils.convertSuiToERC20Decimal( + IERC20Metadata(config.tokenAddressOf(BridgeUtils.BTC)).decimals(), + config.tokenSuiDecimalOf(BridgeUtils.BTC), + suiAmount + ); + assertEq(suiAmount, ethAmount); + } + function testEncodeMessage() public { bytes memory moveEncodedMessage = abi.encodePacked( hex"5355495f4252494447455f4d45535341474500010000000000000000012080ab1ee086210a3a37355300ca24672e81062fcdb5ced6618dab203f6a3b291c0b14b18f79fe671db47393315ffdb377da4ea1b7af96010084d71700000000" @@ -17,9 +121,9 @@ contract BridgeMessageTest is BridgeBaseTest { hex"2080ab1ee086210a3a37355300ca24672e81062fcdb5ced6618dab203f6a3b291c0b14b18f79fe671db47393315ffdb377da4ea1b7af96010084d71700000000" ); - bytes memory abiEncodedMessage = BridgeMessage.encodeMessage( - BridgeMessage.Message({ - messageType: BridgeMessage.TOKEN_TRANSFER, + bytes memory abiEncodedMessage = BridgeUtils.encodeMessage( + BridgeUtils.Message({ + messageType: BridgeUtils.TOKEN_TRANSFER, version: 1, nonce: nonce, chainID: suiChainId, @@ -45,8 +149,8 @@ contract BridgeMessageTest is BridgeBaseTest { bytes memory payload = hex"2080ab1ee086210a3a37355300ca24672e81062fcdb5ced6618dab203f6a3b291c0b14b18f79fe671db47393315ffdb377da4ea1b7af9602000000c70432b1dd"; - BridgeMessage.TokenTransferPayload memory _payload = - BridgeMessage.decodeTokenTransferPayload(payload); + BridgeUtils.TokenTransferPayload memory _payload = + BridgeUtils.decodeTokenTransferPayload(payload); assertEq(_payload.senderAddressLength, uint8(32)); assertEq( @@ -56,15 +160,14 @@ contract BridgeMessageTest is BridgeBaseTest { assertEq(_payload.targetChain, uint8(11)); assertEq(_payload.recipientAddressLength, uint8(20)); assertEq(_payload.recipientAddress, 0xb18f79Fe671db47393315fFDB377Da4Ea1B7AF96); - assertEq(_payload.tokenID, BridgeMessage.ETH); + assertEq(_payload.tokenID, BridgeUtils.ETH); assertEq(_payload.amount, uint64(854768923101)); } function testDecodeBlocklistPayload() public { bytes memory payload = hex"010268b43fd906c0b8f024a18c56e06744f7c6157c65acaef39832cb995c4e049437a3e2ec6a7bad1ab5"; - (bool blocklisting, address[] memory members) = - BridgeMessage.decodeBlocklistPayload(payload); + (bool blocklisting, address[] memory members) = BridgeUtils.decodeBlocklistPayload(payload); assertEq(members.length, 2); assertEq(members[0], 0x68B43fD906C0B8F024a18C56e06744F7c6157c65); @@ -74,21 +177,22 @@ contract BridgeMessageTest is BridgeBaseTest { function testDecodeUpdateLimitPayload() public { bytes memory payload = hex"0c00000002540be400"; - (uint8 sourceChainID, uint64 newLimit) = BridgeMessage.decodeUpdateLimitPayload(payload); + (uint8 sourceChainID, uint64 newLimit) = BridgeUtils.decodeUpdateLimitPayload(payload); assertEq(sourceChainID, 12); - assertEq(newLimit, 1_000_000_0000); + assertEq(newLimit, 100 * USD_VALUE_MULTIPLIER); } - function testdecodeUpdateTokenPricePayload() public { + function testDecodeUpdateTokenPricePayload() public { bytes memory payload = hex"01000000003b9aca00"; - (uint8 tokenID, uint64 newPrice) = BridgeMessage.decodeUpdateTokenPricePayload(payload); - assertEq(tokenID, 1); - assertEq(newPrice, 100_000_0000); + (uint8 _tokenID, uint64 _newPrice) = BridgeUtils.decodeUpdateTokenPricePayload(payload); + + assertEq(_tokenID, 1); + assertEq(_newPrice, 10 * USD_VALUE_MULTIPLIER); } function testDecodeEmergencyOpPayload() public { bytes memory payload = hex"01"; - bool pausing = BridgeMessage.decodeEmergencyOpPayload(payload); + bool pausing = BridgeUtils.decodeEmergencyOpPayload(payload); assertFalse(pausing); } @@ -100,7 +204,7 @@ contract BridgeMessageTest is BridgeBaseTest { ); (address proxy, address newImp, bytes memory _calldata) = - BridgeMessage.decodeUpgradePayload(payload); + BridgeUtils.decodeUpgradePayload(payload); assertEq(proxy, address(100)); assertEq(newImp, address(200)); @@ -117,19 +221,19 @@ contract BridgeMessageTest is BridgeBaseTest { bytes4 initV2CallData = bytes4(keccak256(bytes("initializeV2()"))); // Create upgrade message - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.UPGRADE, + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.UPGRADE, version: 1, nonce: 123, chainID: 12, payload: initV2Payload }); - bytes memory encodedMessage = BridgeMessage.encodeMessage(message); + bytes memory encodedMessage = BridgeUtils.encodeMessage(message); assertEq(encodedMessage, initV2Message); (address proxy, address newImp, bytes memory _calldata) = - BridgeMessage.decodeUpgradePayload(initV2Payload); + BridgeUtils.decodeUpgradePayload(initV2Payload); assertEq(proxy, address(0x0606060606060606060606060606060606060606)); assertEq(newImp, address(0x0909090909090909090909090909090909090909)); @@ -146,19 +250,19 @@ contract BridgeMessageTest is BridgeBaseTest { bytes4 newMockFunc1CallData = bytes4(keccak256(bytes("newMockFunction(bool)"))); // Create upgrade message - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.UPGRADE, + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.UPGRADE, version: 1, nonce: 123, chainID: 12, payload: newMockFunc1Payload }); - bytes memory encodedMessage = BridgeMessage.encodeMessage(message); + bytes memory encodedMessage = BridgeUtils.encodeMessage(message); assertEq(encodedMessage, newMockFunc1Message); (address proxy, address newImp, bytes memory _calldata) = - BridgeMessage.decodeUpgradePayload(newMockFunc1Payload); + BridgeUtils.decodeUpgradePayload(newMockFunc1Payload); assertEq(proxy, address(0x0606060606060606060606060606060606060606)); assertEq(newImp, address(0x0909090909090909090909090909090909090909)); @@ -175,19 +279,19 @@ contract BridgeMessageTest is BridgeBaseTest { bytes4 newMockFunc2CallData = bytes4(keccak256(bytes("newMockFunction(bool,uint8)"))); // Create upgrade message - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.UPGRADE, + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.UPGRADE, version: 1, nonce: 123, chainID: 12, payload: newMockFunc2Payload }); - bytes memory encodedMessage = BridgeMessage.encodeMessage(message); + bytes memory encodedMessage = BridgeUtils.encodeMessage(message); assertEq(encodedMessage, newMockFunc2Message); (address proxy, address newImp, bytes memory _calldata) = - BridgeMessage.decodeUpgradePayload(newMockFunc2Payload); + BridgeUtils.decodeUpgradePayload(newMockFunc2Payload); assertEq(proxy, address(0x0606060606060606060606060606060606060606)); assertEq(newImp, address(0x0909090909090909090909090909090909090909)); @@ -202,19 +306,19 @@ contract BridgeMessageTest is BridgeBaseTest { hex"0000000000000000000000000606060606060606060606060606060606060606000000000000000000000000090909090909090909090909090909090909090900000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000"; // Create upgrade message - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.UPGRADE, + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.UPGRADE, version: 1, nonce: 123, chainID: 12, payload: emptyCalldataPayload }); - bytes memory encodedMessage = BridgeMessage.encodeMessage(message); + bytes memory encodedMessage = BridgeUtils.encodeMessage(message); assertEq(encodedMessage, emptyCalldataMessage); (address proxy, address newImp, bytes memory _calldata) = - BridgeMessage.decodeUpgradePayload(emptyCalldataPayload); + BridgeUtils.decodeUpgradePayload(emptyCalldataPayload); assertEq(proxy, address(0x0606060606060606060606060606060606060606)); assertEq(newImp, address(0x0909090909090909090909090909090909090909)); @@ -223,7 +327,7 @@ contract BridgeMessageTest is BridgeBaseTest { function testrequiredStakeInvalidType() public { uint8 invalidType = 100; - vm.expectRevert("BridgeMessage: Invalid message type"); - BridgeMessage.requiredStake(BridgeMessage.Message(invalidType, 0, 0, 1, bytes(hex"00"))); + vm.expectRevert("BridgeUtils: Invalid message type"); + BridgeUtils.requiredStake(BridgeUtils.Message(invalidType, 0, 0, 1, bytes(hex"00"))); } } diff --git a/bridge/evm/test/CommitteeUpgradeableTest.t.sol b/bridge/evm/test/CommitteeUpgradeableTest.t.sol index 46a2bc3b1128c..c6b68bdb992d5 100644 --- a/bridge/evm/test/CommitteeUpgradeableTest.t.sol +++ b/bridge/evm/test/CommitteeUpgradeableTest.t.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.20; import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol"; +import "openzeppelin-foundry-upgrades/Options.sol"; import {ERC1967Utils} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol"; import "./mocks/MockSuiBridgeV2.sol"; import "../contracts/BridgeCommittee.sol"; @@ -29,31 +30,40 @@ contract CommitteeUpgradeableTest is BridgeBaseTest { _stake[3] = 2002; _stake[4] = 4998; - address[] memory _supportedTokens = new address[](4); - _supportedTokens[0] = wBTC; - _supportedTokens[1] = wETH; - _supportedTokens[2] = USDC; - _supportedTokens[3] = USDT; uint8[] memory _supportedDestinationChains = new uint8[](1); _supportedDestinationChains[0] = 0; - BridgeConfig _config = - new BridgeConfig(_chainID, _supportedTokens, _supportedDestinationChains); + + Options memory opts; + opts.unsafeSkipAllChecks = true; // deploy bridge committee address _committee = Upgrades.deployUUPSProxy( "BridgeCommittee.sol", abi.encodeCall( - BridgeCommittee.initialize, - (address(_config), _committeeMembers, _stake, minStakeRequired) - ) + BridgeCommittee.initialize, (_committeeMembers, _stake, minStakeRequired) + ), + opts ); committee = BridgeCommittee(_committee); + // deploy bridge config + address _config = Upgrades.deployUUPSProxy( + "BridgeConfig.sol", + abi.encodeCall( + BridgeConfig.initialize, + (_committee, _chainID, supportedTokens, tokenPrices, _supportedDestinationChains) + ), + opts + ); + + committee.initializeConfig(_config); + // deploy sui bridge address _bridge = Upgrades.deployUUPSProxy( "SuiBridge.sol", - abi.encodeCall(SuiBridge.initialize, (_committee, address(0), address(0), address(0))) + abi.encodeCall(SuiBridge.initialize, (_committee, address(0), address(0))), + opts ); bridge = SuiBridge(_bridge); @@ -65,14 +75,14 @@ contract CommitteeUpgradeableTest is BridgeBaseTest { bytes memory payload = abi.encode(address(bridge), address(bridgeV2), initializer); // Create upgrade message - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.UPGRADE, + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.UPGRADE, version: 1, nonce: 0, chainID: _chainID, payload: payload }); - bytes memory encodedMessage = BridgeMessage.encodeMessage(message); + bytes memory encodedMessage = BridgeUtils.encodeMessage(message); bytes32 messageHash = keccak256(encodedMessage); bytes[] memory signatures = new bytes[](4); signatures[0] = getSignature(messageHash, committeeMemberPkA); @@ -91,14 +101,14 @@ contract CommitteeUpgradeableTest is BridgeBaseTest { bytes memory payload = abi.encode(address(bridge), address(bridgeV2), initializer); // Create upgrade message - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.UPGRADE, + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.UPGRADE, version: 1, nonce: 0, chainID: _chainID, payload: payload }); - bytes memory encodedMessage = BridgeMessage.encodeMessage(message); + bytes memory encodedMessage = BridgeUtils.encodeMessage(message); bytes32 messageHash = keccak256(encodedMessage); bytes[] memory signatures = new bytes[](2); signatures[0] = getSignature(messageHash, committeeMemberPkA); @@ -108,14 +118,14 @@ contract CommitteeUpgradeableTest is BridgeBaseTest { } function testUpgradeWithSignaturesMessageDoesNotMatchType() public { - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.TOKEN_TRANSFER, + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.TOKEN_TRANSFER, version: 1, nonce: 0, chainID: _chainID, payload: abi.encode(0) }); - bytes memory encodedMessage = BridgeMessage.encodeMessage(message); + bytes memory encodedMessage = BridgeUtils.encodeMessage(message); bytes32 messageHash = keccak256(encodedMessage); bytes[] memory signatures = new bytes[](4); signatures[0] = getSignature(messageHash, committeeMemberPkA); @@ -127,14 +137,14 @@ contract CommitteeUpgradeableTest is BridgeBaseTest { } function testUpgradeWithSignaturesInvalidNonce() public { - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.UPGRADE, + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.UPGRADE, version: 1, nonce: 10, chainID: _chainID, payload: abi.encode(0) }); - bytes memory encodedMessage = BridgeMessage.encodeMessage(message); + bytes memory encodedMessage = BridgeUtils.encodeMessage(message); bytes32 messageHash = keccak256(encodedMessage); bytes[] memory signatures = new bytes[](4); signatures[0] = getSignature(messageHash, committeeMemberPkA); @@ -149,14 +159,14 @@ contract CommitteeUpgradeableTest is BridgeBaseTest { bytes memory initializer = abi.encodeCall(MockSuiBridgeV2.initializeV2, ()); bytes memory payload = abi.encode(address(bridge), address(this), initializer); - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.UPGRADE, + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.UPGRADE, version: 1, nonce: 0, chainID: _chainID, payload: payload }); - bytes memory encodedMessage = BridgeMessage.encodeMessage(message); + bytes memory encodedMessage = BridgeUtils.encodeMessage(message); bytes32 messageHash = keccak256(encodedMessage); bytes[] memory signatures = new bytes[](4); signatures[0] = getSignature(messageHash, committeeMemberPkA); @@ -176,14 +186,14 @@ contract CommitteeUpgradeableTest is BridgeBaseTest { bytes memory initializer = abi.encodeCall(MockSuiBridgeV2.initializeV2, ()); bytes memory payload = abi.encode(address(this), address(bridgeV2), initializer); - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.UPGRADE, + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.UPGRADE, version: 1, nonce: 0, chainID: _chainID, payload: payload }); - bytes memory encodedMessage = BridgeMessage.encodeMessage(message); + bytes memory encodedMessage = BridgeUtils.encodeMessage(message); bytes32 messageHash = keccak256(encodedMessage); bytes[] memory signatures = new bytes[](4); signatures[0] = getSignature(messageHash, committeeMemberPkA); @@ -210,8 +220,8 @@ contract CommitteeUpgradeableTest is BridgeBaseTest { bytes32 messageHash = keccak256(encodedMessage); // Create upgrade message - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.UPGRADE, + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.UPGRADE, version: 1, nonce: 0, chainID: _chainID, @@ -245,8 +255,8 @@ contract CommitteeUpgradeableTest is BridgeBaseTest { bytes32 messageHash = keccak256(encodedMessage); // Create upgrade message - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.UPGRADE, + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.UPGRADE, version: 1, nonce: 0, chainID: _chainID, @@ -281,8 +291,8 @@ contract CommitteeUpgradeableTest is BridgeBaseTest { bytes32 messageHash = keccak256(encodedMessage); // Create upgrade message - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.UPGRADE, + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.UPGRADE, version: 1, nonce: 0, chainID: _chainID, @@ -318,8 +328,8 @@ contract CommitteeUpgradeableTest is BridgeBaseTest { bytes32 messageHash = keccak256(encodedMessage); // Create upgrade message - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.UPGRADE, + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.UPGRADE, version: 1, nonce: 0, chainID: _chainID, @@ -337,6 +347,4 @@ contract CommitteeUpgradeableTest is BridgeBaseTest { MockSuiBridgeV2(address(bridge)); assertEq(Upgrades.getImplementationAddress(address(bridge)), address(bridgeV2)); } - - // TODO: addMockUpgradeTest using OZ upgrades package to show upgrade safety checks } diff --git a/bridge/evm/test/SuiBridgeTest.t.sol b/bridge/evm/test/SuiBridgeTest.t.sol index b55350ece056e..bb09de4505403 100644 --- a/bridge/evm/test/SuiBridgeTest.t.sol +++ b/bridge/evm/test/SuiBridgeTest.t.sol @@ -15,7 +15,6 @@ contract SuiBridgeTest is BridgeBaseTest, ISuiBridge { function testSuiBridgeInitialization() public { assertEq(address(bridge.committee()), address(committee)); assertEq(address(bridge.vault()), address(vault)); - assertEq(address(bridge.wETH()), wETH); } function testTransferBridgedTokensWithSignaturesTokenDailyLimitExceeded() public { @@ -24,8 +23,8 @@ contract SuiBridgeTest is BridgeBaseTest, ISuiBridge { uint8 targetChain = chainID; uint8 recipientAddressLength = 20; address recipientAddress = bridgerA; - uint8 tokenID = BridgeMessage.ETH; - uint64 amount = 100000000000000; + uint8 tokenID = BridgeUtils.ETH; + uint64 amount = 1_000_000 * USD_VALUE_MULTIPLIER; bytes memory payload = abi.encodePacked( senderAddressLength, senderAddress, @@ -36,15 +35,15 @@ contract SuiBridgeTest is BridgeBaseTest, ISuiBridge { amount ); - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.TOKEN_TRANSFER, + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.TOKEN_TRANSFER, version: 1, nonce: 1, chainID: 0, payload: payload }); - bytes memory encodedMessage = BridgeMessage.encodeMessage(message); + bytes memory encodedMessage = BridgeUtils.encodeMessage(message); bytes32 messageHash = keccak256(encodedMessage); bytes[] memory signatures = new bytes[](4); @@ -62,7 +61,7 @@ contract SuiBridgeTest is BridgeBaseTest, ISuiBridge { uint8 targetChain = 0; uint8 recipientAddressLength = 20; address recipientAddress = bridgerA; - uint8 tokenID = BridgeMessage.ETH; + uint8 tokenID = BridgeUtils.ETH; uint64 amount = 10000; bytes memory payload = abi.encodePacked( senderAddressLength, @@ -74,15 +73,15 @@ contract SuiBridgeTest is BridgeBaseTest, ISuiBridge { amount ); - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.TOKEN_TRANSFER, + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.TOKEN_TRANSFER, version: 1, nonce: 1, chainID: 1, payload: payload }); - bytes memory encodedMessage = BridgeMessage.encodeMessage(message); + bytes memory encodedMessage = BridgeUtils.encodeMessage(message); bytes32 messageHash = keccak256(encodedMessage); bytes[] memory signatures = new bytes[](4); @@ -96,24 +95,24 @@ contract SuiBridgeTest is BridgeBaseTest, ISuiBridge { function testTransferBridgedTokensWithSignaturesInsufficientStakeAmount() public { // Create transfer message - BridgeMessage.TokenTransferPayload memory payload = BridgeMessage.TokenTransferPayload({ + BridgeUtils.TokenTransferPayload memory payload = BridgeUtils.TokenTransferPayload({ senderAddressLength: 0, senderAddress: abi.encode(0), targetChain: 1, recipientAddressLength: 0, recipientAddress: bridgerA, - tokenID: BridgeMessage.ETH, + tokenID: BridgeUtils.ETH, // This is Sui amount (eth decimal 8) amount: 100_000_000 }); - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.TOKEN_TRANSFER, + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.TOKEN_TRANSFER, version: 1, nonce: 1, chainID: chainID, payload: abi.encode(payload) }); - bytes memory encodedMessage = BridgeMessage.encodeMessage(message); + bytes memory encodedMessage = BridgeUtils.encodeMessage(message); bytes32 messageHash = keccak256(encodedMessage); bytes[] memory signatures = new bytes[](2); signatures[0] = getSignature(messageHash, committeeMemberPkA); @@ -124,24 +123,24 @@ contract SuiBridgeTest is BridgeBaseTest, ISuiBridge { function testTransferBridgedTokensWithSignaturesMessageDoesNotMatchType() public { // Create transfer message - BridgeMessage.TokenTransferPayload memory payload = BridgeMessage.TokenTransferPayload({ + BridgeUtils.TokenTransferPayload memory payload = BridgeUtils.TokenTransferPayload({ senderAddressLength: 0, senderAddress: abi.encode(0), targetChain: 1, recipientAddressLength: 0, recipientAddress: bridgerA, - tokenID: BridgeMessage.ETH, + tokenID: BridgeUtils.ETH, // This is Sui amount (eth decimal 8) amount: 100_000_000 }); - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.EMERGENCY_OP, + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.EMERGENCY_OP, version: 1, nonce: 1, chainID: chainID, payload: abi.encode(payload) }); - bytes memory encodedMessage = BridgeMessage.encodeMessage(message); + bytes memory encodedMessage = BridgeUtils.encodeMessage(message); bytes32 messageHash = keccak256(encodedMessage); bytes[] memory signatures = new bytes[](2); signatures[0] = getSignature(messageHash, committeeMemberPkA); @@ -162,7 +161,7 @@ contract SuiBridgeTest is BridgeBaseTest, ISuiBridge { uint8 targetChain = chainID; uint8 recipientAddressLength = 20; address recipientAddress = bridgerA; - uint8 tokenID = BridgeMessage.ETH; + uint8 tokenID = BridgeUtils.ETH; uint64 amount = 100000000; // 1 ether in sui decimals bytes memory payload = abi.encodePacked( senderAddressLength, @@ -175,15 +174,15 @@ contract SuiBridgeTest is BridgeBaseTest, ISuiBridge { ); // Create transfer message - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.TOKEN_TRANSFER, + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.TOKEN_TRANSFER, version: 1, nonce: 1, chainID: 0, payload: payload }); - bytes memory encodedMessage = BridgeMessage.encodeMessage(message); + bytes memory encodedMessage = BridgeUtils.encodeMessage(message); bytes32 messageHash = keccak256(encodedMessage); @@ -214,7 +213,7 @@ contract SuiBridgeTest is BridgeBaseTest, ISuiBridge { uint8 targetChain = chainID; uint8 recipientAddressLength = 20; address recipientAddress = bridgerA; - uint8 tokenID = BridgeMessage.USDC; + uint8 tokenID = BridgeUtils.USDC; uint64 amount = 1_000_000; bytes memory payload = abi.encodePacked( senderAddressLength, @@ -227,15 +226,15 @@ contract SuiBridgeTest is BridgeBaseTest, ISuiBridge { ); // Create transfer message - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.TOKEN_TRANSFER, + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.TOKEN_TRANSFER, version: 1, nonce: 1, chainID: 0, payload: payload }); - bytes memory encodedMessage = BridgeMessage.encodeMessage(message); + bytes memory encodedMessage = BridgeUtils.encodeMessage(message); bytes32 messageHash = keccak256(encodedMessage); bytes[] memory signatures = new bytes[](4); @@ -251,33 +250,33 @@ contract SuiBridgeTest is BridgeBaseTest, ISuiBridge { } function testExecuteEmergencyOpWithSignaturesInvalidOpCode() public { - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.EMERGENCY_OP, + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.EMERGENCY_OP, version: 1, nonce: 0, chainID: chainID, payload: hex"02" }); - bytes memory encodedMessage = BridgeMessage.encodeMessage(message); + bytes memory encodedMessage = BridgeUtils.encodeMessage(message); bytes32 messageHash = keccak256(encodedMessage); bytes[] memory signatures = new bytes[](4); signatures[0] = getSignature(messageHash, committeeMemberPkA); signatures[1] = getSignature(messageHash, committeeMemberPkB); signatures[2] = getSignature(messageHash, committeeMemberPkC); signatures[3] = getSignature(messageHash, committeeMemberPkD); - vm.expectRevert(bytes("BridgeMessage: Invalid op code")); + vm.expectRevert(bytes("BridgeUtils: Invalid op code")); bridge.executeEmergencyOpWithSignatures(signatures, message); } function testExecuteEmergencyOpWithSignaturesInvalidNonce() public { - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.EMERGENCY_OP, + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.EMERGENCY_OP, version: 1, nonce: 1, chainID: chainID, payload: bytes(hex"00") }); - bytes memory encodedMessage = BridgeMessage.encodeMessage(message); + bytes memory encodedMessage = BridgeUtils.encodeMessage(message); bytes32 messageHash = keccak256(encodedMessage); bytes[] memory signatures = new bytes[](4); signatures[0] = getSignature(messageHash, committeeMemberPkA); @@ -289,14 +288,14 @@ contract SuiBridgeTest is BridgeBaseTest, ISuiBridge { } function testExecuteEmergencyOpWithSignaturesMessageDoesNotMatchType() public { - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.TOKEN_TRANSFER, + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.TOKEN_TRANSFER, version: 1, nonce: 0, chainID: chainID, payload: abi.encode(0) }); - bytes memory encodedMessage = BridgeMessage.encodeMessage(message); + bytes memory encodedMessage = BridgeUtils.encodeMessage(message); bytes32 messageHash = keccak256(encodedMessage); bytes[] memory signatures = new bytes[](4); signatures[0] = getSignature(messageHash, committeeMemberPkA); @@ -308,14 +307,14 @@ contract SuiBridgeTest is BridgeBaseTest, ISuiBridge { } function testExecuteEmergencyOpWithSignaturesInvalidSignatures() public { - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.EMERGENCY_OP, + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.EMERGENCY_OP, version: 1, nonce: 0, chainID: chainID, payload: bytes(hex"01") }); - bytes memory encodedMessage = BridgeMessage.encodeMessage(message); + bytes memory encodedMessage = BridgeUtils.encodeMessage(message); bytes32 messageHash = keccak256(encodedMessage); bytes[] memory signatures = new bytes[](2); signatures[0] = getSignature(messageHash, committeeMemberPkA); @@ -326,15 +325,15 @@ contract SuiBridgeTest is BridgeBaseTest, ISuiBridge { function testFreezeBridgeEmergencyOp() public { // Create emergency op message - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.EMERGENCY_OP, + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.EMERGENCY_OP, version: 1, nonce: 0, chainID: chainID, payload: bytes(hex"00") }); - bytes memory encodedMessage = BridgeMessage.encodeMessage(message); + bytes memory encodedMessage = BridgeUtils.encodeMessage(message); bytes32 messageHash = keccak256(encodedMessage); bytes[] memory signatures = new bytes[](4); @@ -352,15 +351,15 @@ contract SuiBridgeTest is BridgeBaseTest, ISuiBridge { function testUnfreezeBridgeEmergencyOp() public { testFreezeBridgeEmergencyOp(); // Create emergency op message - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.EMERGENCY_OP, + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.EMERGENCY_OP, version: 1, nonce: 1, chainID: chainID, payload: bytes(hex"01") }); - bytes memory encodedMessage = BridgeMessage.encodeMessage(message); + bytes memory encodedMessage = BridgeUtils.encodeMessage(message); bytes32 messageHash = keccak256(encodedMessage); bytes[] memory signatures = new bytes[](4); @@ -381,7 +380,7 @@ contract SuiBridgeTest is BridgeBaseTest, ISuiBridge { function testBridgeERC20InsufficientAllowance() public { vm.expectRevert(bytes("SuiBridge: Insufficient allowance")); - bridge.bridgeERC20(BridgeMessage.ETH, type(uint256).max, abi.encode("suiAddress"), 0); + bridge.bridgeERC20(BridgeUtils.ETH, type(uint256).max, abi.encode("suiAddress"), 0); } function testBridgeWETH() public { @@ -397,16 +396,16 @@ contract SuiBridgeTest is BridgeBaseTest, ISuiBridge { chainID, 0, // nonce 0, // destination chain id - BridgeMessage.ETH, + BridgeUtils.ETH, 1_00_000_000, // 1 ether deployer, abi.encode("suiAddress") ); - bridge.bridgeERC20(BridgeMessage.ETH, 1 ether, abi.encode("suiAddress"), 0); + bridge.bridgeERC20(BridgeUtils.ETH, 1 ether, abi.encode("suiAddress"), 0); assertEq(IERC20(wETH).balanceOf(address(vault)), 1 ether); assertEq(IERC20(wETH).balanceOf(deployer), balance - 1 ether); - assertEq(bridge.nonces(BridgeMessage.TOKEN_TRANSFER), 1); + assertEq(bridge.nonces(BridgeUtils.TOKEN_TRANSFER), 1); // Now test rounding. For ETH, the last 10 digits are rounded vm.expectEmit(true, true, true, false); @@ -414,30 +413,103 @@ contract SuiBridgeTest is BridgeBaseTest, ISuiBridge { chainID, 1, // nonce 0, // destination chain id - BridgeMessage.ETH, + BridgeUtils.ETH, 2.00000001 ether, deployer, abi.encode("suiAddress") ); // 2_000_000_011_000_000_888 is rounded to 2.00000001 eth - bridge.bridgeERC20( - BridgeMessage.ETH, 2_000_000_011_000_000_888, abi.encode("suiAddress"), 0 - ); + bridge.bridgeERC20(BridgeUtils.ETH, 2_000_000_011_000_000_888, abi.encode("suiAddress"), 0); assertEq(IERC20(wETH).balanceOf(address(vault)), 3_000_000_011_000_000_888); assertEq(IERC20(wETH).balanceOf(deployer), balance - 3_000_000_011_000_000_888); - assertEq(bridge.nonces(BridgeMessage.TOKEN_TRANSFER), 2); + assertEq(bridge.nonces(BridgeUtils.TOKEN_TRANSFER), 2); } function testBridgeUSDC() public { - // TODO test and make sure adjusted amount in event is correct + changePrank(USDCWhale); + + uint256 usdcAmount = 1000000; + + // approve + IERC20(USDC).approve(address(bridge), usdcAmount); + + assertEq(IERC20(USDC).balanceOf(address(vault)), 0); + uint256 balance = IERC20(USDC).balanceOf(USDCWhale); + + // assert emitted event + vm.expectEmit(true, true, true, false); + emit TokensDeposited( + chainID, + 0, // nonce + 0, // destination chain id + BridgeUtils.USDC, + 1_000_000, // 1 ether + USDCWhale, + abi.encode("suiAddress") + ); + bridge.bridgeERC20(BridgeUtils.USDC, usdcAmount, abi.encode("suiAddress"), 0); + + assertEq(IERC20(USDC).balanceOf(USDCWhale), balance - usdcAmount); + assertEq(IERC20(USDC).balanceOf(address(vault)), usdcAmount); } function testBridgeUSDT() public { - // TODO test and make sure adjusted amount in event is correct + changePrank(USDTWhale); + + uint256 usdtAmount = 1000000; + + // approve + bytes4 selector = bytes4(keccak256("approve(address,uint256)")); + bytes memory data = abi.encodeWithSelector(selector, address(bridge), usdtAmount); + (bool success, bytes memory returnData) = USDT.call(data); + require(success, "Call failed"); + + assertEq(IERC20(USDT).balanceOf(address(vault)), 0); + uint256 balance = IERC20(USDT).balanceOf(USDTWhale); + + // assert emitted event + vm.expectEmit(true, true, true, false); + emit TokensDeposited( + chainID, + 0, // nonce + 0, // destination chain id + BridgeUtils.USDT, + 1_000_000, // 1 ether + USDTWhale, + abi.encode("suiAddress") + ); + bridge.bridgeERC20(BridgeUtils.USDT, usdtAmount, abi.encode("suiAddress"), 0); + + assertEq(IERC20(USDT).balanceOf(USDTWhale), balance - usdtAmount); + assertEq(IERC20(USDT).balanceOf(address(vault)), usdtAmount); } function testBridgeBTC() public { - // TODO test and make sure adjusted amount in event is correct + changePrank(wBTCWhale); + + uint256 wbtcAmount = 1000000; + + // approve + IERC20(wBTC).approve(address(bridge), wbtcAmount); + + assertEq(IERC20(wBTC).balanceOf(address(vault)), 0); + uint256 balance = IERC20(wBTC).balanceOf(wBTCWhale); + + // assert emitted event + vm.expectEmit(true, true, true, false); + emit TokensDeposited( + chainID, + 0, // nonce + 0, // destination chain id + BridgeUtils.BTC, + 1_000_000, // 1 ether + wBTCWhale, + abi.encode("suiAddress") + ); + bridge.bridgeERC20(BridgeUtils.BTC, wbtcAmount, abi.encode("suiAddress"), 0); + + assertEq(IERC20(wBTC).balanceOf(wBTCWhale), balance - wbtcAmount); + assertEq(IERC20(wBTC).balanceOf(address(vault)), wbtcAmount); } function testBridgeEth() public { @@ -451,7 +523,7 @@ contract SuiBridgeTest is BridgeBaseTest, ISuiBridge { chainID, 0, // nonce 0, // destination chain id - BridgeMessage.ETH, + BridgeUtils.ETH, 1_000_000_00, // 1 ether deployer, abi.encode("suiAddress") @@ -460,278 +532,9 @@ contract SuiBridgeTest is BridgeBaseTest, ISuiBridge { bridge.bridgeETH{value: 1 ether}(abi.encode("suiAddress"), 0); assertEq(IERC20(wETH).balanceOf(address(vault)), 1 ether); assertEq(deployer.balance, balance - 1 ether); - assertEq(bridge.nonces(BridgeMessage.TOKEN_TRANSFER), 1); + assertEq(bridge.nonces(BridgeUtils.TOKEN_TRANSFER), 1); } - // TESTS FOR GAS MEASUREMENT - - // function testTransferBridgedTokensWith7Signatures() public { - // // define committee with 50 members - // address[] memory _committee = new address[](56); - // uint256[] memory pks = new uint256[](56); - // uint16[] memory _stake = new uint16[](56); - // for (uint256 i = 0; i < 56; i++) { - // string memory name = string(abi.encodePacked("committeeMember", i)); - // (address member, uint256 pk) = makeAddrAndKey(name); - // _committee[i] = member; - // pks[i] = pk; - // // 1 member with 2500 stake - // if (i == 55) { - // _stake[i] = 2500; - // // 50 members with 100 stake (total: 5000) - // } else if (i < 50) { - // _stake[i] = 100; - // // 5 members with 500 stake (total: 2500) - // } else { - // _stake[i] = 500; - // } - // } - // committee = new BridgeCommittee(); - // committee.initialize(address(config), _committee, _stake, minStakeRequired); - // uint256[] memory tokenPrices = new uint256[](4); - // tokenPrices[0] = 10000; // SUI PRICE - // tokenPrices[1] = 10000; // BTC PRICE - // tokenPrices[2] = 10000; // ETH PRICE - // tokenPrices[3] = 10000; // USDC PRICE - // uint64[] memory totalLimits = new uint64[](1); - // totalLimits[0] = 1000000; - // skip(2 days); - // SuiBridge _bridge = new SuiBridge(); - // _bridge.initialize(address(committee), address(vault), address(limiter), wETH); - // changePrank(address(bridge)); - // limiter.transferOwnership(address(_bridge)); - // vault.transferOwnership(address(_bridge)); - // bridge = _bridge; - - // // Fill vault with WETH - // changePrank(deployer); - // IWETH9(wETH).deposit{value: 10 ether}(); - // IERC20(wETH).transfer(address(vault), 10 ether); - - // // transfer bridged tokens with 7 signatures - // // Create transfer payload - // uint8 senderAddressLength = 32; - // bytes memory senderAddress = abi.encode(0); - // uint8 targetChain = chainID; - // uint8 recipientAddressLength = 20; - // address recipientAddress = bridgerA; - // uint8 tokenID = BridgeMessage.ETH; - // uint64 amount = 100000000; // 1 ether in sui decimals - // bytes memory payload = abi.encodePacked( - // senderAddressLength, - // senderAddress, - // targetChain, - // recipientAddressLength, - // recipientAddress, - // tokenID, - // amount - // ); - - // // Create transfer message - // BridgeMessage.Message memory message = BridgeMessage.Message({ - // messageType: BridgeMessage.TOKEN_TRANSFER, - // version: 1, - // nonce: 1, - // chainID: 0, - // payload: payload - // }); - - // bytes memory encodedMessage = BridgeMessage.encodeMessage(message); - - // bytes32 messageHash = keccak256(encodedMessage); - - // bytes[] memory signatures = new bytes[](7); - - // uint8 index; - // for (uint256 i = 50; i < 55; i++) { - // signatures[index++] = getSignature(messageHash, pks[i]); - // } - // signatures[5] = getSignature(messageHash, pks[55]); - // signatures[6] = getSignature(messageHash, pks[0]); - - // bridge.transferBridgedTokensWithSignatures(signatures, message); - // } - - // function testTransferBridgedTokensWith26Signatures() public { - // // define committee with 50 members - // address[] memory _committee = new address[](56); - // uint256[] memory pks = new uint256[](56); - // uint16[] memory _stake = new uint16[](56); - // for (uint256 i = 0; i < 56; i++) { - // string memory name = string(abi.encodePacked("committeeMember", i)); - // (address member, uint256 pk) = makeAddrAndKey(name); - // _committee[i] = member; - // pks[i] = pk; - // // 1 member with 2500 stake - // if (i == 55) { - // _stake[i] = 2500; - // // 50 members with 100 stake (total: 5000) - // } else if (i < 50) { - // _stake[i] = 100; - // // 5 members with 500 stake (total: 2500) - // } else { - // _stake[i] = 500; - // } - // } - // committee = new BridgeCommittee(); - // committee.initialize(address(config), _committee, _stake, minStakeRequired); - // uint256[] memory tokenPrices = new uint256[](4); - // tokenPrices[0] = 10000; // SUI PRICE - // tokenPrices[1] = 10000; // BTC PRICE - // tokenPrices[2] = 10000; // ETH PRICE - // tokenPrices[3] = 10000; // USDC PRICE - // uint64[] memory totalLimits = new uint64[](1); - // totalLimits[0] = 1000000; - // skip(2 days); - // SuiBridge _bridge = new SuiBridge(); - // _bridge.initialize(address(committee), address(vault), address(limiter), wETH); - // changePrank(address(bridge)); - // limiter.transferOwnership(address(_bridge)); - // vault.transferOwnership(address(_bridge)); - // bridge = _bridge; - - // // Fill vault with WETH - // changePrank(deployer); - // IWETH9(wETH).deposit{value: 10 ether}(); - // IERC20(wETH).transfer(address(vault), 10 ether); - - // // transfer bridged tokens with 26 signatures - - // // Create transfer payload - // uint8 senderAddressLength = 32; - // bytes memory senderAddress = abi.encode(0); - // uint8 targetChain = chainID; - // uint8 recipientAddressLength = 20; - // address recipientAddress = bridgerA; - // uint8 tokenID = BridgeMessage.ETH; - // uint64 amount = 100000000; // 1 ether in sui decimals - // bytes memory payload = abi.encodePacked( - // senderAddressLength, - // senderAddress, - // targetChain, - // recipientAddressLength, - // recipientAddress, - // tokenID, - // amount - // ); - - // // Create transfer message - // BridgeMessage.Message memory message = BridgeMessage.Message({ - // messageType: BridgeMessage.TOKEN_TRANSFER, - // version: 1, - // nonce: 2, - // chainID: 0, - // payload: payload - // }); - - // bytes memory encodedMessage = BridgeMessage.encodeMessage(message); - - // bytes32 messageHash = keccak256(encodedMessage); - - // bytes[] memory signatures = new bytes[](25); - - // uint256 index = 0; - // // add 5 committee members with 100 stake - // for (uint256 i = 50; i < 55; i++) { - // signatures[index++] = getSignature(messageHash, pks[i]); - // } - // // add last committee member with 2500 stake - // signatures[5] = getSignature(messageHash, pks[55]); - - // // add 20 committee members with 100 stake - // for (uint256 i = 0; i < 20; i++) { - // signatures[index++] = getSignature(messageHash, pks[i]); - // } - - // bridge.transferBridgedTokensWithSignatures(signatures, message); - // } - - // function testTransferBridgedTokensWith56Signatures() public { - // // define committee with 50 members - // address[] memory _committee = new address[](56); - // uint256[] memory pks = new uint256[](56); - // uint16[] memory _stake = new uint16[](56); - // for (uint256 i = 0; i < 56; i++) { - // string memory name = string(abi.encodePacked("committeeMember", i)); - // (address member, uint256 pk) = makeAddrAndKey(name); - // _committee[i] = member; - // pks[i] = pk; - // // 1 member with 2500 stake - // if (i == 55) { - // _stake[i] = 2500; - // // 50 members with 100 stake (total: 5000) - // } else if (i < 50) { - // _stake[i] = 100; - // // 5 members with 500 stake (total: 2500) - // } else { - // _stake[i] = 500; - // } - // } - // committee = new BridgeCommittee(); - // committee.initialize(address(config), _committee, _stake, minStakeRequired); - // uint256[] memory tokenPrices = new uint256[](4); - // tokenPrices[0] = 10000; // SUI PRICE - // tokenPrices[1] = 10000; // BTC PRICE - // tokenPrices[2] = 10000; // ETH PRICE - // tokenPrices[3] = 10000; // USDC PRICE - // uint64[] memory totalLimits = new uint64[](1); - // totalLimits[0] = 1000000; - // skip(2 days); - // SuiBridge _bridge = new SuiBridge(); - // _bridge.initialize(address(committee), address(vault), address(limiter), wETH); - // changePrank(address(bridge)); - // limiter.transferOwnership(address(_bridge)); - // vault.transferOwnership(address(_bridge)); - // bridge = _bridge; - - // // Fill vault with WETH - // changePrank(deployer); - // IWETH9(wETH).deposit{value: 10 ether}(); - // IERC20(wETH).transfer(address(vault), 10 ether); - - // // transfer bridged tokens with 56 signatures - - // // Create transfer payload - // uint8 senderAddressLength = 32; - // bytes memory senderAddress = abi.encode(0); - // uint8 targetChain = chainID; - // uint8 recipientAddressLength = 20; - // address recipientAddress = bridgerA; - // uint8 tokenID = BridgeMessage.ETH; - // uint64 amount = 100000000; // 1 ether in sui decimals - // bytes memory payload = abi.encodePacked( - // senderAddressLength, - // senderAddress, - // targetChain, - // recipientAddressLength, - // recipientAddress, - // tokenID, - // amount - // ); - - // // Create transfer message - // BridgeMessage.Message memory message = BridgeMessage.Message({ - // messageType: BridgeMessage.TOKEN_TRANSFER, - // version: 1, - // nonce: 3, - // chainID: 0, - // payload: payload - // }); - - // bytes memory encodedMessage = BridgeMessage.encodeMessage(message); - - // bytes32 messageHash = keccak256(encodedMessage); - - // bytes[] memory signatures = new bytes[](56); - - // // get all signatures - // for (uint256 i = 0; i < 56; i++) { - // signatures[i] = getSignature(messageHash, pks[i]); - // } - - // bridge.transferBridgedTokensWithSignatures(signatures, message); - // } - // An e2e token transfer regression test covering message ser/de and signature verification function testTransferSuiToEthRegressionTest() public { address[] memory _committee = new address[](4); @@ -746,34 +549,40 @@ contract SuiBridgeTest is BridgeBaseTest, ISuiBridge { _stake[3] = 2500; committee = new BridgeCommittee(); + committee.initialize(_committee, _stake, minStakeRequired); + vault = new BridgeVault(wETH); + tokenPrices = new uint64[](5); + tokenPrices[0] = 1 * USD_VALUE_MULTIPLIER; // SUI PRICE + tokenPrices[1] = 1 * USD_VALUE_MULTIPLIER; // BTC PRICE + tokenPrices[2] = 1 * USD_VALUE_MULTIPLIER; // ETH PRICE + tokenPrices[3] = 1 * USD_VALUE_MULTIPLIER; // USDC PRICE + tokenPrices[4] = 1 * USD_VALUE_MULTIPLIER; // USDT PRICE + // deploy bridge config with 11 chainID - address[] memory _supportedTokens = new address[](4); - _supportedTokens[0] = wBTC; - _supportedTokens[1] = wETH; - _supportedTokens[2] = USDC; - _supportedTokens[3] = USDT; + address[] memory _supportedTokens = new address[](5); + _supportedTokens[0] = address(0); + _supportedTokens[1] = wBTC; + _supportedTokens[2] = wETH; + _supportedTokens[3] = USDC; + _supportedTokens[4] = USDT; uint8 supportedChainID = 1; uint8[] memory _supportedDestinationChains = new uint8[](1); _supportedDestinationChains[0] = 1; - BridgeConfig _config = new BridgeConfig(11, _supportedTokens, _supportedDestinationChains); + BridgeConfig _config = new BridgeConfig(); + _config.initialize( + address(committee), 11, _supportedTokens, tokenPrices, _supportedDestinationChains + ); - committee.initialize(address(_config), _committee, _stake, minStakeRequired); - vault = new BridgeVault(wETH); - uint256[] memory tokenPrices = new uint256[](4); - tokenPrices[0] = 10000; // SUI PRICE - tokenPrices[1] = 10000; // BTC PRICE - tokenPrices[2] = 10000; // ETH PRICE - tokenPrices[3] = 10000; // USDC PRICE - uint64[] memory totalLimits = new uint64[](1); - totalLimits[0] = 1000000; + committee.initializeConfig(address(_config)); skip(2 days); + + uint64[] memory totalLimits = new uint64[](1); + totalLimits[0] = 100 * USD_VALUE_MULTIPLIER; limiter = new BridgeLimiter(); - limiter.initialize( - address(committee), tokenPrices, _supportedDestinationChains, totalLimits - ); + limiter.initialize(address(committee), _supportedDestinationChains, totalLimits); bridge = new SuiBridge(); - bridge.initialize(address(committee), address(vault), address(limiter), wETH); + bridge.initialize(address(committee), address(vault), address(limiter)); vault.transferOwnership(address(bridge)); limiter.transferOwnership(address(bridge)); @@ -786,14 +595,14 @@ contract SuiBridgeTest is BridgeBaseTest, ISuiBridge { bytes memory payload = hex"2080ab1ee086210a3a37355300ca24672e81062fcdb5ced6618dab203f6a3b291c0b14b18f79fe671db47393315ffdb377da4ea1b7af960200000000000186a0"; // Create transfer message - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.TOKEN_TRANSFER, + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.TOKEN_TRANSFER, version: 1, nonce: 1, chainID: supportedChainID, payload: payload }); - bytes memory encodedMessage = BridgeMessage.encodeMessage(message); + bytes memory encodedMessage = BridgeUtils.encodeMessage(message); bytes memory expectedEncodedMessage = hex"5355495f4252494447455f4d45535341474500010000000000000001012080ab1ee086210a3a37355300ca24672e81062fcdb5ced6618dab203f6a3b291c0b14b18f79fe671db47393315ffdb377da4ea1b7af960200000000000186a0"; @@ -825,45 +634,51 @@ contract SuiBridgeTest is BridgeBaseTest, ISuiBridge { _stake[1] = 2500; _stake[2] = 2500; _stake[3] = 2500; - uint8 _chainID = 3; - uint8[] memory _supportedDestinationChains = new uint8[](1); - _supportedDestinationChains[0] = 0; - address[] memory _supportedTokens = new address[](4); - _supportedTokens[0] = wBTC; - _supportedTokens[1] = wETH; - _supportedTokens[2] = USDC; - _supportedTokens[3] = USDT; - config = new BridgeConfig(_chainID, _supportedTokens, _supportedDestinationChains); committee = new BridgeCommittee(); - committee.initialize(address(config), _committee, _stake, minStakeRequired); + committee.initialize(_committee, _stake, minStakeRequired); vault = new BridgeVault(wETH); - uint256[] memory tokenPrices = new uint256[](4); - tokenPrices[0] = 10000; // SUI PRICE - tokenPrices[1] = 10000; // BTC PRICE - tokenPrices[2] = 10000; // ETH PRICE - tokenPrices[3] = 10000; // USDC PRICE + tokenPrices = new uint64[](5); + tokenPrices[0] = 1 * USD_VALUE_MULTIPLIER; // SUI PRICE + tokenPrices[1] = 1 * USD_VALUE_MULTIPLIER; // BTC PRICE + tokenPrices[2] = 1 * USD_VALUE_MULTIPLIER; // ETH PRICE + tokenPrices[3] = 1 * USD_VALUE_MULTIPLIER; // USDC PRICE + tokenPrices[4] = 1 * USD_VALUE_MULTIPLIER; // USDT PRICE + uint8 _chainID = 2; + uint8[] memory _supportedDestinationChains = new uint8[](1); + _supportedDestinationChains[0] = 0; + address[] memory _supportedTokens = new address[](5); + _supportedTokens[0] = address(0); + _supportedTokens[1] = wBTC; + _supportedTokens[2] = wETH; + _supportedTokens[3] = USDC; + _supportedTokens[4] = USDT; + config = new BridgeConfig(); + config.initialize( + address(committee), _chainID, _supportedTokens, tokenPrices, _supportedDestinationChains + ); + + committee.initializeConfig(address(config)); + uint64[] memory totalLimits = new uint64[](1); - totalLimits[0] = 1000000; + totalLimits[0] = 100 * USD_VALUE_MULTIPLIER; skip(2 days); limiter = new BridgeLimiter(); - limiter.initialize( - address(committee), tokenPrices, _supportedDestinationChains, totalLimits - ); + limiter.initialize(address(committee), _supportedDestinationChains, totalLimits); bridge = new SuiBridge(); - bridge.initialize(address(committee), address(vault), address(limiter), wETH); + bridge.initialize(address(committee), address(vault), address(limiter)); bytes memory payload = hex"00"; // Create emergency op message - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.EMERGENCY_OP, + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.EMERGENCY_OP, version: 1, nonce: 55, chainID: _chainID, payload: payload }); - bytes memory encodedMessage = BridgeMessage.encodeMessage(message); + bytes memory encodedMessage = BridgeUtils.encodeMessage(message); bytes memory expectedEncodedMessage = - hex"5355495f4252494447455f4d455353414745020100000000000000370300"; + hex"5355495f4252494447455f4d455353414745020100000000000000370200"; assertEq(encodedMessage, expectedEncodedMessage); } @@ -871,44 +686,51 @@ contract SuiBridgeTest is BridgeBaseTest, ISuiBridge { // An e2e emergency op regression test covering message ser/de and signature verification function testEmergencyOpRegressionTestWithSigVerification() public { address[] memory _committee = new address[](4); - uint16[] memory _stake = new uint16[](4); - uint8 chainID = 11; _committee[0] = 0x68B43fD906C0B8F024a18C56e06744F7c6157c65; _committee[1] = 0xaCAEf39832CB995c4E049437A3E2eC6a7bad1Ab5; _committee[2] = 0x8061f127910e8eF56F16a2C411220BaD25D61444; _committee[3] = 0x508F3F1ff45F4ca3D8e86CDCC91445F00aCC59fC; + uint16[] memory _stake = new uint16[](4); _stake[0] = 2500; _stake[1] = 2500; _stake[2] = 2500; _stake[3] = 2500; - config = new BridgeConfig(chainID, supportedTokens, supportedChains); + committee = new BridgeCommittee(); - committee.initialize(address(config), _committee, _stake, minStakeRequired); + committee.initialize(_committee, _stake, minStakeRequired); + + uint8 chainID = 11; + + config = new BridgeConfig(); + config.initialize( + address(committee), chainID, supportedTokens, tokenPrices, supportedChains + ); + + committee.initializeConfig(address(config)); + vault = new BridgeVault(wETH); uint64[] memory totalLimits = new uint64[](1); - totalLimits[0] = 1000000; + totalLimits[0] = 100 * USD_VALUE_MULTIPLIER; skip(2 days); limiter = new BridgeLimiter(); - limiter.initialize( - address(committee), tokenPrices, supportedChains, totalLimits - ); + limiter.initialize(address(committee), supportedChains, totalLimits); bridge = new SuiBridge(); - bridge.initialize(address(committee), address(vault), address(limiter), wETH); + bridge.initialize(address(committee), address(vault), address(limiter)); assertFalse(bridge.paused()); // pause bytes memory payload = hex"00"; - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.EMERGENCY_OP, + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.EMERGENCY_OP, version: 1, nonce: 0, chainID: chainID, payload: payload }); - bytes memory encodedMessage = BridgeMessage.encodeMessage(message); + bytes memory encodedMessage = BridgeUtils.encodeMessage(message); bytes memory expectedEncodedMessage = hex"5355495f4252494447455f4d455353414745020100000000000000000b00"; @@ -924,35 +746,34 @@ contract SuiBridgeTest is BridgeBaseTest, ISuiBridge { // unpause payload = hex"01"; - message = BridgeMessage.Message({ - messageType: BridgeMessage.EMERGENCY_OP, + message = BridgeUtils.Message({ + messageType: BridgeUtils.EMERGENCY_OP, version: 1, nonce: 1, chainID: chainID, payload: payload }); - encodedMessage = BridgeMessage.encodeMessage(message); - expectedEncodedMessage = - hex"5355495f4252494447455f4d455353414745020100000000000000010b01"; + encodedMessage = BridgeUtils.encodeMessage(message); + expectedEncodedMessage = hex"5355495f4252494447455f4d455353414745020100000000000000010b01"; assertEq(encodedMessage, expectedEncodedMessage); - bytes[] memory signatures2 = new bytes[](3); + signatures = new bytes[](3); - signatures2[0] = + signatures[0] = hex"de5ca964c5aa1aa323cc480cd6de46eae980a1670a5fe8e12e31f724d0bcec6516e54b516737bb6ed6ccad775370c14d46f2e10100e9d16851d2050bf2349c6401"; - signatures2[1] = + signatures[1] = hex"fe8006e2013eaa7b8af0e5ac9f2890c2b2bd375d343684b2604ac6acd4142ccf5c9ec1914bce53a005232ef880bf0f597eed319d41d80e92d035c8314e1198ff00"; - signatures2[2] = + signatures[2] = hex"f5749ac37e11f22da0622082c9e63a91dc7b5c59cfdaa86438d9f6a53bbacf6b763126f1a20a826d7dff73252cf2fd68da67b9caec4d3c24a07fbd566a7a6bec00"; - bridge.executeEmergencyOpWithSignatures(signatures2, message); + bridge.executeEmergencyOpWithSignatures(signatures, message); assertFalse(bridge.paused()); // reusing the sig from nonce 0 will revert payload = hex"00"; - message = BridgeMessage.Message({ - messageType: BridgeMessage.EMERGENCY_OP, + message = BridgeUtils.Message({ + messageType: BridgeUtils.EMERGENCY_OP, version: 1, nonce: 0, chainID: chainID, @@ -983,33 +804,37 @@ contract SuiBridgeTest is BridgeBaseTest, ISuiBridge { _stake[2] = 2500; _stake[3] = 2500; committee = new BridgeCommittee(); - + committee.initialize(_committee, _stake, minStakeRequired); + vault = new BridgeVault(wETH); + tokenPrices = new uint64[](5); + tokenPrices[0] = 1 * USD_VALUE_MULTIPLIER; // SUI PRICE + tokenPrices[1] = 1 * USD_VALUE_MULTIPLIER; // BTC PRICE + tokenPrices[2] = 1 * USD_VALUE_MULTIPLIER; // ETH PRICE + tokenPrices[3] = 1 * USD_VALUE_MULTIPLIER; // USDC PRICE + tokenPrices[4] = 1 * USD_VALUE_MULTIPLIER; // USDT PRICE uint8 _chainID = 12; uint8[] memory _supportedDestinationChains = new uint8[](1); _supportedDestinationChains[0] = 0; - address[] memory _supportedTokens = new address[](4); - _supportedTokens[0] = wBTC; - _supportedTokens[1] = wETH; - _supportedTokens[2] = USDC; - _supportedTokens[3] = USDT; - config = new BridgeConfig(_chainID, _supportedTokens, _supportedDestinationChains); - - committee.initialize(address(config), _committee, _stake, minStakeRequired); - vault = new BridgeVault(wETH); - uint256[] memory tokenPrices = new uint256[](4); - tokenPrices[0] = 10000; // SUI PRICE - tokenPrices[1] = 10000; // BTC PRICE - tokenPrices[2] = 10000; // ETH PRICE - tokenPrices[3] = 10000; // USDC PRICE + address[] memory _supportedTokens = new address[](5); + _supportedTokens[0] = address(0); + _supportedTokens[1] = wBTC; + _supportedTokens[2] = wETH; + _supportedTokens[3] = USDC; + _supportedTokens[4] = USDT; + config = new BridgeConfig(); + config.initialize( + address(committee), _chainID, _supportedTokens, tokenPrices, _supportedDestinationChains + ); + + committee.initializeConfig(address(config)); + skip(2 days); uint64[] memory totalLimits = new uint64[](1); - totalLimits[0] = 1000000; + totalLimits[0] = 1_000_000 * USD_VALUE_MULTIPLIER; limiter = new BridgeLimiter(); - limiter.initialize( - address(committee), tokenPrices, _supportedDestinationChains, totalLimits - ); + limiter.initialize(address(committee), _supportedDestinationChains, totalLimits); bridge = new SuiBridge(); - bridge.initialize(address(committee), address(vault), address(limiter), wETH); + bridge.initialize(address(committee), address(vault), address(limiter)); vault.transferOwnership(address(bridge)); limiter.transferOwnership(address(bridge)); @@ -1021,21 +846,21 @@ contract SuiBridgeTest is BridgeBaseTest, ISuiBridge { bytes memory payload = hex"00000000000000000000000006060606060606060606060606060606060606060000000000000000000000000909090909090909090909090909090909090909000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000045cd8a76b00000000000000000000000000000000000000000000000000000000"; // Create transfer message - BridgeMessage.Message memory message = BridgeMessage.Message({ - messageType: BridgeMessage.UPGRADE, + BridgeUtils.Message memory message = BridgeUtils.Message({ + messageType: BridgeUtils.UPGRADE, version: 1, nonce: 123, chainID: _chainID, payload: payload }); - bytes memory encodedMessage = BridgeMessage.encodeMessage(message); + bytes memory encodedMessage = BridgeUtils.encodeMessage(message); bytes memory expectedEncodedMessage = hex"5355495f4252494447455f4d4553534147450501000000000000007b0c00000000000000000000000006060606060606060606060606060606060606060000000000000000000000000909090909090909090909090909090909090909000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000045cd8a76b00000000000000000000000000000000000000000000000000000000"; assertEq(encodedMessage, expectedEncodedMessage); (address proxy, address newImp, bytes memory _calldata) = - BridgeMessage.decodeUpgradePayload(payload); + BridgeUtils.decodeUpgradePayload(payload); assertEq(proxy, address(0x0606060606060606060606060606060606060606)); assertEq(newImp, address(0x0909090909090909090909090909090909090909)); diff --git a/bridge/evm/test/mocks/MockSuiBridgeV2.sol b/bridge/evm/test/mocks/MockSuiBridgeV2.sol index e816b5b2fa088..ad6ea31fb6fd8 100644 --- a/bridge/evm/test/mocks/MockSuiBridgeV2.sol +++ b/bridge/evm/test/mocks/MockSuiBridgeV2.sol @@ -11,6 +11,16 @@ contract MockSuiBridgeV2 is SuiBridge { _pause(); } + function initializeV2Params(uint256 value, bool _override, string memory _event) external { + if (_override) { + _pause(); + } else if (value == 42) { + _pause(); + } + + emit MockEvent(_event); + } + function newMockFunction(bool _pausing) external { isPausing = _pausing; } @@ -21,5 +31,7 @@ contract MockSuiBridgeV2 is SuiBridge { } // used to ignore for forge coverage - function test() external view {} + function testSkip() external view {} + + event MockEvent(string _event); } diff --git a/bridge/evm/test/mocks/MockTokens.sol b/bridge/evm/test/mocks/MockTokens.sol index 415cc90fa436a..3a1bfbd059f39 100644 --- a/bridge/evm/test/mocks/MockTokens.sol +++ b/bridge/evm/test/mocks/MockTokens.sol @@ -18,7 +18,7 @@ contract MockWBTC is ERC20 { return 8; } - function testMock() public {} + function testSkip() public {} } contract MockUSDC is ERC20 { @@ -36,25 +36,7 @@ contract MockUSDC is ERC20 { return 6; } - function testMock() public {} -} - -contract MockSmallUSDC is ERC20 { - constructor() ERC20("USD Coin", "USDC") {} - - function mint(address to, uint256 amount) public virtual { - _mint(to, amount); - } - - function burn(address form, uint256 amount) public virtual { - _burn(form, amount); - } - - function decimals() public view virtual override returns (uint8) { - return 5; - } - - function testMock() public {} + function testSkip() public {} } contract MockUSDT is ERC20 { @@ -72,10 +54,9 @@ contract MockUSDT is ERC20 { return 6; } - function testMock() public {} + function testSkip() public {} } - contract WETH { string public name = "Wrapped Ether"; string public symbol = "WETH"; @@ -131,5 +112,5 @@ contract WETH { return true; } - function testMock() public {} + function testSkip() public {} } diff --git a/bridge/move/tokens/btc/Move.toml b/bridge/move/tokens/btc/Move.toml new file mode 100644 index 0000000000000..cbd69d8556431 --- /dev/null +++ b/bridge/move/tokens/btc/Move.toml @@ -0,0 +1,11 @@ +[package] +name = "BridgedBTC" +version = "0.0.1" + +[dependencies] +MoveStdlib = { local = "../../../../crates/sui-framework/packages/move-stdlib" } +Sui = { local = "../../../../crates/sui-framework/packages/sui-framework" } + +[addresses] +bridged_btc = "0x0" + diff --git a/bridge/move/tokens/btc/sources/btc.move b/bridge/move/tokens/btc/sources/btc.move new file mode 100644 index 0000000000000..d3872d449195f --- /dev/null +++ b/bridge/move/tokens/btc/sources/btc.move @@ -0,0 +1,29 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module bridged_btc::btc { + use std::option; + + use sui::coin; + use sui::transfer; + use sui::tx_context; + use sui::tx_context::TxContext; + + struct BTC has drop {} + + const DECIMAL: u8 = 8; + + fun init(otw: BTC, ctx: &mut TxContext) { + let (treasury_cap, metadata) = coin::create_currency( + otw, + DECIMAL, + b"BTC", + b"Bitcoin", + b"Bridged Bitcoin token", + option::none(), + ctx + ); + transfer::public_freeze_object(metadata); + transfer::public_transfer(treasury_cap, tx_context::sender(ctx)); + } +} diff --git a/bridge/move/tokens/eth/Move.toml b/bridge/move/tokens/eth/Move.toml new file mode 100644 index 0000000000000..215aa37ce8687 --- /dev/null +++ b/bridge/move/tokens/eth/Move.toml @@ -0,0 +1,11 @@ +[package] +name = "BridgedETH" +version = "0.0.1" + +[dependencies] +MoveStdlib = { local = "../../../../crates/sui-framework/packages/move-stdlib" } +Sui = { local = "../../../../crates/sui-framework/packages/sui-framework" } + +[addresses] +bridged_eth = "0x0" + diff --git a/bridge/move/tokens/eth/sources/eth.move b/bridge/move/tokens/eth/sources/eth.move new file mode 100644 index 0000000000000..03aea25804fef --- /dev/null +++ b/bridge/move/tokens/eth/sources/eth.move @@ -0,0 +1,29 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module bridged_eth::eth { + use std::option; + + use sui::coin; + use sui::transfer; + use sui::tx_context; + use sui::tx_context::TxContext; + + struct ETH has drop {} + + const DECIMAL: u8 = 8; + + fun init(otw: ETH, ctx: &mut TxContext) { + let (treasury_cap, metadata) = coin::create_currency( + otw, + DECIMAL, + b"ETH", + b"Ethereum", + b"Bridged Ethereum token", + option::none(), + ctx + ); + transfer::public_freeze_object(metadata); + transfer::public_transfer(treasury_cap, tx_context::sender(ctx)) + } +} diff --git a/bridge/move/tokens/mock/ka/Move.toml b/bridge/move/tokens/mock/ka/Move.toml new file mode 100644 index 0000000000000..ad08496bd39e1 --- /dev/null +++ b/bridge/move/tokens/mock/ka/Move.toml @@ -0,0 +1,11 @@ +[package] +name = "BridgedKa" +version = "0.0.1" + +[dependencies] +MoveStdlib = { local = "../../../../../crates/sui-framework/packages/move-stdlib" } +Sui = { local = "../../../../../crates/sui-framework/packages/sui-framework" } + +[addresses] +bridged_ka = "0x0" + diff --git a/bridge/move/tokens/mock/ka/sources/ka.move b/bridge/move/tokens/mock/ka/sources/ka.move new file mode 100644 index 0000000000000..db66070efd780 --- /dev/null +++ b/bridge/move/tokens/mock/ka/sources/ka.move @@ -0,0 +1,29 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module bridged_ka::ka { + use std::option; + + use sui::coin; + use sui::transfer; + use sui::tx_context; + use sui::tx_context::TxContext; + + struct KA has drop {} + + const DECIMAL: u8 = 9; + + fun init(otw: KA, ctx: &mut TxContext) { + let (treasury_cap, metadata) = coin::create_currency( + otw, + DECIMAL, + b"Ka", + b"Ka Coin", + b"Ka, the opposite of Sui", + option::none(), + ctx + ); + transfer::public_freeze_object(metadata); + transfer::public_transfer(treasury_cap, tx_context::sender(ctx)); + } +} diff --git a/bridge/move/tokens/usdc/Move.toml b/bridge/move/tokens/usdc/Move.toml new file mode 100644 index 0000000000000..e98ae4d71c621 --- /dev/null +++ b/bridge/move/tokens/usdc/Move.toml @@ -0,0 +1,11 @@ +[package] +name = "BridgedUSDC" +version = "0.0.1" + +[dependencies] +MoveStdlib = { local = "../../../../crates/sui-framework/packages/move-stdlib" } +Sui = { local = "../../../../crates/sui-framework/packages/sui-framework" } + +[addresses] +bridged_usdc = "0x0" + diff --git a/bridge/move/tokens/usdc/sources/usdc.move b/bridge/move/tokens/usdc/sources/usdc.move new file mode 100644 index 0000000000000..b5af7603de241 --- /dev/null +++ b/bridge/move/tokens/usdc/sources/usdc.move @@ -0,0 +1,29 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module bridged_usdc::usdc { + use std::option; + + use sui::coin; + use sui::transfer; + use sui::tx_context; + use sui::tx_context::TxContext; + + struct USDC has drop {} + + const DECIMAL: u8 = 6; + + fun init(otw: USDC, ctx: &mut TxContext) { + let (treasury_cap, metadata) = coin::create_currency( + otw, + DECIMAL, + b"USDC", + b"USD Coin", + b"Bridged USD Coin token", + option::none(), + ctx + ); + transfer::public_freeze_object(metadata); + transfer::public_transfer(treasury_cap, tx_context::sender(ctx)); + } +} diff --git a/bridge/move/tokens/usdt/Move.toml b/bridge/move/tokens/usdt/Move.toml new file mode 100644 index 0000000000000..699123ddb0325 --- /dev/null +++ b/bridge/move/tokens/usdt/Move.toml @@ -0,0 +1,11 @@ +[package] +name = "BridgedUSDT" +version = "0.0.1" + +[dependencies] +MoveStdlib = { local = "../../../../crates/sui-framework/packages/move-stdlib" } +Sui = { local = "../../../../crates/sui-framework/packages/sui-framework" } + +[addresses] +bridged_usdt = "0x0" + diff --git a/bridge/move/tokens/usdt/sources/usdt.move b/bridge/move/tokens/usdt/sources/usdt.move new file mode 100644 index 0000000000000..b46fd3c417564 --- /dev/null +++ b/bridge/move/tokens/usdt/sources/usdt.move @@ -0,0 +1,29 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module bridged_usdt::usdt { + use std::option; + + use sui::coin; + use sui::transfer; + use sui::tx_context; + use sui::tx_context::TxContext; + + struct USDT has drop {} + + const DECIMAL: u8 = 6; + + fun init(otw: USDT, ctx: &mut TxContext) { + let (treasury_cap, metadata) = coin::create_currency( + otw, + DECIMAL, + b"USDT", + b"Tether", + b"Bridged Tether token", + option::none(), + ctx + ); + transfer::public_freeze_object(metadata); + transfer::public_transfer(treasury_cap, tx_context::sender(ctx)); + } +} diff --git a/crates/sui-bridge/Cargo.toml b/crates/sui-bridge/Cargo.toml index fc820400ec5e4..451c551e253eb 100644 --- a/crates/sui-bridge/Cargo.toml +++ b/crates/sui-bridge/Cargo.toml @@ -44,9 +44,9 @@ tap.workspace = true rand.workspace = true lru.workspace = true shared-crypto.workspace = true -backoff = { version = "0.4.0", features = [ - "futures", -] } +backoff.workspace = true +enum_dispatch.workspace = true +sui-json-rpc-api.workspace = true [dev-dependencies] sui-types = { workspace = true, features = ["test-utils"] } diff --git a/crates/sui-bridge/abi/bridge_committee.json b/crates/sui-bridge/abi/bridge_committee.json index e67f2e5c3d7bf..23ff17267c16d 100644 --- a/crates/sui-bridge/abi/bridge_committee.json +++ b/crates/sui-bridge/abi/bridge_committee.json @@ -1,47 +1,84 @@ [ { - "anonymous": false, "inputs": [ { - "indexed": false, "internalType": "address", - "name": "previousAdmin", + "name": "target", "type": "address" - }, + } + ], + "name": "AddressEmptyCode", + "type": "error" + }, + { + "inputs": [ { - "indexed": false, "internalType": "address", - "name": "newAdmin", + "name": "implementation", "type": "address" } ], - "name": "AdminChanged", - "type": "event" + "name": "ERC1967InvalidImplementation", + "type": "error" + }, + { + "inputs": [], + "name": "ERC1967NonPayable", + "type": "error" + }, + { + "inputs": [], + "name": "FailedInnerCall", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidInitialization", + "type": "error" + }, + { + "inputs": [], + "name": "NotInitializing", + "type": "error" + }, + { + "inputs": [], + "name": "ReentrancyGuardReentrantCall", + "type": "error" + }, + { + "inputs": [], + "name": "UUPSUnauthorizedCallContext", + "type": "error" }, { - "anonymous": false, "inputs": [ { - "indexed": true, - "internalType": "address", - "name": "beacon", - "type": "address" + "internalType": "bytes32", + "name": "slot", + "type": "bytes32" } ], - "name": "BeaconUpgraded", - "type": "event" + "name": "UUPSUnsupportedProxiableUUID", + "type": "error" }, { "anonymous": false, "inputs": [ { "indexed": false, - "internalType": "uint8", - "name": "version", - "type": "uint8" + "internalType": "address[]", + "name": "updatedMembers", + "type": "address[]" + }, + { + "indexed": false, + "internalType": "bool", + "name": "isBlocklisted", + "type": "bool" } ], - "name": "Initialized", + "name": "BlocklistUpdated", "type": "event" }, { @@ -49,12 +86,12 @@ "inputs": [ { "indexed": false, - "internalType": "bytes", - "name": "message", - "type": "bytes" + "internalType": "uint64", + "name": "version", + "type": "uint64" } ], - "name": "MessageProcessed", + "name": "Initialized", "type": "event" }, { @@ -72,25 +109,12 @@ }, { "inputs": [], - "name": "BLOCKLIST_STAKE_REQUIRED", + "name": "UPGRADE_INTERFACE_VERSION", "outputs": [ { - "internalType": "uint16", + "internalType": "string", "name": "", - "type": "uint16" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "COMMITTEE_UPGRADE_STAKE_REQUIRED", - "outputs": [ - { - "internalType": "uint16", - "name": "", - "type": "uint16" + "type": "string" } ], "stateMutability": "view", @@ -100,7 +124,7 @@ "inputs": [ { "internalType": "address", - "name": "", + "name": "committeeMember", "type": "address" } ], @@ -108,7 +132,7 @@ "outputs": [ { "internalType": "bool", - "name": "", + "name": "isBlocklisted", "type": "bool" } ], @@ -116,19 +140,13 @@ "type": "function" }, { - "inputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], + "inputs": [], "name": "committee", "outputs": [ { - "internalType": "uint16", + "internalType": "contract IBridgeCommittee", "name": "", - "type": "uint16" + "type": "address" } ], "stateMutability": "view", @@ -137,57 +155,70 @@ { "inputs": [ { - "internalType": "bytes", - "name": "payload", - "type": "bytes" + "internalType": "address", + "name": "committeeMember", + "type": "address" } ], - "name": "decodeBlocklistPayload", + "name": "committeeIndex", "outputs": [ { - "internalType": "bool", - "name": "", - "type": "bool" - }, - { - "internalType": "address[]", - "name": "", - "type": "address[]" + "internalType": "uint8", + "name": "index", + "type": "uint8" } ], - "stateMutability": "pure", + "stateMutability": "view", "type": "function" }, { "inputs": [ { - "internalType": "bytes", - "name": "payload", - "type": "bytes" + "internalType": "address", + "name": "committeeMember", + "type": "address" } ], - "name": "decodeUpgradePayload", + "name": "committeeStake", "outputs": [ { - "internalType": "address", + "internalType": "uint16", + "name": "stakeAmount", + "type": "uint16" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "config", + "outputs": [ + { + "internalType": "contract IBridgeConfig", "name": "", "type": "address" } ], - "stateMutability": "pure", + "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "address[]", - "name": "_committee", + "name": "committee", "type": "address[]" }, { "internalType": "uint16[]", - "name": "stakes", + "name": "stake", "type": "uint16[]" + }, + { + "internalType": "uint16", + "name": "minStakeRequired", + "type": "uint16" } ], "name": "initialize", @@ -195,11 +226,24 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "_config", + "type": "address" + } + ], + "name": "initializeConfig", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { "internalType": "uint8", - "name": "", + "name": "messageType", "type": "uint8" } ], @@ -207,7 +251,7 @@ "outputs": [ { "internalType": "uint64", - "name": "", + "name": "nonce", "type": "uint64" } ], @@ -262,7 +306,7 @@ "type": "bytes" } ], - "internalType": "struct Messages.Message", + "internalType": "struct BridgeUtils.Message", "name": "message", "type": "tuple" } @@ -272,6 +316,24 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, { "inputs": [ { @@ -307,47 +369,16 @@ "type": "bytes" } ], - "internalType": "struct Messages.Message", + "internalType": "struct BridgeUtils.Message", "name": "message", "type": "tuple" } ], - "name": "upgradeCommitteeWithSignatures", + "name": "upgradeWithSignatures", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [ - { - "internalType": "address", - "name": "newImplementation", - "type": "address" - } - ], - "name": "upgradeTo", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "newImplementation", - "type": "address" - }, - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - } - ], - "name": "upgradeToAndCall", - "outputs": [], - "stateMutability": "payable", - "type": "function" - }, { "inputs": [ { @@ -356,24 +387,40 @@ "type": "bytes[]" }, { - "internalType": "bytes32", - "name": "messageHash", - "type": "bytes32" - }, - { - "internalType": "uint16", - "name": "requiredStake", - "type": "uint16" - } - ], - "name": "verifyMessageSignatures", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" + "components": [ + { + "internalType": "uint8", + "name": "messageType", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "version", + "type": "uint8" + }, + { + "internalType": "uint64", + "name": "nonce", + "type": "uint64" + }, + { + "internalType": "uint8", + "name": "chainID", + "type": "uint8" + }, + { + "internalType": "bytes", + "name": "payload", + "type": "bytes" + } + ], + "internalType": "struct BridgeUtils.Message", + "name": "message", + "type": "tuple" } ], + "name": "verifySignatures", + "outputs": [], "stateMutability": "view", "type": "function" } diff --git a/crates/sui-bridge/abi/bridge_committee_upgradeable.json b/crates/sui-bridge/abi/bridge_committee_upgradeable.json new file mode 100644 index 0000000000000..9bd3d2624650d --- /dev/null +++ b/crates/sui-bridge/abi/bridge_committee_upgradeable.json @@ -0,0 +1,212 @@ +[ + { + "type": "function", + "name": "UPGRADE_INTERFACE_VERSION", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "committee", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract IBridgeCommittee" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "nonces", + "inputs": [ + { + "name": "messageType", + "type": "uint8", + "internalType": "uint8" + } + ], + "outputs": [ + { + "name": "nonce", + "type": "uint64", + "internalType": "uint64" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "proxiableUUID", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "upgradeToAndCall", + "inputs": [ + { + "name": "newImplementation", + "type": "address", + "internalType": "address" + }, + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "upgradeWithSignatures", + "inputs": [ + { + "name": "signatures", + "type": "bytes[]", + "internalType": "bytes[]" + }, + { + "name": "message", + "type": "tuple", + "internalType": "struct BridgeUtils.Message", + "components": [ + { + "name": "messageType", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "version", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "nonce", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "chainID", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "payload", + "type": "bytes", + "internalType": "bytes" + } + ] + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "event", + "name": "Initialized", + "inputs": [ + { + "name": "version", + "type": "uint64", + "indexed": false, + "internalType": "uint64" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Upgraded", + "inputs": [ + { + "name": "implementation", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "AddressEmptyCode", + "inputs": [ + { + "name": "target", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ERC1967InvalidImplementation", + "inputs": [ + { + "name": "implementation", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ERC1967NonPayable", + "inputs": [] + }, + { + "type": "error", + "name": "FailedInnerCall", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidInitialization", + "inputs": [] + }, + { + "type": "error", + "name": "NotInitializing", + "inputs": [] + }, + { + "type": "error", + "name": "ReentrancyGuardReentrantCall", + "inputs": [] + }, + { + "type": "error", + "name": "UUPSUnauthorizedCallContext", + "inputs": [] + }, + { + "type": "error", + "name": "UUPSUnsupportedProxiableUUID", + "inputs": [ + { + "name": "slot", + "type": "bytes32", + "internalType": "bytes32" + } + ] + } +] \ No newline at end of file diff --git a/crates/sui-bridge/abi/bridge_config.json b/crates/sui-bridge/abi/bridge_config.json new file mode 100644 index 0000000000000..48997486cd1c7 --- /dev/null +++ b/crates/sui-bridge/abi/bridge_config.json @@ -0,0 +1,560 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "target", + "type": "address" + } + ], + "name": "AddressEmptyCode", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "ERC1967InvalidImplementation", + "type": "error" + }, + { + "inputs": [], + "name": "ERC1967NonPayable", + "type": "error" + }, + { + "inputs": [], + "name": "FailedInnerCall", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidInitialization", + "type": "error" + }, + { + "inputs": [], + "name": "NotInitializing", + "type": "error" + }, + { + "inputs": [], + "name": "ReentrancyGuardReentrantCall", + "type": "error" + }, + { + "inputs": [], + "name": "UUPSUnauthorizedCallContext", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "slot", + "type": "bytes32" + } + ], + "name": "UUPSUnsupportedProxiableUUID", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "version", + "type": "uint64" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "tokenID", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "suiDecimal", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint64", + "name": "tokenPrice", + "type": "uint64" + } + ], + "name": "TokenAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "tokenID", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint64", + "name": "tokenPrice", + "type": "uint64" + } + ], + "name": "TokenPriceUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "UPGRADE_INTERFACE_VERSION", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes[]", + "name": "signatures", + "type": "bytes[]" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "messageType", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "version", + "type": "uint8" + }, + { + "internalType": "uint64", + "name": "nonce", + "type": "uint64" + }, + { + "internalType": "uint8", + "name": "chainID", + "type": "uint8" + }, + { + "internalType": "bytes", + "name": "payload", + "type": "bytes" + } + ], + "internalType": "struct BridgeUtils.Message", + "name": "message", + "type": "tuple" + } + ], + "name": "addTokensWithSignatures", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "chainID", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "committee", + "outputs": [ + { + "internalType": "contract IBridgeCommittee", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_committee", + "type": "address" + }, + { + "internalType": "uint8", + "name": "_chainID", + "type": "uint8" + }, + { + "internalType": "address[]", + "name": "_supportedTokens", + "type": "address[]" + }, + { + "internalType": "uint64[]", + "name": "_tokenPrices", + "type": "uint64[]" + }, + { + "internalType": "uint8[]", + "name": "_supportedChains", + "type": "uint8[]" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "chainId", + "type": "uint8" + } + ], + "name": "isChainSupported", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "tokenID", + "type": "uint8" + } + ], + "name": "isTokenSupported", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "messageType", + "type": "uint8" + } + ], + "name": "nonces", + "outputs": [ + { + "internalType": "uint64", + "name": "nonce", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "chainId", + "type": "uint8" + } + ], + "name": "supportedChains", + "outputs": [ + { + "internalType": "bool", + "name": "isSupported", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "tokenID", + "type": "uint8" + } + ], + "name": "supportedTokens", + "outputs": [ + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "internalType": "uint8", + "name": "suiDecimal", + "type": "uint8" + }, + { + "internalType": "bool", + "name": "native", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "tokenID", + "type": "uint8" + } + ], + "name": "tokenAddressOf", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "tokenID", + "type": "uint8" + } + ], + "name": "tokenPriceOf", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "tokenID", + "type": "uint8" + } + ], + "name": "tokenPrices", + "outputs": [ + { + "internalType": "uint64", + "name": "tokenPrice", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "tokenID", + "type": "uint8" + } + ], + "name": "tokenSuiDecimalOf", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes[]", + "name": "signatures", + "type": "bytes[]" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "messageType", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "version", + "type": "uint8" + }, + { + "internalType": "uint64", + "name": "nonce", + "type": "uint64" + }, + { + "internalType": "uint8", + "name": "chainID", + "type": "uint8" + }, + { + "internalType": "bytes", + "name": "payload", + "type": "bytes" + } + ], + "internalType": "struct BridgeUtils.Message", + "name": "message", + "type": "tuple" + } + ], + "name": "updateTokenPriceWithSignatures", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes[]", + "name": "signatures", + "type": "bytes[]" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "messageType", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "version", + "type": "uint8" + }, + { + "internalType": "uint64", + "name": "nonce", + "type": "uint64" + }, + { + "internalType": "uint8", + "name": "chainID", + "type": "uint8" + }, + { + "internalType": "bytes", + "name": "payload", + "type": "bytes" + } + ], + "internalType": "struct BridgeUtils.Message", + "name": "message", + "type": "tuple" + } + ], + "name": "upgradeWithSignatures", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/crates/sui-bridge/abi/bridge_limiter.json b/crates/sui-bridge/abi/bridge_limiter.json new file mode 100644 index 0000000000000..fb0169a17e3a5 --- /dev/null +++ b/crates/sui-bridge/abi/bridge_limiter.json @@ -0,0 +1,605 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "target", + "type": "address" + } + ], + "name": "AddressEmptyCode", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "ERC1967InvalidImplementation", + "type": "error" + }, + { + "inputs": [], + "name": "ERC1967NonPayable", + "type": "error" + }, + { + "inputs": [], + "name": "FailedInnerCall", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidInitialization", + "type": "error" + }, + { + "inputs": [], + "name": "NotInitializing", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "OwnableInvalidOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "OwnableUnauthorizedAccount", + "type": "error" + }, + { + "inputs": [], + "name": "ReentrancyGuardReentrantCall", + "type": "error" + }, + { + "inputs": [], + "name": "UUPSUnauthorizedCallContext", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "slot", + "type": "bytes32" + } + ], + "name": "UUPSUnsupportedProxiableUUID", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint32", + "name": "hourUpdated", + "type": "uint32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "HourlyTransferAmountUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "version", + "type": "uint64" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "sourceChainID", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint64", + "name": "newLimit", + "type": "uint64" + } + ], + "name": "LimitUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "UPGRADE_INTERFACE_VERSION", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "tokenID", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "calculateAmountInUSD", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "chainID", + "type": "uint8" + } + ], + "name": "calculateWindowAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "total", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "chainHourTimestamp", + "type": "uint256" + } + ], + "name": "chainHourlyTransferAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "totalAmountBridged", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "chainID", + "type": "uint8" + } + ], + "name": "chainLimits", + "outputs": [ + { + "internalType": "uint64", + "name": "totalLimit", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "committee", + "outputs": [ + { + "internalType": "contract IBridgeCommittee", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "currentHour", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "chainID", + "type": "uint8" + }, + { + "internalType": "uint32", + "name": "hourTimestamp", + "type": "uint32" + } + ], + "name": "getChainHourTimestampKey", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_committee", + "type": "address" + }, + { + "internalType": "uint8[]", + "name": "chainIDs", + "type": "uint8[]" + }, + { + "internalType": "uint64[]", + "name": "_totalLimits", + "type": "uint64[]" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "messageType", + "type": "uint8" + } + ], + "name": "nonces", + "outputs": [ + { + "internalType": "uint64", + "name": "nonce", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "chainID", + "type": "uint8" + } + ], + "name": "oldestChainTimestamp", + "outputs": [ + { + "internalType": "uint32", + "name": "oldestHourTimestamp", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "chainID", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "tokenID", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "recordBridgeTransfers", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes[]", + "name": "signatures", + "type": "bytes[]" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "messageType", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "version", + "type": "uint8" + }, + { + "internalType": "uint64", + "name": "nonce", + "type": "uint64" + }, + { + "internalType": "uint8", + "name": "chainID", + "type": "uint8" + }, + { + "internalType": "bytes", + "name": "payload", + "type": "bytes" + } + ], + "internalType": "struct BridgeUtils.Message", + "name": "message", + "type": "tuple" + } + ], + "name": "updateLimitWithSignatures", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes[]", + "name": "signatures", + "type": "bytes[]" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "messageType", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "version", + "type": "uint8" + }, + { + "internalType": "uint64", + "name": "nonce", + "type": "uint64" + }, + { + "internalType": "uint8", + "name": "chainID", + "type": "uint8" + }, + { + "internalType": "bytes", + "name": "payload", + "type": "bytes" + } + ], + "internalType": "struct BridgeUtils.Message", + "name": "message", + "type": "tuple" + } + ], + "name": "upgradeWithSignatures", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "chainID", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "tokenID", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "willAmountExceedLimit", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "chainID", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "willUSDAmountExceedLimit", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/crates/sui-bridge/abi/bridge_vault.json b/crates/sui-bridge/abi/bridge_vault.json index a55aa4d767a44..71a0893fd9272 100644 --- a/crates/sui-bridge/abi/bridge_vault.json +++ b/crates/sui-bridge/abi/bridge_vault.json @@ -10,6 +10,28 @@ "stateMutability": "nonpayable", "type": "constructor" }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "OwnableInvalidOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "OwnableUnauthorizedAccount", + "type": "error" + }, { "anonymous": false, "inputs": [ @@ -29,10 +51,6 @@ "name": "OwnershipTransferred", "type": "event" }, - { - "stateMutability": "payable", - "type": "fallback" - }, { "inputs": [], "name": "owner", @@ -62,7 +80,7 @@ }, { "internalType": "address", - "name": "targetAddress", + "name": "recipientAddress", "type": "address" }, { @@ -80,7 +98,7 @@ "inputs": [ { "internalType": "address payable", - "name": "targetAddress", + "name": "recipientAddress", "type": "address" }, { diff --git a/crates/sui-bridge/abi/sui_bridge.json b/crates/sui-bridge/abi/sui_bridge.json index f9b0cea834378..273877f530af8 100644 --- a/crates/sui-bridge/abi/sui_bridge.json +++ b/crates/sui-bridge/abi/sui_bridge.json @@ -1,644 +1,581 @@ [ - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "previousAdmin", - "type": "address" - }, - { - "indexed": false, - "internalType": "address", - "name": "newAdmin", - "type": "address" - } - ], - "name": "AdminChanged", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "beacon", - "type": "address" - } - ], - "name": "BeaconUpgraded", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint8", - "name": "version", - "type": "uint8" - } - ], - "name": "Initialized", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "Paused", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "uint8", - "name": "sourceChainId", - "type": "uint8" - }, - { - "indexed": true, - "internalType": "uint64", - "name": "nonce", - "type": "uint64" - }, - { - "indexed": true, - "internalType": "uint8", - "name": "destinationChainId", - "type": "uint8" - }, - { - "indexed": false, - "internalType": "uint8", - "name": "tokenCode", - "type": "uint8" - }, - { - "indexed": false, - "internalType": "uint64", - "name": "suiAdjustedAmount", - "type": "uint64" - }, - { - "indexed": false, - "internalType": "address", - "name": "sourceAddress", - "type": "address" - }, - { - "indexed": false, - "internalType": "bytes", - "name": "targetAddress", - "type": "bytes" - } - ], - "name": "TokensBridgedToSui", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "Unpaused", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "implementation", - "type": "address" - } - ], - "name": "Upgraded", - "type": "event" - }, - { - "inputs": [], - "name": "BRIDGE_UPGRADE_STAKE_REQUIRED", - "outputs": [ - { - "internalType": "uint16", - "name": "", - "type": "uint16" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "FREEZING_STAKE_REQUIRED", - "outputs": [ - { - "internalType": "uint16", - "name": "", - "type": "uint16" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "TRANSFER_STAKE_REQUIRED", - "outputs": [ - { - "internalType": "uint16", - "name": "", - "type": "uint16" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "UNFREEZING_STAKE_REQUIRED", - "outputs": [ - { - "internalType": "uint16", - "name": "", - "type": "uint16" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint8", - "name": "tokenId", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "originalAmount", - "type": "uint64" - }, - { - "internalType": "uint8", - "name": "ethDecimal", - "type": "uint8" - } - ], - "name": "adjustDecimalsForErc20", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint8", - "name": "tokenId", - "type": "uint8" - }, - { - "internalType": "uint256", - "name": "originalAmount", - "type": "uint256" - }, - { - "internalType": "uint8", - "name": "ethDecimal", - "type": "uint8" - } - ], - "name": "adjustDecimalsForSuiToken", - "outputs": [ - { - "internalType": "uint64", - "name": "", - "type": "uint64" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes", - "name": "targetAddress", - "type": "bytes" - }, - { - "internalType": "uint8", - "name": "destinationChainId", - "type": "uint8" - } - ], - "name": "bridgeETHToSui", - "outputs": [], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint8", - "name": "tokenId", - "type": "uint8" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - }, - { - "internalType": "bytes", - "name": "targetAddress", - "type": "bytes" - }, - { - "internalType": "uint8", - "name": "destinationChainId", - "type": "uint8" - } - ], - "name": "bridgeToSui", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "chainId", - "outputs": [ - { - "internalType": "uint8", - "name": "", - "type": "uint8" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "committee", - "outputs": [ - { - "internalType": "contract IBridgeCommittee", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes[]", - "name": "signatures", - "type": "bytes[]" - }, - { - "components": [ - { - "internalType": "uint8", - "name": "messageType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "version", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "nonce", - "type": "uint64" - }, - { - "internalType": "uint8", - "name": "chainID", - "type": "uint8" - }, - { - "internalType": "bytes", - "name": "payload", - "type": "bytes" - } - ], - "internalType": "struct Messages.Message", - "name": "message", - "type": "tuple" - } - ], - "name": "executeEmergencyOpWithSignatures", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address[]", - "name": "_supportedTokens", - "type": "address[]" - }, - { - "internalType": "address", - "name": "_committee", - "type": "address" - }, - { - "internalType": "address", - "name": "_vault", - "type": "address" - }, - { - "internalType": "address", - "name": "_weth9", - "type": "address" - }, - { - "internalType": "uint8", - "name": "_chainId", - "type": "uint8" - } - ], - "name": "initialize", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint64", - "name": "", - "type": "uint64" - } - ], - "name": "messageProcessed", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint8", - "name": "", - "type": "uint8" - } - ], - "name": "nonces", - "outputs": [ - { - "internalType": "uint64", - "name": "", - "type": "uint64" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "paused", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "proxiableUUID", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint8", - "name": "", - "type": "uint8" - } - ], - "name": "requiredApprovalStake", - "outputs": [ - { - "internalType": "uint16", - "name": "", - "type": "uint16" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint8", - "name": "", - "type": "uint8" - } - ], - "name": "supportedTokens", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes[]", - "name": "signatures", - "type": "bytes[]" - }, - { - "components": [ - { - "internalType": "uint8", - "name": "messageType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "version", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "nonce", - "type": "uint64" - }, - { - "internalType": "uint8", - "name": "chainID", - "type": "uint8" - }, - { - "internalType": "bytes", - "name": "payload", - "type": "bytes" - } - ], - "internalType": "struct Messages.Message", - "name": "message", - "type": "tuple" - } - ], - "name": "transferTokensWithSignatures", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes[]", - "name": "signatures", - "type": "bytes[]" - }, - { - "components": [ - { - "internalType": "uint8", - "name": "messageType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "version", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "nonce", - "type": "uint64" - }, - { - "internalType": "uint8", - "name": "chainID", - "type": "uint8" - }, - { - "internalType": "bytes", - "name": "payload", - "type": "bytes" - } - ], - "internalType": "struct Messages.Message", - "name": "message", - "type": "tuple" - } - ], - "name": "upgradeBridgeWithSignatures", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "newImplementation", - "type": "address" - } - ], - "name": "upgradeTo", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "newImplementation", - "type": "address" - }, - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - } - ], - "name": "upgradeToAndCall", - "outputs": [], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [], - "name": "vault", - "outputs": [ - { - "internalType": "contract IBridgeVault", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "weth9", - "outputs": [ - { - "internalType": "contract IWETH9", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - } + { + "inputs": [ + { + "internalType": "address", + "name": "target", + "type": "address" + } + ], + "name": "AddressEmptyCode", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "ERC1967InvalidImplementation", + "type": "error" + }, + { + "inputs": [], + "name": "ERC1967NonPayable", + "type": "error" + }, + { + "inputs": [], + "name": "EnforcedPause", + "type": "error" + }, + { + "inputs": [], + "name": "ExpectedPause", + "type": "error" + }, + { + "inputs": [], + "name": "FailedInnerCall", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidInitialization", + "type": "error" + }, + { + "inputs": [], + "name": "NotInitializing", + "type": "error" + }, + { + "inputs": [], + "name": "ReentrancyGuardReentrantCall", + "type": "error" + }, + { + "inputs": [], + "name": "UUPSUnauthorizedCallContext", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "slot", + "type": "bytes32" + } + ], + "name": "UUPSUnsupportedProxiableUUID", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "version", + "type": "uint64" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint8", + "name": "sourceChainID", + "type": "uint8" + }, + { + "indexed": true, + "internalType": "uint64", + "name": "nonce", + "type": "uint64" + }, + { + "indexed": true, + "internalType": "uint8", + "name": "destinationChainID", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "tokenID", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "erc20AdjustedAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "senderAddress", + "type": "bytes" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipientAddress", + "type": "address" + } + ], + "name": "TokensClaimed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint8", + "name": "sourceChainID", + "type": "uint8" + }, + { + "indexed": true, + "internalType": "uint64", + "name": "nonce", + "type": "uint64" + }, + { + "indexed": true, + "internalType": "uint8", + "name": "destinationChainID", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "tokenID", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint64", + "name": "suiAdjustedAmount", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "address", + "name": "senderAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "recipientAddress", + "type": "bytes" + } + ], + "name": "TokensDeposited", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "UPGRADE_INTERFACE_VERSION", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "tokenID", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "recipientAddress", + "type": "bytes" + }, + { + "internalType": "uint8", + "name": "destinationChainID", + "type": "uint8" + } + ], + "name": "bridgeERC20", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "recipientAddress", + "type": "bytes" + }, + { + "internalType": "uint8", + "name": "destinationChainID", + "type": "uint8" + } + ], + "name": "bridgeETH", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "committee", + "outputs": [ + { + "internalType": "contract IBridgeCommittee", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes[]", + "name": "signatures", + "type": "bytes[]" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "messageType", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "version", + "type": "uint8" + }, + { + "internalType": "uint64", + "name": "nonce", + "type": "uint64" + }, + { + "internalType": "uint8", + "name": "chainID", + "type": "uint8" + }, + { + "internalType": "bytes", + "name": "payload", + "type": "bytes" + } + ], + "internalType": "struct BridgeUtils.Message", + "name": "message", + "type": "tuple" + } + ], + "name": "executeEmergencyOpWithSignatures", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_committee", + "type": "address" + }, + { + "internalType": "address", + "name": "_vault", + "type": "address" + }, + { + "internalType": "address", + "name": "_limiter", + "type": "address" + }, + { + "internalType": "address", + "name": "_wETH", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "nonce", + "type": "uint64" + } + ], + "name": "isTransferProcessed", + "outputs": [ + { + "internalType": "bool", + "name": "isProcessed", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "limiter", + "outputs": [ + { + "internalType": "contract IBridgeLimiter", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "messageType", + "type": "uint8" + } + ], + "name": "nonces", + "outputs": [ + { + "internalType": "uint64", + "name": "nonce", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "paused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes[]", + "name": "signatures", + "type": "bytes[]" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "messageType", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "version", + "type": "uint8" + }, + { + "internalType": "uint64", + "name": "nonce", + "type": "uint64" + }, + { + "internalType": "uint8", + "name": "chainID", + "type": "uint8" + }, + { + "internalType": "bytes", + "name": "payload", + "type": "bytes" + } + ], + "internalType": "struct BridgeUtils.Message", + "name": "message", + "type": "tuple" + } + ], + "name": "transferBridgedTokensWithSignatures", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes[]", + "name": "signatures", + "type": "bytes[]" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "messageType", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "version", + "type": "uint8" + }, + { + "internalType": "uint64", + "name": "nonce", + "type": "uint64" + }, + { + "internalType": "uint8", + "name": "chainID", + "type": "uint8" + }, + { + "internalType": "bytes", + "name": "payload", + "type": "bytes" + } + ], + "internalType": "struct BridgeUtils.Message", + "name": "message", + "type": "tuple" + } + ], + "name": "upgradeWithSignatures", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "vault", + "outputs": [ + { + "internalType": "contract IBridgeVault", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "wETH", + "outputs": [ + { + "internalType": "contract IWETH9", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + } ] diff --git a/crates/sui-bridge/abi/tests/mock_sui_bridge_v2.json b/crates/sui-bridge/abi/tests/mock_sui_bridge_v2.json new file mode 100644 index 0000000000000..c96e9d58fc17b --- /dev/null +++ b/crates/sui-bridge/abi/tests/mock_sui_bridge_v2.json @@ -0,0 +1,692 @@ +[ + { + "type": "function", + "name": "UPGRADE_INTERFACE_VERSION", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "bridgeERC20", + "inputs": [ + { + "name": "tokenID", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "recipientAddress", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "destinationChainID", + "type": "uint8", + "internalType": "uint8" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "bridgeETH", + "inputs": [ + { + "name": "recipientAddress", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "destinationChainID", + "type": "uint8", + "internalType": "uint8" + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "committee", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract IBridgeCommittee" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "executeEmergencyOpWithSignatures", + "inputs": [ + { + "name": "signatures", + "type": "bytes[]", + "internalType": "bytes[]" + }, + { + "name": "message", + "type": "tuple", + "internalType": "struct BridgeUtils.Message", + "components": [ + { + "name": "messageType", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "version", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "nonce", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "chainID", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "payload", + "type": "bytes", + "internalType": "bytes" + } + ] + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "initialize", + "inputs": [ + { + "name": "_committee", + "type": "address", + "internalType": "address" + }, + { + "name": "_vault", + "type": "address", + "internalType": "address" + }, + { + "name": "_limiter", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "initializeV2", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "initializeV2Params", + "inputs": [ + { + "name": "value", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "_override", + "type": "bool", + "internalType": "bool" + }, + { + "name": "_event", + "type": "string", + "internalType": "string" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "isPausing", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "isTransferProcessed", + "inputs": [ + { + "name": "nonce", + "type": "uint64", + "internalType": "uint64" + } + ], + "outputs": [ + { + "name": "isProcessed", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "limiter", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract IBridgeLimiter" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "mock", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint8", + "internalType": "uint8" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "newMockFunction", + "inputs": [ + { + "name": "_pausing", + "type": "bool", + "internalType": "bool" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "newMockFunction", + "inputs": [ + { + "name": "_pausing", + "type": "bool", + "internalType": "bool" + }, + { + "name": "_mock", + "type": "uint8", + "internalType": "uint8" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "nonces", + "inputs": [ + { + "name": "messageType", + "type": "uint8", + "internalType": "uint8" + } + ], + "outputs": [ + { + "name": "nonce", + "type": "uint64", + "internalType": "uint64" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "paused", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "proxiableUUID", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "testSkip", + "inputs": [], + "outputs": [], + "stateMutability": "view" + }, + { + "type": "function", + "name": "transferBridgedTokensWithSignatures", + "inputs": [ + { + "name": "signatures", + "type": "bytes[]", + "internalType": "bytes[]" + }, + { + "name": "message", + "type": "tuple", + "internalType": "struct BridgeUtils.Message", + "components": [ + { + "name": "messageType", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "version", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "nonce", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "chainID", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "payload", + "type": "bytes", + "internalType": "bytes" + } + ] + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "upgradeToAndCall", + "inputs": [ + { + "name": "newImplementation", + "type": "address", + "internalType": "address" + }, + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "upgradeWithSignatures", + "inputs": [ + { + "name": "signatures", + "type": "bytes[]", + "internalType": "bytes[]" + }, + { + "name": "message", + "type": "tuple", + "internalType": "struct BridgeUtils.Message", + "components": [ + { + "name": "messageType", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "version", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "nonce", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "chainID", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "payload", + "type": "bytes", + "internalType": "bytes" + } + ] + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "vault", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract IBridgeVault" + } + ], + "stateMutability": "view" + }, + { + "type": "event", + "name": "Initialized", + "inputs": [ + { + "name": "version", + "type": "uint64", + "indexed": false, + "internalType": "uint64" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "MockEvent", + "inputs": [ + { + "name": "_event", + "type": "string", + "indexed": false, + "internalType": "string" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Paused", + "inputs": [ + { + "name": "account", + "type": "address", + "indexed": false, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "TokensClaimed", + "inputs": [ + { + "name": "sourceChainID", + "type": "uint8", + "indexed": true, + "internalType": "uint8" + }, + { + "name": "nonce", + "type": "uint64", + "indexed": true, + "internalType": "uint64" + }, + { + "name": "destinationChainID", + "type": "uint8", + "indexed": true, + "internalType": "uint8" + }, + { + "name": "tokenID", + "type": "uint8", + "indexed": false, + "internalType": "uint8" + }, + { + "name": "erc20AdjustedAmount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "senderAddress", + "type": "bytes", + "indexed": false, + "internalType": "bytes" + }, + { + "name": "recipientAddress", + "type": "address", + "indexed": false, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "TokensDeposited", + "inputs": [ + { + "name": "sourceChainID", + "type": "uint8", + "indexed": true, + "internalType": "uint8" + }, + { + "name": "nonce", + "type": "uint64", + "indexed": true, + "internalType": "uint64" + }, + { + "name": "destinationChainID", + "type": "uint8", + "indexed": true, + "internalType": "uint8" + }, + { + "name": "tokenID", + "type": "uint8", + "indexed": false, + "internalType": "uint8" + }, + { + "name": "suiAdjustedAmount", + "type": "uint64", + "indexed": false, + "internalType": "uint64" + }, + { + "name": "senderAddress", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "recipientAddress", + "type": "bytes", + "indexed": false, + "internalType": "bytes" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Unpaused", + "inputs": [ + { + "name": "account", + "type": "address", + "indexed": false, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Upgraded", + "inputs": [ + { + "name": "implementation", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "AddressEmptyCode", + "inputs": [ + { + "name": "target", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "AddressInsufficientBalance", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ERC1967InvalidImplementation", + "inputs": [ + { + "name": "implementation", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ERC1967NonPayable", + "inputs": [] + }, + { + "type": "error", + "name": "EnforcedPause", + "inputs": [] + }, + { + "type": "error", + "name": "ExpectedPause", + "inputs": [] + }, + { + "type": "error", + "name": "FailedInnerCall", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidInitialization", + "inputs": [] + }, + { + "type": "error", + "name": "NotInitializing", + "inputs": [] + }, + { + "type": "error", + "name": "ReentrancyGuardReentrantCall", + "inputs": [] + }, + { + "type": "error", + "name": "SafeERC20FailedOperation", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "UUPSUnauthorizedCallContext", + "inputs": [] + }, + { + "type": "error", + "name": "UUPSUnsupportedProxiableUUID", + "inputs": [ + { + "name": "slot", + "type": "bytes32", + "internalType": "bytes32" + } + ] + } +] diff --git a/crates/sui-bridge/build.rs b/crates/sui-bridge/build.rs index d0b0d41262c4e..d674701fe1ced 100644 --- a/crates/sui-bridge/build.rs +++ b/crates/sui-bridge/build.rs @@ -23,8 +23,13 @@ fn main() -> Result<(), ExitStatus> { .output() .map(|output| output.status.success()) .unwrap_or(false); - if !forge_installed { - eprintln!("Installing forge"); + let anvil_installed = Command::new("which") + .arg("anvil") + .output() + .map(|output| output.status.success()) + .unwrap_or(false); + if !forge_installed || !anvil_installed { + eprintln!("Installing forge and/or anvil"); // Also print the path where foundryup is installed let install_cmd = "curl -L https://foundry.paradigm.xyz | { cat; echo 'echo foundryup-path=\"$FOUNDRY_BIN_DIR/foundryup\"'; } | bash"; @@ -32,7 +37,7 @@ fn main() -> Result<(), ExitStatus> { .arg("-c") .arg(install_cmd) .output() - .expect("Failed to install Forge"); + .expect("Failed to fetch foundryup"); // extract foundryup path let output_str = String::from_utf8_lossy(&output.stdout); @@ -44,7 +49,7 @@ fn main() -> Result<(), ExitStatus> { } } if foundryup_path.is_none() { - eprintln!("Error installing forge: expect a foundry path in output"); + eprintln!("Error installing forge/anvil: expect a foundry path in output"); exit(1); } let foundryup_path = foundryup_path.unwrap(); diff --git a/crates/sui-bridge/src/abi.rs b/crates/sui-bridge/src/abi.rs index f6801c84963bf..cd2733222cb0a 100644 --- a/crates/sui-bridge/src/abi.rs +++ b/crates/sui-bridge/src/abi.rs @@ -1,9 +1,21 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 +use crate::encoding::{ + BridgeMessageEncoding, ADD_TOKENS_ON_EVM_MESSAGE_VERSION, ASSET_PRICE_UPDATE_MESSAGE_VERSION, + EVM_CONTRACT_UPGRADE_MESSAGE_VERSION, LIMIT_UPDATE_MESSAGE_VERSION, +}; +use crate::encoding::{ + COMMITTEE_BLOCKLIST_MESSAGE_VERSION, EMERGENCY_BUTTON_MESSAGE_VERSION, + TOKEN_TRANSFER_MESSAGE_VERSION, +}; use crate::error::{BridgeError, BridgeResult}; -use crate::types::{BridgeAction, EthToSuiBridgeAction}; -use crate::types::{BridgeChainId, EthLog, TokenId}; +use crate::types::{ + AddTokensOnEvmAction, AssetPriceUpdateAction, BlocklistCommitteeAction, BridgeAction, + BridgeActionType, EmergencyAction, EthLog, EthToSuiBridgeAction, EvmContractUpgradeAction, + LimitUpdateAction, SuiToEthBridgeAction, +}; +use ethers::types::Log; use ethers::{ abi::RawLog, contract::{abigen, EthLogDecode}, @@ -11,6 +23,7 @@ use ethers::{ }; use serde::{Deserialize, Serialize}; use sui_types::base_types::SuiAddress; +use sui_types::bridge::BridgeChainId; // TODO: write a macro to handle variants @@ -26,6 +39,36 @@ abigen!( event_derives(serde::Deserialize, serde::Serialize) ); +abigen!( + EthBridgeCommittee, + "abi/bridge_committee.json", + event_derives(serde::Deserialize, serde::Serialize) +); + +abigen!( + EthBridgeVault, + "abi/bridge_vault.json", + event_derives(serde::Deserialize, serde::Serialize) +); + +abigen!( + EthBridgeLimiter, + "abi/bridge_limiter.json", + event_derives(serde::Deserialize, serde::Serialize) +); + +abigen!( + EthBridgeConfig, + "abi/bridge_config.json", + event_derives(serde::Deserialize, serde::Serialize) +); + +abigen!( + EthCommitteeUpgradeableContract, + "abi/bridge_committee_upgradeable.json", + event_derives(serde::Deserialize, serde::Serialize) +); + impl EthBridgeEvent { pub fn try_from_eth_log(log: &EthLog) -> Option { let raw_log = RawLog { @@ -40,6 +83,20 @@ impl EthBridgeEvent { // TODO: try other variants None } + + pub fn try_from_log(log: &Log) -> Option { + let raw_log = RawLog { + topics: log.topics.clone(), + data: log.data.to_vec(), + }; + + if let Ok(decoded) = EthSuiBridgeEvents::decode_log(&raw_log) { + return Some(EthBridgeEvent::EthSuiBridgeEvents(decoded)); + } + + // TODO: try other variants + None + } } impl EthBridgeEvent { @@ -51,7 +108,7 @@ impl EthBridgeEvent { match self { EthBridgeEvent::EthSuiBridgeEvents(event) => { match event { - EthSuiBridgeEvents::TokensBridgedToSuiFilter(event) => { + EthSuiBridgeEvents::TokensDepositedFilter(event) => { let bridge_event = match EthToSuiTokenBridgeV1::try_from(&event) { Ok(bridge_event) => bridge_event, // This only happens when solidity code does not align with rust code. @@ -59,7 +116,7 @@ impl EthBridgeEvent { // We log error here. // TODO: add metrics and alert Err(e) => { - tracing::error!(?eth_tx_hash, eth_event_index, "Failed to convert TokensBridgedToSui log to EthToSuiTokenBridgeV1. This indicates incorrect parameters or a bug in the code: {:?}. Err: {:?}", event, e); + tracing::error!(?eth_tx_hash, eth_event_index, "Failed to convert TokensDepositedFilter log to EthToSuiTokenBridgeV1. This indicates incorrect parameters or a bug in the code: {:?}. Err: {:?}", event, e); return None; } }; @@ -70,14 +127,19 @@ impl EthBridgeEvent { eth_bridge_event: bridge_event, })) } - _ => None, + EthSuiBridgeEvents::TokensClaimedFilter(_event) => None, + EthSuiBridgeEvents::PausedFilter(_event) => None, + EthSuiBridgeEvents::UnpausedFilter(_event) => None, + EthSuiBridgeEvents::UpgradedFilter(_event) => None, + EthSuiBridgeEvents::InitializedFilter(_event) => None, } } } } } -// Sanity checked version of TokensBridgedToSuiFilter +/// The event emitted when tokens are deposited into the bridge on Ethereum. +/// Sanity checked version of TokensDepositedFilter #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Hash)] pub struct EthToSuiTokenBridgeV1 { pub nonce: u64, @@ -85,21 +147,272 @@ pub struct EthToSuiTokenBridgeV1 { pub eth_chain_id: BridgeChainId, pub sui_address: SuiAddress, pub eth_address: EthAddress, - pub token_id: TokenId, - pub amount: u64, + pub token_id: u8, + pub sui_adjusted_amount: u64, } -impl TryFrom<&TokensBridgedToSuiFilter> for EthToSuiTokenBridgeV1 { +impl TryFrom<&TokensDepositedFilter> for EthToSuiTokenBridgeV1 { type Error = BridgeError; - fn try_from(event: &TokensBridgedToSuiFilter) -> BridgeResult { + fn try_from(event: &TokensDepositedFilter) -> BridgeResult { Ok(Self { nonce: event.nonce, sui_chain_id: BridgeChainId::try_from(event.destination_chain_id)?, eth_chain_id: BridgeChainId::try_from(event.source_chain_id)?, - sui_address: SuiAddress::from_bytes(event.target_address.as_ref())?, - eth_address: event.source_address, - token_id: TokenId::try_from(event.token_code)?, - amount: event.sui_adjusted_amount, + sui_address: SuiAddress::from_bytes(event.recipient_address.as_ref())?, + eth_address: event.sender_address, + token_id: event.token_id, + sui_adjusted_amount: event.sui_adjusted_amount, }) } } + +//////////////////////////////////////////////////////////////////////// +// Eth Message Conversion // +//////////////////////////////////////////////////////////////////////// + +impl From for eth_sui_bridge::Message { + fn from(action: SuiToEthBridgeAction) -> Self { + eth_sui_bridge::Message { + message_type: BridgeActionType::TokenTransfer as u8, + version: TOKEN_TRANSFER_MESSAGE_VERSION, + nonce: action.sui_bridge_event.nonce, + chain_id: action.sui_bridge_event.sui_chain_id as u8, + payload: action.as_payload_bytes().into(), + } + } +} + +impl From for eth_sui_bridge::Message { + fn from(action: EmergencyAction) -> Self { + eth_sui_bridge::Message { + message_type: BridgeActionType::EmergencyButton as u8, + version: EMERGENCY_BUTTON_MESSAGE_VERSION, + nonce: action.nonce, + chain_id: action.chain_id as u8, + payload: action.as_payload_bytes().into(), + } + } +} + +impl From for eth_bridge_committee::Message { + fn from(action: BlocklistCommitteeAction) -> Self { + eth_bridge_committee::Message { + message_type: BridgeActionType::UpdateCommitteeBlocklist as u8, + version: COMMITTEE_BLOCKLIST_MESSAGE_VERSION, + nonce: action.nonce, + chain_id: action.chain_id as u8, + payload: action.as_payload_bytes().into(), + } + } +} + +impl From for eth_bridge_limiter::Message { + fn from(action: LimitUpdateAction) -> Self { + eth_bridge_limiter::Message { + message_type: BridgeActionType::LimitUpdate as u8, + version: LIMIT_UPDATE_MESSAGE_VERSION, + nonce: action.nonce, + chain_id: action.chain_id as u8, + payload: action.as_payload_bytes().into(), + } + } +} + +impl From for eth_bridge_config::Message { + fn from(action: AssetPriceUpdateAction) -> Self { + eth_bridge_config::Message { + message_type: BridgeActionType::AssetPriceUpdate as u8, + version: ASSET_PRICE_UPDATE_MESSAGE_VERSION, + nonce: action.nonce, + chain_id: action.chain_id as u8, + payload: action.as_payload_bytes().into(), + } + } +} + +impl From for eth_bridge_config::Message { + fn from(action: AddTokensOnEvmAction) -> Self { + eth_bridge_config::Message { + message_type: BridgeActionType::AddTokensOnEvm as u8, + version: ADD_TOKENS_ON_EVM_MESSAGE_VERSION, + nonce: action.nonce, + chain_id: action.chain_id as u8, + payload: action.as_payload_bytes().into(), + } + } +} + +impl From for eth_committee_upgradeable_contract::Message { + fn from(action: EvmContractUpgradeAction) -> Self { + eth_committee_upgradeable_contract::Message { + message_type: BridgeActionType::EvmContractUpgrade as u8, + version: EVM_CONTRACT_UPGRADE_MESSAGE_VERSION, + nonce: action.nonce, + chain_id: action.chain_id as u8, + payload: action.as_payload_bytes().into(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + crypto::BridgeAuthorityPublicKeyBytes, + types::{BlocklistType, EmergencyActionType}, + }; + use fastcrypto::encoding::{Encoding, Hex}; + use sui_types::{bridge::TOKEN_ID_ETH, crypto::ToFromBytes}; + + #[test] + fn test_eth_message_conversion_emergency_action_regression() -> anyhow::Result<()> { + telemetry_subscribers::init_for_testing(); + + let action = EmergencyAction { + nonce: 2, + chain_id: BridgeChainId::EthSepolia, + action_type: EmergencyActionType::Pause, + }; + let message: eth_sui_bridge::Message = action.into(); + assert_eq!( + message, + eth_sui_bridge::Message { + message_type: BridgeActionType::EmergencyButton as u8, + version: EMERGENCY_BUTTON_MESSAGE_VERSION, + nonce: 2, + chain_id: BridgeChainId::EthSepolia as u8, + payload: vec![0].into(), + } + ); + Ok(()) + } + + #[test] + fn test_eth_message_conversion_update_blocklist_action_regression() -> anyhow::Result<()> { + telemetry_subscribers::init_for_testing(); + let pub_key_bytes = BridgeAuthorityPublicKeyBytes::from_bytes( + &Hex::decode("02321ede33d2c2d7a8a152f275a1484edef2098f034121a602cb7d767d38680aa4") + .unwrap(), + ) + .unwrap(); + let action = BlocklistCommitteeAction { + nonce: 0, + chain_id: BridgeChainId::EthSepolia, + blocklist_type: BlocklistType::Blocklist, + blocklisted_members: vec![pub_key_bytes], + }; + let message: eth_bridge_committee::Message = action.into(); + assert_eq!( + message, + eth_bridge_committee::Message { + message_type: BridgeActionType::UpdateCommitteeBlocklist as u8, + version: COMMITTEE_BLOCKLIST_MESSAGE_VERSION, + nonce: 0, + chain_id: BridgeChainId::EthSepolia as u8, + payload: Hex::decode("000168b43fd906c0b8f024a18c56e06744f7c6157c65") + .unwrap() + .into(), + } + ); + Ok(()) + } + + #[test] + fn test_eth_message_conversion_update_limit_action_regression() -> anyhow::Result<()> { + telemetry_subscribers::init_for_testing(); + let action = LimitUpdateAction { + nonce: 2, + chain_id: BridgeChainId::EthSepolia, + sending_chain_id: BridgeChainId::SuiTestnet, + new_usd_limit: 4200000, + }; + let message: eth_bridge_limiter::Message = action.into(); + assert_eq!( + message, + eth_bridge_limiter::Message { + message_type: BridgeActionType::LimitUpdate as u8, + version: LIMIT_UPDATE_MESSAGE_VERSION, + nonce: 2, + chain_id: BridgeChainId::EthSepolia as u8, + payload: Hex::decode("010000000000401640").unwrap().into(), + } + ); + Ok(()) + } + + #[test] + fn test_eth_message_conversion_contract_upgrade_action_regression() -> anyhow::Result<()> { + telemetry_subscribers::init_for_testing(); + let action = EvmContractUpgradeAction { + nonce: 2, + chain_id: BridgeChainId::EthSepolia, + proxy_address: EthAddress::repeat_byte(1), + new_impl_address: EthAddress::repeat_byte(2), + call_data: Vec::from("deadbeef"), + }; + let message: eth_committee_upgradeable_contract::Message = action.into(); + assert_eq!( + message, + eth_committee_upgradeable_contract::Message { + message_type: BridgeActionType::EvmContractUpgrade as u8, + version: EVM_CONTRACT_UPGRADE_MESSAGE_VERSION, + nonce: 2, + chain_id: BridgeChainId::EthSepolia as u8, + payload: Hex::decode("0x00000000000000000000000001010101010101010101010101010101010101010000000000000000000000000202020202020202020202020202020202020202000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000086465616462656566000000000000000000000000000000000000000000000000").unwrap().into(), + } + ); + Ok(()) + } + + #[test] + fn test_eth_message_conversion_update_price_action_regression() -> anyhow::Result<()> { + telemetry_subscribers::init_for_testing(); + let action = AssetPriceUpdateAction { + nonce: 2, + chain_id: BridgeChainId::EthSepolia, + token_id: TOKEN_ID_ETH, + new_usd_price: 80000000, + }; + let message: eth_bridge_config::Message = action.into(); + assert_eq!( + message, + eth_bridge_config::Message { + message_type: BridgeActionType::AssetPriceUpdate as u8, + version: ASSET_PRICE_UPDATE_MESSAGE_VERSION, + nonce: 2, + chain_id: BridgeChainId::EthSepolia as u8, + payload: Hex::decode("020000000004c4b400").unwrap().into(), + } + ); + Ok(()) + } + + #[test] + fn test_eth_message_conversion_add_tokens_on_evm_action_regression() -> anyhow::Result<()> { + let action = AddTokensOnEvmAction { + nonce: 5, + chain_id: BridgeChainId::EthCustom, + native: true, + token_ids: vec![99, 100, 101], + token_addresses: vec![ + EthAddress::repeat_byte(1), + EthAddress::repeat_byte(2), + EthAddress::repeat_byte(3), + ], + token_sui_decimals: vec![5, 6, 7], + token_prices: vec![1_000_000_000, 2_000_000_000, 3_000_000_000], + }; + let message: eth_bridge_config::Message = action.into(); + assert_eq!( + message, + eth_bridge_config::Message { + message_type: BridgeActionType::AddTokensOnEvm as u8, + version: ADD_TOKENS_ON_EVM_MESSAGE_VERSION, + nonce: 5, + chain_id: BridgeChainId::EthCustom as u8, + payload: Hex::decode("0103636465030101010101010101010101010101010101010101020202020202020202020202020202020202020203030303030303030303030303030303030303030305060703000000003b9aca00000000007735940000000000b2d05e00").unwrap().into(), + } + ); + Ok(()) + } +} diff --git a/crates/sui-bridge/src/action_executor.rs b/crates/sui-bridge/src/action_executor.rs index 03d0042677774..c5f3cdc73e8df 100644 --- a/crates/sui-bridge/src/action_executor.rs +++ b/crates/sui-bridge/src/action_executor.rs @@ -7,11 +7,11 @@ use mysten_metrics::spawn_logged_monitored_task; use shared_crypto::intent::{Intent, IntentMessage}; use sui_json_rpc_types::{ - SuiExecutionStatus, SuiTransactionBlockEffects, SuiTransactionBlockEffectsAPI, + SuiExecutionStatus, SuiTransactionBlockEffectsAPI, SuiTransactionBlockResponse, }; +use sui_types::transaction::ObjectArg; use sui_types::{ base_types::{ObjectID, ObjectRef, SuiAddress}, - committee::VALIDITY_THRESHOLD, crypto::{Signature, SuiKeyPair}, digests::TransactionDigest, gas_coin::GasCoin, @@ -19,12 +19,16 @@ use sui_types::{ transaction::Transaction, }; +use crate::events::{ + TokenTransferAlreadyApproved, TokenTransferAlreadyClaimed, TokenTransferApproved, + TokenTransferClaimed, +}; use crate::{ client::bridge_authority_aggregator::BridgeAuthorityAggregator, error::BridgeError, storage::BridgeOrchestratorTables, sui_client::{SuiClient, SuiClientInner}, - sui_transaction_builder::build_transaction, + sui_transaction_builder::build_sui_transaction, types::{BridgeAction, BridgeActionStatus, VerifiedCertifiedBridgeAction}, }; use std::sync::Arc; @@ -64,6 +68,7 @@ pub struct BridgeActionExecutor { sui_address: SuiAddress, gas_object_id: ObjectID, store: Arc, + bridge_object_arg: ObjectArg, } impl BridgeActionExecutorTrait for BridgeActionExecutor @@ -85,7 +90,7 @@ impl BridgeActionExecutor where C: SuiClientInner + 'static, { - pub fn new( + pub async fn new( sui_client: Arc>, bridge_auth_agg: Arc, store: Arc, @@ -93,6 +98,9 @@ where sui_address: SuiAddress, gas_object_id: ObjectID, ) -> Self { + let bridge_object_arg = sui_client + .get_mutable_bridge_object_arg_must_succeed() + .await; Self { sui_client, bridge_auth_agg, @@ -100,6 +108,7 @@ where key, gas_object_id, sui_address, + bridge_object_arg, } } @@ -153,6 +162,7 @@ where self.store.clone(), execution_tx_clone, execution_rx, + self.bridge_object_arg, ) )); (tasks, sender, execution_tx) @@ -198,7 +208,10 @@ where store: &Arc, ) -> bool { let status = sui_client - .get_token_transfer_action_onchain_status_until_success(action) + .get_token_transfer_action_onchain_status_until_success( + action.chain_id() as u8, + action.seq_number(), + ) .await; match status { BridgeActionStatus::Approved | BridgeActionStatus::Claimed => { @@ -214,8 +227,8 @@ where true } // Although theoretically a legit SuiToEthBridgeAction should not have - // status `RecordNotFound` - BridgeActionStatus::Pending | BridgeActionStatus::RecordNotFound => false, + // status `NotFound` + BridgeActionStatus::Pending | BridgeActionStatus::NotFound => false, } } @@ -243,10 +256,9 @@ where { return; } - - // TODO: use different threshold based on action types. + let threshold = action.approval_threshold(); match auth_agg - .request_committee_signatures(action.clone(), VALIDITY_THRESHOLD) + .request_committee_signatures(action.clone(), threshold) .await { Ok(certificate) => { @@ -285,8 +297,12 @@ where mut execution_queue_receiver: mysten_metrics::metered_channel::Receiver< CertifiedBridgeActionExecutionWrapper, >, + bridge_object_arg: ObjectArg, ) { info!("Starting run_onchain_execution_loop"); + // Get token id maps, this must succeed to continue. + let sui_token_type_tags = sui_client.get_token_id_map().await.unwrap(); + while let Some(certificate_wrapper) = execution_queue_receiver.recv().await { info!( "Received certified action for execution: {:?}", @@ -311,7 +327,14 @@ where let (_gas_coin, gas_object_ref) = Self::get_gas_data_assert_ownership(sui_address, gas_object_id, &sui_client).await; let ceriticate_clone = certificate.clone(); - let tx_data = match build_transaction(sui_address, &gas_object_ref, ceriticate_clone) { + info!("Building Sui transaction for action: {:?}", action); + let tx_data = match build_sui_transaction( + sui_address, + &gas_object_ref, + ceriticate_clone, + bridge_object_arg, + &sui_token_type_tags, + ) { Ok(tx_data) => tx_data, Err(err) => { // TODO: add mertrics @@ -337,10 +360,7 @@ where .execute_transaction_block_with_effects(signed_tx) .await { - Ok(effects) => { - let effects = effects.effects.expect("We requested effects but got None."); - Self::handle_execution_effects(tx_digest, effects, &store, action).await - } + Ok(resp) => Self::handle_execution_effects(tx_digest, resp, &store, action).await, // If the transaction did not go through, retry up to a certain times. Err(err) => { @@ -373,13 +393,30 @@ where // TODO: do we need a mechanism to periodically read pending actions from DB? async fn handle_execution_effects( tx_digest: TransactionDigest, - effects: SuiTransactionBlockEffects, + response: SuiTransactionBlockResponse, store: &Arc, action: &BridgeAction, ) { + let effects = response + .effects + .clone() + .expect("We requested effects but got None."); let status = effects.status(); match status { SuiExecutionStatus::Success => { + let events = response.events.expect("We requested events but got None."); + // If the transaction is successful, there must be either + // TokenTransferAlreadyClaimed or TokenTransferClaimed event. + assert!(events + .data + .iter() + .any(|e| e.type_ == *TokenTransferAlreadyClaimed.get().unwrap() + || e.type_ == *TokenTransferClaimed.get().unwrap() + || e.type_ == *TokenTransferApproved.get().unwrap() + || e.type_ == *TokenTransferAlreadyApproved.get().unwrap()), + "Expected TokenTransferAlreadyClaimed, TokenTransferClaimed, TokenTransferApproved or TokenTransferAlreadyApproved event but got: {:?}", + events, + ); info!(?tx_digest, "Sui transaction executed successfully"); store .remove_pending_actions(&[action.digest()]) @@ -434,13 +471,19 @@ pub async fn submit_to_executor( #[cfg(test)] mod tests { - use std::collections::BTreeMap; - + use crate::events::init_all_struct_tags; + use crate::test_utils::DUMMY_MUTALBE_BRIDGE_OBJECT_ARG; use fastcrypto::traits::KeyPair; use prometheus::Registry; - use sui_json_rpc_types::SuiTransactionBlockResponse; + use std::collections::{BTreeMap, HashMap}; + use std::str::FromStr; + use sui_json_rpc_types::SuiTransactionBlockEffects; + use sui_json_rpc_types::SuiTransactionBlockEvents; + use sui_json_rpc_types::{SuiEvent, SuiTransactionBlockResponse}; + use sui_types::bridge::{TOKEN_ID_BTC, TOKEN_ID_ETH, TOKEN_ID_USDC, TOKEN_ID_USDT}; use sui_types::crypto::get_key_pair; use sui_types::gas_coin::GasCoin; + use sui_types::TypeTag; use sui_types::{base_types::random_object_ref, transaction::TransactionData}; use crate::{ @@ -476,11 +519,8 @@ mod tests { _handles, gas_object_ref, sui_address, - ) = setup(); - - // TODO: remove once we don't rely on env var to get object id - std::env::set_var("ROOT_BRIDGE_OBJECT_ID", "0x09"); - std::env::set_var("ROOT_BRIDGE_OBJECT_INITIAL_SHARED_VERSION", "1"); + id_token_map, + ) = setup().await; let (action_certificate, _, _) = get_bridge_authority_approved_action( vec![&mock0, &mock1, &mock2, &mock3], @@ -488,7 +528,14 @@ mod tests { ); let action = action_certificate.data().clone(); - let tx_data = build_transaction(sui_address, &gas_object_ref, action_certificate).unwrap(); + let tx_data = build_sui_transaction( + sui_address, + &gas_object_ref, + action_certificate, + DUMMY_MUTALBE_BRIDGE_OBJECT_ARG, + &id_token_map, + ) + .unwrap(); let tx_digest = get_tx_digest(tx_data, &dummy_sui_key); @@ -500,16 +547,20 @@ mod tests { ); // Mock the transaction to be successfully executed + let mut event = SuiEvent::random_for_testing(); + event.type_ = TokenTransferClaimed.get().unwrap().clone(); + let events = vec![event]; mock_transaction_response( &sui_client_mock, tx_digest, SuiExecutionStatus::Success, + Some(events), true, ); store.insert_pending_actions(&[action.clone()]).unwrap(); assert_eq!( - store.get_all_pending_actions().unwrap()[&action.digest()], + store.get_all_pending_actions()[&action.digest()], action.clone() ); @@ -520,7 +571,7 @@ mod tests { // Expect to see the transaction to be requested and successfully executed hence removed from WAL tx_subscription.recv().await.unwrap(); - assert!(store.get_all_pending_actions().unwrap().is_empty()); + assert!(store.get_all_pending_actions().is_empty()); ///////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////// Test execution failure /////////////////////////////////// @@ -533,7 +584,14 @@ mod tests { let action = action_certificate.data().clone(); - let tx_data = build_transaction(sui_address, &gas_object_ref, action_certificate).unwrap(); + let tx_data = build_sui_transaction( + sui_address, + &gas_object_ref, + action_certificate, + DUMMY_MUTALBE_BRIDGE_OBJECT_ARG, + &id_token_map, + ) + .unwrap(); let tx_digest = get_tx_digest(tx_data, &dummy_sui_key); // Mock the transaction to fail @@ -543,12 +601,13 @@ mod tests { SuiExecutionStatus::Failure { error: "failure is mother of success".to_string(), }, + None, true, ); store.insert_pending_actions(&[action.clone()]).unwrap(); assert_eq!( - store.get_all_pending_actions().unwrap()[&action.digest()], + store.get_all_pending_actions()[&action.digest()], action.clone() ); @@ -561,7 +620,7 @@ mod tests { tx_subscription.recv().await.unwrap(); // The action is not removed from WAL because the transaction failed assert_eq!( - store.get_all_pending_actions().unwrap()[&action.digest()], + store.get_all_pending_actions()[&action.digest()], action.clone() ); @@ -576,7 +635,14 @@ mod tests { let action = action_certificate.data().clone(); - let tx_data = build_transaction(sui_address, &gas_object_ref, action_certificate).unwrap(); + let tx_data = build_sui_transaction( + sui_address, + &gas_object_ref, + action_certificate, + DUMMY_MUTALBE_BRIDGE_OBJECT_ARG, + &id_token_map, + ) + .unwrap(); let tx_digest = get_tx_digest(tx_data, &dummy_sui_key); mock_transaction_error( &sui_client_mock, @@ -587,7 +653,7 @@ mod tests { store.insert_pending_actions(&[action.clone()]).unwrap(); assert_eq!( - store.get_all_pending_actions().unwrap()[&action.digest()], + store.get_all_pending_actions()[&action.digest()], action.clone() ); @@ -603,14 +669,17 @@ mod tests { // The retry is still going on, action still in WAL assert!(store .get_all_pending_actions() - .unwrap() .contains_key(&action.digest())); // Now let it succeed + let mut event = SuiEvent::random_for_testing(); + event.type_ = TokenTransferClaimed.get().unwrap().clone(); + let events = vec![event]; mock_transaction_response( &sui_client_mock, tx_digest, SuiExecutionStatus::Success, + Some(events), true, ); @@ -619,7 +688,6 @@ mod tests { // The action is successful and should be removed from WAL now assert!(!store .get_all_pending_actions() - .unwrap() .contains_key(&action.digest())); } @@ -640,11 +708,8 @@ mod tests { _handles, gas_object_ref, sui_address, - ) = setup(); - - // TODO: remove once we don't rely on env var to get object id - std::env::set_var("ROOT_BRIDGE_OBJECT_ID", "0x09"); - std::env::set_var("ROOT_BRIDGE_OBJECT_INITIAL_SHARED_VERSION", "1"); + id_token_map, + ) = setup().await; let (action_certificate, sui_tx_digest, sui_tx_event_index) = get_bridge_authority_approved_action( @@ -674,7 +739,7 @@ mod tests { store.insert_pending_actions(&[action.clone()]).unwrap(); assert_eq!( - store.get_all_pending_actions().unwrap()[&action.digest()], + store.get_all_pending_actions()[&action.digest()], action.clone() ); @@ -699,7 +764,7 @@ mod tests { ); // Still in WAL assert_eq!( - store.get_all_pending_actions().unwrap()[&action.digest()], + store.get_all_pending_actions()[&action.digest()], action.clone() ); @@ -717,14 +782,24 @@ mod tests { BridgeCommitteeValiditySignInfo { signatures: sigs }, ); let action_certificate = VerifiedCertifiedBridgeAction::new_from_verified(certified_action); - - let tx_data = build_transaction(sui_address, &gas_object_ref, action_certificate).unwrap(); + let tx_data = build_sui_transaction( + sui_address, + &gas_object_ref, + action_certificate, + DUMMY_MUTALBE_BRIDGE_OBJECT_ARG, + &id_token_map, + ) + .unwrap(); let tx_digest = get_tx_digest(tx_data, &dummy_sui_key); + let mut event = SuiEvent::random_for_testing(); + event.type_ = TokenTransferClaimed.get().unwrap().clone(); + let events = vec![event]; mock_transaction_response( &sui_client_mock, tx_digest, SuiExecutionStatus::Success, + Some(events), true, ); @@ -733,7 +808,6 @@ mod tests { // The action is removed from WAL assert!(!store .get_all_pending_actions() - .unwrap() .contains_key(&action.digest())); } @@ -754,11 +828,8 @@ mod tests { _handles, _gas_object_ref, _sui_address, - ) = setup(); - - // TODO: remove once we don't rely on env var to get object id - std::env::set_var("ROOT_BRIDGE_OBJECT_ID", "0x09"); - std::env::set_var("ROOT_BRIDGE_OBJECT_INITIAL_SHARED_VERSION", "1"); + _id_token_map, + ) = setup().await; let sui_tx_digest = TransactionDigest::random(); let sui_tx_event_index = 0; @@ -767,6 +838,9 @@ mod tests { Some(sui_tx_event_index), None, None, + None, + None, + None, ); mock_bridge_authority_signing_errors( vec![&mock0, &mock1, &mock2, &mock3], @@ -775,7 +849,7 @@ mod tests { ); store.insert_pending_actions(&[action.clone()]).unwrap(); assert_eq!( - store.get_all_pending_actions().unwrap()[&action.digest()], + store.get_all_pending_actions()[&action.digest()], action.clone() ); @@ -789,20 +863,13 @@ mod tests { tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; tx_subscription.try_recv().unwrap_err(); // And the action is still in WAL - assert!(store - .get_all_pending_actions() - .unwrap() - .contains_key(&action_digest)); + assert!(store.get_all_pending_actions().contains_key(&action_digest)); sui_client_mock.set_action_onchain_status(&action, BridgeActionStatus::Approved); // The next retry will see the action is already processed on chain and remove it from WAL let now = std::time::Instant::now(); - while store - .get_all_pending_actions() - .unwrap() - .contains_key(&action_digest) - { + while store.get_all_pending_actions().contains_key(&action_digest) { if now.elapsed().as_secs() > 10 { panic!("Timeout waiting for action to be removed from WAL"); } @@ -828,11 +895,8 @@ mod tests { _handles, gas_object_ref, sui_address, - ) = setup(); - - // TODO: remove once we don't rely on env var to get object id - std::env::set_var("ROOT_BRIDGE_OBJECT_ID", "0x09"); - std::env::set_var("ROOT_BRIDGE_OBJECT_INITIAL_SHARED_VERSION", "1"); + id_token_map, + ) = setup().await; let (action_certificate, _, _) = get_bridge_authority_approved_action( vec![&mock0, &mock1, &mock2, &mock3], @@ -840,9 +904,15 @@ mod tests { ); let action = action_certificate.data().clone(); - - let tx_data = - build_transaction(sui_address, &gas_object_ref, action_certificate.clone()).unwrap(); + let arg = DUMMY_MUTALBE_BRIDGE_OBJECT_ARG; + let tx_data = build_sui_transaction( + sui_address, + &gas_object_ref, + action_certificate.clone(), + arg, + &id_token_map, + ) + .unwrap(); let tx_digest = get_tx_digest(tx_data, &dummy_sui_key); mock_transaction_error( &sui_client_mock, @@ -862,7 +932,7 @@ mod tests { store.insert_pending_actions(&[action.clone()]).unwrap(); assert_eq!( - store.get_all_pending_actions().unwrap()[&action.digest()], + store.get_all_pending_actions()[&action.digest()], action.clone() ); @@ -881,11 +951,7 @@ mod tests { // The next retry will see the action is already processed on chain and remove it from WAL let now = std::time::Instant::now(); let action_digest = action.digest(); - while store - .get_all_pending_actions() - .unwrap() - .contains_key(&action_digest) - { + while store.get_all_pending_actions().contains_key(&action_digest) { if now.elapsed().as_secs() > 10 { panic!("Timeout waiting for action to be removed from WAL"); } @@ -940,6 +1006,9 @@ mod tests { Some(sui_tx_event_index), None, None, + None, + None, + None, ); let sigs = @@ -971,10 +1040,14 @@ mod tests { sui_client_mock: &SuiMockClient, tx_digest: TransactionDigest, status: SuiExecutionStatus, + events: Option>, wildcard: bool, ) { let mut response = SuiTransactionBlockResponse::new(tx_digest); let effects = SuiTransactionBlockEffects::new_for_testing(tx_digest, status); + if let Some(events) = events { + response.events = Some(SuiTransactionBlockEvents { data: events }); + } response.effects = Some(effects); if wildcard { sui_client_mock.set_wildcard_transaction_response(Ok(response)); @@ -997,7 +1070,7 @@ mod tests { } #[allow(clippy::type_complexity)] - fn setup() -> ( + async fn setup() -> ( mysten_metrics::metered_channel::Sender, mysten_metrics::metered_channel::Sender, SuiMockClient, @@ -1012,10 +1085,12 @@ mod tests { Vec>, ObjectRef, SuiAddress, + HashMap, ) { telemetry_subscribers::init_for_testing(); let registry = Registry::new(); mysten_metrics::init_metrics(®istry); + init_all_struct_tags(); let (sui_address, kp): (_, fastcrypto::secp256k1::Secp256k1KeyPair) = get_key_pair(); let sui_key = SuiKeyPair::from(kp); @@ -1052,10 +1127,19 @@ mod tests { sui_key, sui_address, gas_object_ref.0, - ); + ) + .await; let (executor_handle, signing_tx, execution_tx) = executor.run_inner(); handles.extend(executor_handle); + + // Mock id token type map for testing + let mut id_token_map = HashMap::new(); + id_token_map.insert(TOKEN_ID_BTC, TypeTag::from_str("0xb::btc::BTC").unwrap()); + id_token_map.insert(TOKEN_ID_ETH, TypeTag::from_str("0xb::eth::ETH").unwrap()); + id_token_map.insert(TOKEN_ID_USDC, TypeTag::from_str("0xb::usdc::USDC").unwrap()); + id_token_map.insert(TOKEN_ID_USDT, TypeTag::from_str("0xb::usdt::USDT").unwrap()); + ( signing_tx, execution_tx, @@ -1071,6 +1155,7 @@ mod tests { handles, gas_object_ref, sui_address, + id_token_map, ) } } diff --git a/crates/sui-bridge/src/client/bridge_authority_aggregator.rs b/crates/sui-bridge/src/client/bridge_authority_aggregator.rs index 0d647f4f1d829..ba6c66395a66c 100644 --- a/crates/sui-bridge/src/client/bridge_authority_aggregator.rs +++ b/crates/sui-bridge/src/client/bridge_authority_aggregator.rs @@ -61,6 +61,7 @@ impl BridgeAuthorityAggregator { } } + // TODO: change the signature, to remove `threshold`, which can be obtained from `BridgeAction` pub async fn request_committee_signatures( &self, action: BridgeAction, @@ -328,6 +329,9 @@ mod tests { Some(sui_tx_event_index), Some(nonce), Some(amount), + None, + None, + None, ); // All authorities return signatures @@ -422,6 +426,9 @@ mod tests { Some(sui_tx_event_index), Some(nonce), Some(amount), + None, + None, + None, ); // Only mock authority 2 and 3 to return signatures, such that if BridgeAuthorityAggregator @@ -554,6 +561,9 @@ mod tests { Some(sui_tx_event_index), Some(nonce), Some(amount), + None, + None, + None, ); let sig_0 = sign_action_with_key(&action, &secrets[0]); diff --git a/crates/sui-bridge/src/client/bridge_client.rs b/crates/sui-bridge/src/client/bridge_client.rs index 1180f38cd94f0..c4c088a89622f 100644 --- a/crates/sui-bridge/src/client/bridge_client.rs +++ b/crates/sui-bridge/src/client/bridge_client.rs @@ -49,7 +49,7 @@ impl BridgeClient { self.committee = committee; } - // Important: the paths need to match the ones in mod.rs + // Important: the paths need to match the ones in server/mod.rs fn bridge_action_to_path(event: &BridgeAction) -> String { match event { BridgeAction::SuiToEthBridgeAction(e) => format!( @@ -89,7 +89,7 @@ impl BridgeClient { BridgeAction::AssetPriceUpdateAction(a) => { let chain_id = (a.chain_id as u8).to_string(); let nonce = a.nonce.to_string(); - let token_id = (a.token_id as u8).to_string(); + let token_id = a.token_id.to_string(); let new_usd_price = a.new_usd_price.to_string(); format!("sign/update_asset_price/{chain_id}/{nonce}/{token_id}/{new_usd_price}") } @@ -108,6 +108,64 @@ impl BridgeClient { format!("{}/{}", path, call_data) } } + BridgeAction::AddTokensOnSuiAction(a) => { + let chain_id = (a.chain_id as u8).to_string(); + let nonce = a.nonce.to_string(); + let native = if a.native { "1" } else { "0" }; + let token_ids = a + .token_ids + .iter() + .map(|id| id.to_string()) + .collect::>() + .join(","); + let token_type_names = a + .token_type_names + .iter() + .map(|name| name.to_canonical_string(true)) + .collect::>() + .join(","); + let token_prices = a + .token_prices + .iter() + .map(|price| price.to_string()) + .collect::>() + .join(","); + format!( + "sign/add_tokens_on_sui/{chain_id}/{nonce}/{native}/{token_ids}/{token_type_names}/{token_prices}" + ) + } + BridgeAction::AddTokensOnEvmAction(a) => { + let chain_id = (a.chain_id as u8).to_string(); + let nonce = a.nonce.to_string(); + let native = if a.native { "1" } else { "0" }; + let token_ids = a + .token_ids + .iter() + .map(|id| id.to_string()) + .collect::>() + .join(","); + let token_addresses = a + .token_addresses + .iter() + .map(|name| format!("{:?}", name)) + .collect::>() + .join(","); + let token_sui_decimals = a + .token_sui_decimals + .iter() + .map(|id| id.to_string()) + .collect::>() + .join(","); + let token_prices = a + .token_prices + .iter() + .map(|price| price.to_string()) + .collect::>() + .join(","); + format!( + "sign/add_tokens_on_evm/{chain_id}/{nonce}/{native}/{token_ids}/{token_addresses}/{token_sui_decimals}/{token_prices}" + ) + } } } @@ -167,22 +225,23 @@ impl BridgeClient { #[cfg(test)] mod tests { + use super::*; + use crate::test_utils::run_mock_bridge_server; use crate::{ abi::EthToSuiTokenBridgeV1, crypto::BridgeAuthoritySignInfo, events::EmittedSuiToEthTokenBridgeV1, server::mock_handler::BridgeRequestMockHandler, test_utils::{get_test_authority_and_key, get_test_sui_to_eth_bridge_action}, - types::{BridgeChainId, SignedBridgeAction, TokenId}, + types::SignedBridgeAction, }; + use ethers::types::Address as EthAddress; + use ethers::types::TxHash; use fastcrypto::hash::{HashFunction, Keccak256}; use fastcrypto::traits::KeyPair; use prometheus::Registry; - - use super::*; - use crate::test_utils::run_mock_bridge_server; - use ethers::types::Address as EthAddress; - use ethers::types::TxHash; + use sui_types::bridge::{BridgeChainId, TOKEN_ID_BTC, TOKEN_ID_USDT}; + use sui_types::TypeTag; use sui_types::{base_types::SuiAddress, crypto::get_key_pair, digests::TransactionDigest}; #[tokio::test] @@ -193,7 +252,8 @@ mod tests { let pubkey_bytes = BridgeAuthorityPublicKeyBytes::from(&pubkey); let committee = Arc::new(BridgeCommittee::new(vec![authority.clone()]).unwrap()); - let action = get_test_sui_to_eth_bridge_action(None, Some(1), Some(1), Some(100)); + let action = + get_test_sui_to_eth_bridge_action(None, Some(1), Some(1), Some(100), None, None, None); // Ok let client = BridgeClient::new(pubkey_bytes.clone(), committee).unwrap(); @@ -270,8 +330,15 @@ mod tests { let tx_digest = TransactionDigest::random(); let event_idx = 4; - let action = - get_test_sui_to_eth_bridge_action(Some(tx_digest), Some(event_idx), Some(1), Some(100)); + let action = get_test_sui_to_eth_bridge_action( + Some(tx_digest), + Some(event_idx), + Some(1), + Some(100), + None, + None, + None, + ); let sig = BridgeAuthoritySignInfo::new(&action, &secret); let signed_event = SignedBridgeAction::new_from_data_and_sig(action.clone(), sig.clone()); mock_handler.add_sui_event_response(tx_digest, event_idx, Ok(signed_event.clone())); @@ -283,8 +350,15 @@ mod tests { .unwrap(); // mismatched action would fail, this could happen when the authority fetched the wrong event - let action2 = - get_test_sui_to_eth_bridge_action(Some(tx_digest), Some(event_idx), Some(2), Some(200)); + let action2 = get_test_sui_to_eth_bridge_action( + Some(tx_digest), + Some(event_idx), + Some(2), + Some(200), + None, + None, + None, + ); let wrong_sig = BridgeAuthoritySignInfo::new(&action2, &secret); let wrong_signed_action = SignedBridgeAction::new_from_data_and_sig(action2.clone(), wrong_sig.clone()); @@ -358,13 +432,13 @@ mod tests { sui_tx_digest, sui_tx_event_index, sui_bridge_event: EmittedSuiToEthTokenBridgeV1 { - sui_chain_id: BridgeChainId::SuiDevnet, + sui_chain_id: BridgeChainId::SuiCustom, nonce: 1, sui_address: SuiAddress::random_for_testing_only(), eth_chain_id: BridgeChainId::EthSepolia, eth_address: EthAddress::random(), - token_id: TokenId::USDT, - amount: 1, + token_id: TOKEN_ID_USDT, + amount_sui_adjusted: 1, }, }); assert_eq!( @@ -384,10 +458,10 @@ mod tests { eth_chain_id: BridgeChainId::EthSepolia, nonce: 1, eth_address: EthAddress::random(), - sui_chain_id: BridgeChainId::SuiDevnet, + sui_chain_id: BridgeChainId::SuiCustom, sui_address: SuiAddress::random_for_testing_only(), - token_id: TokenId::USDT, - amount: 1, + token_id: TOKEN_ID_USDT, + sui_adjusted_amount: 1, }, }); @@ -435,30 +509,30 @@ mod tests { ); let action = BridgeAction::EmergencyAction(crate::types::EmergencyAction { - chain_id: BridgeChainId::SuiLocalTest, + chain_id: BridgeChainId::SuiCustom, nonce: 5, action_type: crate::types::EmergencyActionType::Pause, }); assert_eq!( BridgeClient::bridge_action_to_path(&action), - "sign/emergency_button/3/5/0", + "sign/emergency_button/2/5/0", ); let action = BridgeAction::LimitUpdateAction(crate::types::LimitUpdateAction { - chain_id: BridgeChainId::SuiLocalTest, + chain_id: BridgeChainId::SuiCustom, nonce: 10, - sending_chain_id: BridgeChainId::EthLocalTest, + sending_chain_id: BridgeChainId::EthCustom, new_usd_limit: 100, }); assert_eq!( BridgeClient::bridge_action_to_path(&action), - "sign/update_limit/3/10/12/100", + "sign/update_limit/2/10/12/100", ); let action = BridgeAction::AssetPriceUpdateAction(crate::types::AssetPriceUpdateAction { - chain_id: BridgeChainId::SuiDevnet, + chain_id: BridgeChainId::SuiCustom, nonce: 8, - token_id: TokenId::BTC, + token_id: TOKEN_ID_BTC, new_usd_price: 100_000_000, }); assert_eq!( @@ -469,7 +543,7 @@ mod tests { let action = BridgeAction::EvmContractUpgradeAction(crate::types::EvmContractUpgradeAction { nonce: 123, - chain_id: BridgeChainId::EthLocalTest, + chain_id: BridgeChainId::EthCustom, proxy_address: EthAddress::repeat_byte(6), new_impl_address: EthAddress::repeat_byte(9), call_data: vec![], @@ -485,7 +559,7 @@ mod tests { let action = BridgeAction::EvmContractUpgradeAction(crate::types::EvmContractUpgradeAction { nonce: 123, - chain_id: BridgeChainId::EthLocalTest, + chain_id: BridgeChainId::EthCustom, proxy_address: EthAddress::repeat_byte(6), new_impl_address: EthAddress::repeat_byte(9), call_data: call_data.clone(), @@ -499,7 +573,7 @@ mod tests { let action = BridgeAction::EvmContractUpgradeAction(crate::types::EvmContractUpgradeAction { nonce: 123, - chain_id: BridgeChainId::EthLocalTest, + chain_id: BridgeChainId::EthCustom, proxy_address: EthAddress::repeat_byte(6), new_impl_address: EthAddress::repeat_byte(9), call_data, @@ -508,5 +582,40 @@ mod tests { BridgeClient::bridge_action_to_path(&action), "sign/upgrade_evm_contract/12/123/0606060606060606060606060606060606060606/0909090909090909090909090909090909090909/5cd8a76b000000000000000000000000000000000000000000000000000000000000002a", ); + + let action = BridgeAction::AddTokensOnSuiAction(crate::types::AddTokensOnSuiAction { + nonce: 3, + chain_id: BridgeChainId::SuiCustom, + native: false, + token_ids: vec![99, 100, 101], + token_type_names: vec![ + TypeTag::from_str("0x0000000000000000000000000000000000000000000000000000000000000abc::my_coin::MyCoin1").unwrap(), + TypeTag::from_str("0x0000000000000000000000000000000000000000000000000000000000000abc::my_coin::MyCoin2").unwrap(), + TypeTag::from_str("0x0000000000000000000000000000000000000000000000000000000000000abc::my_coin::MyCoin3").unwrap(), + ], + token_prices: vec![1_000_000_000, 2_000_000_000, 3_000_000_000], + }); + assert_eq!( + BridgeClient::bridge_action_to_path(&action), + "sign/add_tokens_on_sui/2/3/0/99,100,101/0x0000000000000000000000000000000000000000000000000000000000000abc::my_coin::MyCoin1,0x0000000000000000000000000000000000000000000000000000000000000abc::my_coin::MyCoin2,0x0000000000000000000000000000000000000000000000000000000000000abc::my_coin::MyCoin3/1000000000,2000000000,3000000000", + ); + + let action = BridgeAction::AddTokensOnEvmAction(crate::types::AddTokensOnEvmAction { + nonce: 0, + chain_id: BridgeChainId::EthCustom, + native: true, + token_ids: vec![99, 100, 101], + token_addresses: vec![ + EthAddress::repeat_byte(1), + EthAddress::repeat_byte(2), + EthAddress::repeat_byte(3), + ], + token_sui_decimals: vec![5, 6, 7], + token_prices: vec![1_000_000_000, 2_000_000_000, 3_000_000_000], + }); + assert_eq!( + BridgeClient::bridge_action_to_path(&action), + "sign/add_tokens_on_evm/12/0/1/99,100,101/0x0101010101010101010101010101010101010101,0x0202020202020202020202020202020202020202,0x0303030303030303030303030303030303030303/5,6,7/1000000000,2000000000,3000000000", + ); } } diff --git a/crates/sui-bridge/src/config.rs b/crates/sui-bridge/src/config.rs index 8118b06899736..2127942ea0318 100644 --- a/crates/sui-bridge/src/config.rs +++ b/crates/sui-bridge/src/config.rs @@ -1,82 +1,114 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 +use crate::abi::{EthBridgeCommittee, EthBridgeConfig, EthSuiBridge}; use crate::crypto::BridgeAuthorityKeyPair; use crate::error::BridgeError; use crate::eth_client::EthClient; use crate::sui_client::SuiClient; -use crate::types::BridgeAction; +use crate::types::{is_route_valid, BridgeAction}; use anyhow::anyhow; +use ethers::providers::Middleware; use ethers::types::Address as EthAddress; -use fastcrypto::traits::EncodeDecodeBase64; +use fastcrypto::traits::{EncodeDecodeBase64, KeyPair}; +use futures::{future, StreamExt}; use serde::{Deserialize, Serialize}; use serde_with::serde_as; -use std::collections::{BTreeMap, HashSet}; +use std::collections::HashSet; use std::path::PathBuf; use std::str::FromStr; use std::sync::Arc; use sui_config::Config; -use sui_sdk::SuiClient as SuiSdkClient; +use sui_json_rpc_types::Coin; +use sui_sdk::apis::CoinReadApi; +use sui_sdk::{SuiClient as SuiSdkClient, SuiClientBuilder}; use sui_types::base_types::ObjectRef; use sui_types::base_types::{ObjectID, SuiAddress}; +use sui_types::bridge::BridgeChainId; use sui_types::crypto::SuiKeyPair; +use sui_types::digests::{get_mainnet_chain_identifier, get_testnet_chain_identifier}; use sui_types::event::EventID; use sui_types::object::Owner; -use sui_types::Identifier; use tracing::info; #[serde_as] #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] -pub struct BridgeNodeConfig { - /// The port that the server listens on. - pub server_listen_port: u16, - /// The port that for metrics server. - pub metrics_port: u16, - /// Path of the file where bridge authority key (Secp256k1) is stored as Base64 encoded `privkey`. - pub bridge_authority_key_path_base64_raw: PathBuf, - /// Rpc url for Sui fullnode, used for query stuff and submit transactions. - pub sui_rpc_url: String, +pub struct EthConfig { /// Rpc url for Eth fullnode, used for query stuff. pub eth_rpc_url: String, - /// The eth contract addresses (hex). It must not be empty. It serves two purpose: - /// 1. validator only signs bridge actions that are generated from these contracts. - /// 2. for EthSyncer to watch for when `run_client` is true. - pub eth_addresses: Vec, + /// The proxy address of SuiBridge + pub eth_bridge_proxy_address: String, + /// The expected BridgeChainId on Eth side. + pub eth_bridge_chain_id: u8, + /// The starting block for EthSyncer to monitor eth contracts. + /// It is required when `run_client` is true. Usually this is + /// the block number when the bridge contracts are deployed. + /// When BridgeNode starts, it reads the contract watermark from storage. + /// If the watermark is not found, it will start from this fallback block number. + /// If the watermark is found, it will start from the watermark. + /// this v.s.`eth_contracts_start_block_override`: + pub eth_contracts_start_block_fallback: Option, + /// The starting block for EthSyncer to monitor eth contracts. It overrides + /// the watermark in storage. This is useful when we want to reprocess the events + /// from a specific block number. + /// Note: this field has to be reset after starting the BridgeNode, otherwise it will + /// reprocess the events from this block number every time it starts. + #[serde(skip_serializing_if = "Option::is_none")] + pub eth_contracts_start_block_override: Option, +} + +#[serde_as] +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct SuiConfig { + /// Rpc url for Sui fullnode, used for query stuff and submit transactions. + pub sui_rpc_url: String, + /// The expected BridgeChainId on Sui side. + pub sui_bridge_chain_id: u8, /// Path of the file where bridge client key (any SuiKeyPair) is stored as Base64 encoded `flag || privkey`. /// If `run_client` is true, and this is None, then use `bridge_authority_key_path_base64_raw` as client key. #[serde(skip_serializing_if = "Option::is_none")] pub bridge_client_key_path_base64_sui_key: Option, - /// Whether to run client. If true, `bridge_client_key_path_base64_sui_key`, - /// `bridge_client_gas_object` and `db_path` needs to be provided. - pub run_client: bool, /// The gas object to use for paying for gas fees for the client. It needs to - /// be owned by the address associated with bridge client key. + /// be owned by the address associated with bridge client key. If not set + /// and `run_client` is true, it will query and use the gas object with highest + /// amount for the account. #[serde(skip_serializing_if = "Option::is_none")] pub bridge_client_gas_object: Option, - /// Path of the client storage. Required when `run_client` is true. - #[serde(skip_serializing_if = "Option::is_none")] - pub db_path: Option, - // TODO: this should be hardcoded and removed from config - /// The sui modules of bridge packages for client to watch for. Need to contain at least one item when `run_client` is true. - pub sui_bridge_modules: Option>, - // TODO: we need to hardcode the starting blocks for eth networks for cold start. - /// Override the start block number for each eth address. Key must be in `eth_addresses`. - /// When set, EthSyncer will start from this block number (inclusively) instead of the one in storage. - /// Key: eth address, Value: block number to start from - /// Note: This field should be rarely used. Only use it when you understand how to follow up. - #[serde(skip_serializing_if = "Option::is_none")] - pub eth_bridge_contracts_start_block_override: Option>, - /// Override the last processed EventID for each bridge module. Key must be in `sui_bridge_modules`. + /// Override the last processed EventID for bridge module `bridge`. /// When set, SuiSyncer will start from this cursor (exclusively) instead of the one in storage. + /// If the cursor is not found in storage or override, the query will start from genesis. /// Key: sui module, Value: last processed EventID (tx_digest, event_seq). /// Note 1: This field should be rarely used. Only use it when you understand how to follow up. /// Note 2: the EventID needs to be valid, namely it must exist and matches the filter. /// Otherwise, it will miss one event because of fullnode Event query semantics. #[serde(skip_serializing_if = "Option::is_none")] - pub sui_bridge_modules_last_processed_event_id_override: Option>, + pub sui_bridge_module_last_processed_event_id_override: Option, +} + +#[serde_as] +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct BridgeNodeConfig { + /// The port that the server listens on. + pub server_listen_port: u16, + /// The port that for metrics server. + pub metrics_port: u16, + /// Path of the file where bridge authority key (Secp256k1) is stored as Base64 encoded `privkey`. + pub bridge_authority_key_path_base64_raw: PathBuf, + /// Whether to run client. If true, `bridge_client_key_path_base64_sui_key`, + /// and `db_path` needs to be provided. + pub run_client: bool, + /// Path of the client storage. Required when `run_client` is true. + #[serde(skip_serializing_if = "Option::is_none")] + pub db_path: Option, /// A list of approved governance actions. Action in this list will be signed when requested by client. pub approved_governance_actions: Vec, + /// Sui configuration + pub sui: SuiConfig, + /// Eth configuration + pub eth: EthConfig, } impl Config for BridgeNodeConfig {} @@ -85,38 +117,53 @@ impl BridgeNodeConfig { pub async fn validate( &self, ) -> anyhow::Result<(BridgeServerConfig, Option)> { + if !is_route_valid( + BridgeChainId::try_from(self.sui.sui_bridge_chain_id)?, + BridgeChainId::try_from(self.eth.eth_bridge_chain_id)?, + ) { + return Err(anyhow!( + "Route between Sui chain id {} and Eth chain id {} is not valid", + self.sui.sui_bridge_chain_id, + self.eth.eth_bridge_chain_id, + )); + }; + let bridge_authority_key = read_bridge_authority_key(&self.bridge_authority_key_path_base64_raw)?; - // TODO: verify it's part of bridge committee - let sui_client = Arc::new(SuiClient::::new(&self.sui_rpc_url).await?); - - // TODO(audit-blocking): verify Sui Chain ID matches bridge Chain ID + // we do this check here instead of `prepare_for_sui` below because + // that is only called when `run_client` is true. + let sui_client = Arc::new(SuiClient::::new(&self.sui.sui_rpc_url).await?); + let bridge_committee = sui_client + .get_bridge_committee() + .await + .map_err(|e| anyhow!("Error getting bridge committee: {:?}", e))?; + if !bridge_committee.is_active_member(&bridge_authority_key.public().into()) { + return Err(anyhow!( + "Bridge authority key is not part of bridge committee" + )); + } - if self.eth_addresses.is_empty() { - return Err(anyhow!("`eth_addresses` must contain at least one address")); + let (eth_client, eth_contracts) = self.prepare_for_eth().await?; + let bridge_summary = sui_client + .get_bridge_summary() + .await + .map_err(|e| anyhow!("Error getting bridge summary: {:?}", e))?; + if bridge_summary.chain_id != self.sui.sui_bridge_chain_id { + anyhow::bail!( + "Bridge chain id mismatch: expected {}, but connected to {}", + self.sui.sui_bridge_chain_id, + bridge_summary.chain_id + ); } - let eth_bridge_contracts = self - .eth_addresses - .iter() - .map(|addr| EthAddress::from_str(addr)) - .collect::, _>>()?; - let eth_client = Arc::new( - EthClient::::new( - &self.eth_rpc_url, - HashSet::from_iter(eth_bridge_contracts.iter().cloned()), - ) - .await?, - ); - // TODO(audit-blocking): verify Ethereum Chain ID matches bridge Chain ID // Validate approved actions that must be governace actions for action in &self.approved_governance_actions { if !action.is_governace_action() { - return Err(anyhow::anyhow!(format!( + anyhow::bail!(format!( "{:?}", BridgeError::ActionIsNotGovernanceAction(action.clone()) - ))); + )); } } let approved_governance_actions = self.approved_governance_actions.clone(); @@ -129,85 +176,174 @@ impl BridgeNodeConfig { eth_client: eth_client.clone(), approved_governance_actions, }; - if !self.run_client { return Ok((bridge_server_config, None)); } + // If client is enabled, prepare client config - let bridge_client_key = if self.bridge_client_key_path_base64_sui_key.is_none() { - let bridge_client_key = - read_bridge_authority_key(&self.bridge_authority_key_path_base64_raw)?; - Ok(SuiKeyPair::from(bridge_client_key)) - } else { - read_bridge_client_key(self.bridge_client_key_path_base64_sui_key.as_ref().unwrap()) - }?; + let (bridge_client_key, client_sui_address, gas_object_ref) = + self.prepare_for_sui(sui_client.clone()).await?; - let client_sui_address = SuiAddress::from(&bridge_client_key.public()); - info!("Bridge client sui address: {:?}", client_sui_address); - let gas_object_id = self.bridge_client_gas_object.ok_or(anyhow!( - "`bridge_client_gas_object` is required when `run_client` is true" - ))?; let db_path = self .db_path .clone() .ok_or(anyhow!("`db_path` is required when `run_client` is true"))?; - let mut eth_bridge_contracts_start_block_override = BTreeMap::new(); - match &self.eth_bridge_contracts_start_block_override { - Some(overrides) => { - for (addr, block_number) in overrides { - let address = EthAddress::from_str(addr)?; - if eth_bridge_contracts.contains(&address) { - eth_bridge_contracts_start_block_override.insert(address, *block_number); - } else { - return Err(anyhow!( - "Override start block number for address {:?} is not in `eth_addresses`", - addr - )); - } - } - } - None => {} + let bridge_client_config = BridgeClientConfig { + sui_address: client_sui_address, + key: bridge_client_key, + gas_object_ref, + metrics_port: self.metrics_port, + sui_client: sui_client.clone(), + eth_client: eth_client.clone(), + db_path, + eth_contracts, + // in `prepare_for_eth` we check if this is None when `run_client` is true. Safe to unwrap here. + eth_contracts_start_block_fallback: self + .eth + .eth_contracts_start_block_fallback + .unwrap(), + eth_contracts_start_block_override: self.eth.eth_contracts_start_block_override, + sui_bridge_module_last_processed_event_id_override: self + .sui + .sui_bridge_module_last_processed_event_id_override, + }; + + Ok((bridge_server_config, Some(bridge_client_config))) + } + + async fn prepare_for_eth( + &self, + ) -> anyhow::Result<(Arc>, Vec)> { + let bridge_proxy_address = EthAddress::from_str(&self.eth.eth_bridge_proxy_address)?; + let provider = Arc::new( + ethers::prelude::Provider::::try_from(&self.eth.eth_rpc_url) + .unwrap() + .interval(std::time::Duration::from_millis(2000)), + ); + let chain_id = provider.get_chainid().await?; + let sui_bridge = EthSuiBridge::new(bridge_proxy_address, provider.clone()); + let committee_address: EthAddress = sui_bridge.committee().call().await?; + let limiter_address: EthAddress = sui_bridge.limiter().call().await?; + let vault_address: EthAddress = sui_bridge.vault().call().await?; + let committee = EthBridgeCommittee::new(committee_address, provider.clone()); + let config_address: EthAddress = committee.config().call().await?; + let config = EthBridgeConfig::new(config_address, provider.clone()); + + if self.run_client && self.eth.eth_contracts_start_block_fallback.is_none() { + return Err(anyhow!( + "eth_contracts_start_block_fallback is required when run_client is true" + )); } - let sui_bridge_modules = match &self.sui_bridge_modules { - Some(modules) => { - if modules.is_empty() { - return Err(anyhow!( - "`sui_bridge_modules` is required when `run_client` is true" - )); - } - modules - .iter() - .map(|module| Identifier::from_str(module)) - .collect::, _>>() - .map_err(|e| anyhow!("Error parsing sui module: {:?}", e))? - } + // If bridge chain id is Eth Mainent or Sepolia, we expect to see chain + // identifier to match accordingly. + let bridge_chain_id: u8 = config.chain_id().call().await?; + if self.eth.eth_bridge_chain_id != bridge_chain_id { + return Err(anyhow!( + "Bridge chain id mismatch: expected {}, but connected to {}", + self.eth.eth_bridge_chain_id, + bridge_chain_id + )); + } + if bridge_chain_id == BridgeChainId::EthMainnet as u8 && chain_id.as_u64() != 1 { + anyhow::bail!( + "Expected Eth chain id 1, but connected to {}", + chain_id.as_u64() + ); + } + if bridge_chain_id == BridgeChainId::EthSepolia as u8 && chain_id.as_u64() != 11155111 { + anyhow::bail!( + "Expected Eth chain id 11155111, but connected to {}", + chain_id.as_u64() + ); + } + info!( + "Connected to Eth chain: {}, Bridge chain id: {}", + chain_id.as_u64(), + bridge_chain_id, + ); + + let eth_client = Arc::new( + EthClient::::new( + &self.eth.eth_rpc_url, + HashSet::from_iter(vec![ + bridge_proxy_address, + committee_address, + config_address, + limiter_address, + vault_address, + ]), + ) + .await?, + ); + let contract_addresses = vec![ + bridge_proxy_address, + committee_address, + config_address, + limiter_address, + vault_address, + ]; + Ok((eth_client, contract_addresses)) + } + + async fn prepare_for_sui( + &self, + sui_client: Arc>, + ) -> anyhow::Result<(SuiKeyPair, SuiAddress, ObjectRef)> { + let bridge_client_key = match &self.sui.bridge_client_key_path_base64_sui_key { None => { - return Err(anyhow!( - "`sui_bridge_modules` is required when `run_client` is true" - )) + let bridge_client_key = + read_bridge_authority_key(&self.bridge_authority_key_path_base64_raw)?; + Ok(SuiKeyPair::from(bridge_client_key)) } - }; + Some(path) => read_bridge_client_key(path), + }?; - let mut sui_bridge_modules_last_processed_event_id_override = BTreeMap::new(); - match &self.sui_bridge_modules_last_processed_event_id_override { - Some(overrides) => { - for (module, cursor) in overrides { - let module = Identifier::from_str(module)?; - if sui_bridge_modules.contains(&module) { - sui_bridge_modules_last_processed_event_id_override.insert(module, *cursor); - } else { - return Err(anyhow!( - "Override start tx digest for module {:?} is not in `sui_bridge_modules`", - module - )); - } - } - } - None => {} + // If bridge chain id is Sui Mainent or Testnet, we expect to see chain + // identifier to match accordingly. + let sui_identifier = sui_client + .get_chain_identifier() + .await + .map_err(|e| anyhow!("Error getting chain identifier from Sui: {:?}", e))?; + if self.sui.sui_bridge_chain_id == BridgeChainId::SuiMainnet as u8 + && sui_identifier != get_mainnet_chain_identifier().to_string() + { + anyhow::bail!( + "Expected sui chain identifier {}, but connected to {}", + self.sui.sui_bridge_chain_id, + sui_identifier + ); + } + if self.sui.sui_bridge_chain_id == BridgeChainId::SuiTestnet as u8 + && sui_identifier != get_testnet_chain_identifier().to_string() + { + anyhow::bail!( + "Expected sui chain identifier {}, but connected to {}", + self.sui.sui_bridge_chain_id, + sui_identifier + ); } + info!( + "Connected to Sui chain: {}, Bridge chain id: {}", + sui_identifier, self.sui.sui_bridge_chain_id, + ); + + let client_sui_address = SuiAddress::from(&bridge_client_key.public()); + // TODO: decide a minimal amount here + let gas_object_id = match self.sui.bridge_client_gas_object { + Some(id) => id, + None => { + let sui_client = SuiClientBuilder::default() + .build(&self.sui.sui_rpc_url) + .await?; + let coin = + pick_highest_balance_coin(sui_client.coin_read_api(), client_sui_address, 0) + .await?; + coin.coin_object_id + } + }; let (gas_coin, gas_object_ref, owner) = sui_client .get_gas_data_panic_if_not_gas(gas_object_id) .await; @@ -215,25 +351,13 @@ impl BridgeNodeConfig { return Err(anyhow!("Gas object {:?} is not owned by bridge client key's associated sui address {:?}, but {:?}", gas_object_id, client_sui_address, owner)); } info!( - "Starting bridge client with gas object {:?}, balance: {}", + "Starting bridge client with address: {:?}, gas object {:?}, balance: {}", + client_sui_address, gas_object_ref.0, gas_coin.value() ); - let bridge_client_config = BridgeClientConfig { - sui_address: client_sui_address, - key: bridge_client_key, - gas_object_ref, - metrics_port: self.metrics_port, - sui_client: sui_client.clone(), - eth_client: eth_client.clone(), - db_path, - eth_bridge_contracts, - sui_bridge_modules, - eth_bridge_contracts_start_block_override, - sui_bridge_modules_last_processed_event_id_override, - }; - Ok((bridge_server_config, Some(bridge_client_config))) + Ok((bridge_client_key, client_sui_address, gas_object_ref)) } } @@ -256,10 +380,11 @@ pub struct BridgeClientConfig { pub sui_client: Arc>, pub eth_client: Arc>, pub db_path: PathBuf, - pub eth_bridge_contracts: Vec, - pub sui_bridge_modules: Vec, - pub eth_bridge_contracts_start_block_override: BTreeMap, - pub sui_bridge_modules_last_processed_event_id_override: BTreeMap, + pub eth_contracts: Vec, + // See `BridgeNodeConfig` for the explanation of following two fields. + pub eth_contracts_start_block_fallback: u64, + pub eth_contracts_start_block_override: Option, + pub sui_bridge_module_last_processed_event_id_override: Option, } /// Read Bridge Authority key (Secp256k1KeyPair) from a file. @@ -291,3 +416,51 @@ pub fn read_bridge_client_key(path: &PathBuf) -> Result, +} + +impl Config for BridgeCommitteeConfig {} + +pub async fn pick_highest_balance_coin( + coin_read_api: &CoinReadApi, + address: SuiAddress, + minimal_amount: u64, +) -> anyhow::Result { + let mut highest_balance = 0; + let mut highest_balance_coin = None; + coin_read_api + .get_coins_stream(address, None) + .for_each(|coin: Coin| { + if coin.balance > highest_balance { + highest_balance = coin.balance; + highest_balance_coin = Some(coin.clone()); + } + future::ready(()) + }) + .await; + if highest_balance_coin.is_none() { + return Err(anyhow!("No Sui coins found for address {:?}", address)); + } + if highest_balance < minimal_amount { + return Err(anyhow!( + "Found no single coin that has >= {} balance Sui for address {:?}", + minimal_amount, + address, + )); + } + Ok(highest_balance_coin.unwrap()) +} + +#[derive(Debug, Eq, PartialEq, Clone)] +pub struct EthContractAddresses { + pub sui_bridge: EthAddress, + pub bridge_committee: EthAddress, + pub bridge_config: EthAddress, + pub bridge_limiter: EthAddress, + pub bridge_vault: EthAddress, +} diff --git a/crates/sui-bridge/src/crypto.rs b/crates/sui-bridge/src/crypto.rs index 2a2ff909a014d..2d992d4b19bbe 100644 --- a/crates/sui-bridge/src/crypto.rs +++ b/crates/sui-bridge/src/crypto.rs @@ -63,6 +63,18 @@ impl ToFromBytes for BridgeAuthorityPublicKeyBytes { } } +/// implement `FromStr` for `BridgeAuthorityPublicKeyBytes` +/// to convert a hex-string to public key bytes. +impl std::str::FromStr for BridgeAuthorityPublicKeyBytes { + type Err = FastCryptoError; + fn from_str(s: &str) -> Result { + let bytes = Hex::decode(s).map_err(|e| { + FastCryptoError::GeneralError(format!("Failed to decode hex string: {}", e)) + })?; + Self::from_bytes(&bytes) + } +} + pub struct ConciseBridgeAuthorityPublicKeyBytesRef<'a>(&'a BridgeAuthorityPublicKeyBytes); impl Debug for ConciseBridgeAuthorityPublicKeyBytesRef<'_> { @@ -107,7 +119,6 @@ pub struct BridgeAuthoritySignInfo { impl BridgeAuthoritySignInfo { pub fn new(msg: &BridgeAction, secret: &BridgeAuthorityKeyPair) -> Self { let msg_bytes = msg.to_bytes(); - Self { authority_pub_key: secret.public().clone(), signature: secret.sign_recoverable_with_hash::(&msg_bytes), @@ -171,15 +182,14 @@ mod tests { use crate::events::EmittedSuiToEthTokenBridgeV1; use crate::test_utils::{get_test_authority_and_key, get_test_sui_to_eth_bridge_action}; use crate::types::SignedBridgeAction; - use crate::types::{ - BridgeAction, BridgeAuthority, BridgeChainId, SuiToEthBridgeAction, TokenId, - }; + use crate::types::{BridgeAction, BridgeAuthority, SuiToEthBridgeAction}; use ethers::types::Address as EthAddress; use fastcrypto::traits::{KeyPair, ToFromBytes}; use prometheus::Registry; use std::str::FromStr; use std::sync::Arc; use sui_types::base_types::SuiAddress; + use sui_types::bridge::{BridgeChainId, TOKEN_ID_ETH}; use sui_types::crypto::get_key_pair; use sui_types::digests::TransactionDigest; @@ -200,7 +210,7 @@ mod tests { let committee = BridgeCommittee::new(vec![authority1.clone(), authority2.clone()]).unwrap(); let action: BridgeAction = - get_test_sui_to_eth_bridge_action(None, Some(1), Some(1), Some(100)); + get_test_sui_to_eth_bridge_action(None, Some(1), Some(1), Some(100), None, None, None); let sig = BridgeAuthoritySignInfo::new(&action, &secret); @@ -219,7 +229,7 @@ mod tests { )); let mismatched_action: BridgeAction = - get_test_sui_to_eth_bridge_action(None, Some(2), Some(3), Some(4)); + get_test_sui_to_eth_bridge_action(None, Some(2), Some(3), Some(4), None, None, None); // Verification should fail - mismatched action assert!(matches!( verify_signed_bridge_action( @@ -234,7 +244,7 @@ mod tests { // Signature is invalid (signed over different message), verification should fail let action2: BridgeAction = - get_test_sui_to_eth_bridge_action(None, Some(3), Some(5), Some(77)); + get_test_sui_to_eth_bridge_action(None, Some(3), Some(5), Some(77), None, None, None); let invalid_sig = BridgeAuthoritySignInfo::new(&action2, &secret); let signed_action = SignedBridgeAction::new_from_data_and_sig(action.clone(), invalid_sig); @@ -331,8 +341,8 @@ mod tests { eth_chain_id: BridgeChainId::EthSepolia, eth_address: EthAddress::from_str("0xb18f79Fe671db47393315fFDB377Da4Ea1B7AF96") .unwrap(), - token_id: TokenId::ETH, - amount: 100000u64, + token_id: TOKEN_ID_ETH, + amount_sui_adjusted: 100000u64, }, }); let sig = BridgeAuthoritySignInfo { diff --git a/crates/sui-bridge/src/e2e_tests/basic.rs b/crates/sui-bridge/src/e2e_tests/basic.rs new file mode 100644 index 0000000000000..53605f683a9a7 --- /dev/null +++ b/crates/sui-bridge/src/e2e_tests/basic.rs @@ -0,0 +1,472 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::abi::{eth_sui_bridge, EthBridgeEvent, EthSuiBridge}; +use crate::client::bridge_authority_aggregator::BridgeAuthorityAggregator; +use crate::e2e_tests::test_utils::publish_coins_return_add_coins_on_sui_action; +use crate::e2e_tests::test_utils::{get_signatures, BridgeTestClusterBuilder}; +use crate::events::{ + SuiBridgeEvent, SuiToEthTokenBridgeV1, TokenTransferApproved, TokenTransferClaimed, +}; +use crate::sui_client::SuiBridgeClient; +use crate::sui_transaction_builder::build_add_tokens_on_sui_transaction; +use crate::types::{BridgeAction, BridgeActionStatus, SuiToEthBridgeAction}; +use crate::utils::EthSigner; +use eth_sui_bridge::EthSuiBridgeEvents; +use ethers::prelude::*; +use ethers::types::Address as EthAddress; +use move_core_types::ident_str; +use std::collections::{HashMap, HashSet}; + +use std::path::Path; + +use std::sync::Arc; +use sui_json_rpc_types::{ + SuiExecutionStatus, SuiTransactionBlockEffectsAPI, SuiTransactionBlockResponse, +}; +use sui_sdk::wallet_context::WalletContext; +use sui_sdk::SuiClient; +use sui_types::base_types::{ObjectRef, SuiAddress}; +use sui_types::bridge::{BridgeChainId, BridgeTokenMetadata, BRIDGE_MODULE_NAME, TOKEN_ID_ETH}; +use sui_types::programmable_transaction_builder::ProgrammableTransactionBuilder; +use sui_types::transaction::{ObjectArg, TransactionData}; +use sui_types::{TypeTag, BRIDGE_PACKAGE_ID}; +use tracing::info; + +#[tokio::test] +async fn test_bridge_from_eth_to_sui_to_eth() { + telemetry_subscribers::init_for_testing(); + + let eth_chain_id = BridgeChainId::EthCustom as u8; + let sui_chain_id = BridgeChainId::SuiCustom as u8; + + let mut bridge_test_cluster = BridgeTestClusterBuilder::new() + .with_eth_env(true) + .with_bridge_cluster(true) + .build() + .await; + + let (eth_signer, eth_address) = bridge_test_cluster + .get_eth_signer_and_address() + .await + .unwrap(); + + let sui_client = bridge_test_cluster.sui_client(); + let sui_bridge_client = bridge_test_cluster.sui_bridge_client().await.unwrap(); + let sui_address = bridge_test_cluster.sui_user_address(); + let amount = 42; + let sui_amount = amount * 100_000_000; + + initiate_bridge_eth_to_sui( + &sui_bridge_client, + ð_signer, + bridge_test_cluster.contracts().sui_bridge, + sui_address, + eth_address, + eth_chain_id, + sui_chain_id, + amount, + sui_amount, + TOKEN_ID_ETH, + 0, + ) + .await; + let events = bridge_test_cluster + .new_bridge_events( + HashSet::from_iter([ + TokenTransferApproved.get().unwrap().clone(), + TokenTransferClaimed.get().unwrap().clone(), + ]), + true, + ) + .await; + // There are exactly 1 approved and 1 claimed event + assert_eq!(events.len(), 2); + + let eth_coin = sui_client + .coin_read_api() + .get_all_coins(sui_address, None, None) + .await + .unwrap() + .data + .iter() + .find(|c| c.coin_type.contains("ETH")) + .expect("Recipient should have received ETH coin now") + .clone(); + assert_eq!(eth_coin.balance, sui_amount); + + // Now let the recipient send the coin back to ETH + let eth_address_1 = EthAddress::random(); + let bridge_obj_arg = sui_bridge_client + .get_mutable_bridge_object_arg_must_succeed() + .await; + let nonce = 0; + + let sui_token_type_tags = sui_bridge_client.get_token_id_map().await.unwrap(); + + let sui_to_eth_bridge_action = initiate_bridge_sui_to_eth( + &sui_bridge_client, + &sui_client, + sui_address, + bridge_test_cluster.wallet_mut(), + eth_chain_id, + sui_chain_id, + eth_address_1, + eth_coin.object_ref(), + nonce, + bridge_obj_arg, + sui_amount, + &sui_token_type_tags, + ) + .await; + let events = bridge_test_cluster + .new_bridge_events( + HashSet::from_iter([ + SuiToEthTokenBridgeV1.get().unwrap().clone(), + TokenTransferApproved.get().unwrap().clone(), + TokenTransferClaimed.get().unwrap().clone(), + ]), + true, + ) + .await; + // There are exactly 1 deposit and 1 approved event + assert_eq!(events.len(), 2); + let message = eth_sui_bridge::Message::from(sui_to_eth_bridge_action); + + let signatures = get_signatures( + &sui_bridge_client, + nonce, + sui_chain_id, + &sui_client, + message.message_type, + ) + .await; + + let eth_sui_bridge = EthSuiBridge::new( + bridge_test_cluster.contracts().sui_bridge, + eth_signer.clone().into(), + ); + let tx = eth_sui_bridge.transfer_bridged_tokens_with_signatures(signatures, message); + let _eth_claim_tx_receipt = tx.send().await.unwrap().await.unwrap().unwrap(); + info!("Sui to Eth bridge transfer claimed"); + // Assert eth_address_1 has received ETH + assert_eq!( + eth_signer.get_balance(eth_address_1, None).await.unwrap(), + U256::from(amount) * U256::exp10(18) + ); +} + +#[tokio::test] +async fn test_add_new_coins_on_sui() { + telemetry_subscribers::init_for_testing(); + let mut bridge_test_cluster = BridgeTestClusterBuilder::new() + .with_eth_env(true) + .with_bridge_cluster(false) + .build() + .await; + + let bridge_arg = bridge_test_cluster.get_mut_bridge_arg().await.unwrap(); + + // Register tokens + let token_id = 42; + let token_price = 10000; + let sender = bridge_test_cluster.sui_user_address(); + let tx = bridge_test_cluster + .test_transaction_builder_with_sender(sender) + .await + .publish(Path::new("../../bridge/move/tokens/mock/ka").into()) + .build(); + let publish_token_response = bridge_test_cluster.sign_and_execute_transaction(&tx).await; + info!("Published new token"); + let action = publish_coins_return_add_coins_on_sui_action( + bridge_test_cluster.wallet_mut(), + bridge_arg, + vec![publish_token_response], + vec![token_id], + vec![token_price], + 1, // seq num + ) + .await; + + let sui_bridge_client = bridge_test_cluster.sui_bridge_client().await.unwrap(); + + info!("Starting bridge cluster"); + + bridge_test_cluster.set_approved_governance_actions_for_next_start(vec![ + vec![action.clone()], + vec![action.clone()], + vec![action.clone()], + vec![], + ]); + bridge_test_cluster.start_bridge_cluster().await; + bridge_test_cluster + .wait_for_bridge_cluster_to_be_up(10) + .await; + info!("Bridge cluster is up"); + + let bridge_committee = Arc::new( + sui_bridge_client + .get_bridge_committee() + .await + .expect("Failed to get bridge committee"), + ); + let agg = BridgeAuthorityAggregator::new(bridge_committee); + let threshold = action.approval_threshold(); + let certified_action = agg + .request_committee_signatures(action, threshold) + .await + .expect("Failed to request committee signatures"); + + let tx = build_add_tokens_on_sui_transaction( + sender, + &bridge_test_cluster + .wallet() + .get_one_gas_object_owned_by_address(sender) + .await + .unwrap() + .unwrap(), + certified_action, + bridge_arg, + ) + .unwrap(); + + let response = bridge_test_cluster.sign_and_execute_transaction(&tx).await; + assert_eq!( + response.effects.unwrap().status(), + &SuiExecutionStatus::Success + ); + info!("Approved new token"); + + // Assert new token is correctly added + let treasury_summary = sui_bridge_client.get_treasury_summary().await.unwrap(); + assert_eq!(treasury_summary.id_token_type_map.len(), 5); // 4 + 1 new token + let (id, _type) = treasury_summary + .id_token_type_map + .iter() + .find(|(id, _)| id == &token_id) + .unwrap(); + let (_type, metadata) = treasury_summary + .supported_tokens + .iter() + .find(|(_type_, _)| _type == _type_) + .unwrap(); + assert_eq!( + metadata, + &BridgeTokenMetadata { + id: *id, + decimal_multiplier: 1_000_000_000, + notional_value: token_price, + native_token: false, + } + ); +} + +pub(crate) async fn deposit_native_eth_to_sol_contract( + signer: &EthSigner, + contract_address: EthAddress, + sui_recipient_address: SuiAddress, + sui_chain_id: u8, + amount: u64, +) -> ContractCall { + let contract = EthSuiBridge::new(contract_address, signer.clone().into()); + let sui_recipient_address = sui_recipient_address.to_vec().into(); + let amount = U256::from(amount) * U256::exp10(18); // 1 ETH + contract + .bridge_eth(sui_recipient_address, sui_chain_id) + .value(amount) +} + +async fn deposit_eth_to_sui_package( + sui_client: &SuiClient, + sui_address: SuiAddress, + wallet_context: &mut WalletContext, + target_chain: u8, + target_address: EthAddress, + token: ObjectRef, + bridge_object_arg: ObjectArg, + sui_token_type_tags: &HashMap, +) -> SuiTransactionBlockResponse { + let mut builder = ProgrammableTransactionBuilder::new(); + let arg_target_chain = builder.pure(target_chain).unwrap(); + let arg_target_address = builder.pure(target_address.as_bytes()).unwrap(); + let arg_token = builder.obj(ObjectArg::ImmOrOwnedObject(token)).unwrap(); + let arg_bridge = builder.obj(bridge_object_arg).unwrap(); + + builder.programmable_move_call( + BRIDGE_PACKAGE_ID, + BRIDGE_MODULE_NAME.to_owned(), + ident_str!("send_token").to_owned(), + vec![sui_token_type_tags.get(&TOKEN_ID_ETH).unwrap().clone()], + vec![arg_bridge, arg_target_chain, arg_target_address, arg_token], + ); + + let pt = builder.finish(); + let gas_object_ref = wallet_context + .get_one_gas_object_owned_by_address(sui_address) + .await + .unwrap() + .unwrap(); + let tx_data = TransactionData::new_programmable( + sui_address, + vec![gas_object_ref], + pt, + 500_000_000, + sui_client + .governance_api() + .get_reference_gas_price() + .await + .unwrap(), + ); + let tx = wallet_context.sign_transaction(&tx_data); + wallet_context.execute_transaction_must_succeed(tx).await +} + +async fn initiate_bridge_eth_to_sui( + sui_bridge_client: &SuiBridgeClient, + eth_signer: &EthSigner, + sui_bridge_contract_address: EthAddress, + sui_address: SuiAddress, + eth_address: EthAddress, + eth_chain_id: u8, + sui_chain_id: u8, + amount: u64, + sui_amount: u64, + token_id: u8, + nonce: u64, +) { + let eth_tx = deposit_native_eth_to_sol_contract( + eth_signer, + sui_bridge_contract_address, + sui_address, + sui_chain_id, + amount, + ) + .await; + let pending_tx = eth_tx.send().await.unwrap(); + let tx_receipt = pending_tx.await.unwrap().unwrap(); + let eth_bridge_event = tx_receipt + .logs + .iter() + .find_map(EthBridgeEvent::try_from_log) + .unwrap(); + let EthBridgeEvent::EthSuiBridgeEvents(EthSuiBridgeEvents::TokensDepositedFilter( + eth_bridge_event, + )) = eth_bridge_event + else { + unreachable!(); + }; + // assert eth log matches + assert_eq!(eth_bridge_event.source_chain_id, eth_chain_id); + assert_eq!(eth_bridge_event.nonce, nonce); + assert_eq!(eth_bridge_event.destination_chain_id, sui_chain_id); + assert_eq!(eth_bridge_event.token_id, token_id); + assert_eq!(eth_bridge_event.sui_adjusted_amount, sui_amount); + assert_eq!(eth_bridge_event.sender_address, eth_address); + assert_eq!(eth_bridge_event.recipient_address, sui_address.to_vec()); + info!("Deposited Eth to Sol contract"); + + wait_for_transfer_action_status( + sui_bridge_client, + eth_chain_id, + 0, + BridgeActionStatus::Claimed, + ) + .await; + info!("Eth to Sui bridge transfer claimed"); +} + +async fn initiate_bridge_sui_to_eth( + sui_bridge_client: &SuiBridgeClient, + sui_client: &SuiClient, + sui_address: SuiAddress, + wallet_context: &mut WalletContext, + eth_chain_id: u8, + sui_chain_id: u8, + eth_address: EthAddress, + token: ObjectRef, + nonce: u64, + bridge_object_arg: ObjectArg, + sui_amount: u64, + sui_token_type_tags: &HashMap, +) -> SuiToEthBridgeAction { + let resp = deposit_eth_to_sui_package( + sui_client, + sui_address, + wallet_context, + eth_chain_id, + eth_address, + token, + bridge_object_arg, + sui_token_type_tags, + ) + .await; + let sui_events = resp.events.unwrap().data; + let bridge_event = sui_events + .iter() + .filter_map(|e| { + let sui_bridge_event = SuiBridgeEvent::try_from_sui_event(e).unwrap()?; + sui_bridge_event.try_into_bridge_action(e.id.tx_digest, e.id.event_seq as u16) + }) + .find_map(|e| { + if let BridgeAction::SuiToEthBridgeAction(a) = e { + Some(a) + } else { + None + } + }) + .unwrap(); + info!("Deposited Eth to move package"); + assert_eq!(bridge_event.sui_bridge_event.nonce, nonce); + assert_eq!( + bridge_event.sui_bridge_event.sui_chain_id as u8, + sui_chain_id + ); + assert_eq!( + bridge_event.sui_bridge_event.eth_chain_id as u8, + eth_chain_id + ); + assert_eq!(bridge_event.sui_bridge_event.sui_address, sui_address); + assert_eq!(bridge_event.sui_bridge_event.eth_address, eth_address); + assert_eq!(bridge_event.sui_bridge_event.token_id, TOKEN_ID_ETH); + assert_eq!( + bridge_event.sui_bridge_event.amount_sui_adjusted, + sui_amount + ); + + // Wait for the bridge action to be approved + wait_for_transfer_action_status( + sui_bridge_client, + sui_chain_id, + nonce, + BridgeActionStatus::Approved, + ) + .await; + info!("Sui to Eth bridge transfer approved"); + + bridge_event +} + +async fn wait_for_transfer_action_status( + sui_bridge_client: &SuiBridgeClient, + chain_id: u8, + nonce: u64, + status: BridgeActionStatus, +) { + // Wait for the bridge action to be approved + let now = std::time::Instant::now(); + loop { + let res = sui_bridge_client + .get_token_transfer_action_onchain_status_until_success(chain_id, nonce) + .await; + if res == status { + break; + } + if now.elapsed().as_secs() > 30 { + panic!( + "Timeout waiting for token transfer action to be {:?}. chain_id: {chain_id}, nonce: {nonce}", + status + ); + } + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + } +} diff --git a/crates/sui-bridge/src/e2e_tests/mod.rs b/crates/sui-bridge/src/e2e_tests/mod.rs new file mode 100644 index 0000000000000..0f30720708fdd --- /dev/null +++ b/crates/sui-bridge/src/e2e_tests/mod.rs @@ -0,0 +1,5 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +mod basic; +pub mod test_utils; diff --git a/crates/sui-bridge/src/e2e_tests/test_utils.rs b/crates/sui-bridge/src/e2e_tests/test_utils.rs new file mode 100644 index 0000000000000..02fdffb4e00a4 --- /dev/null +++ b/crates/sui-bridge/src/e2e_tests/test_utils.rs @@ -0,0 +1,879 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::abi::EthBridgeCommittee; +use crate::crypto::BridgeAuthorityKeyPair; +use crate::crypto::BridgeAuthorityPublicKeyBytes; +use crate::events::*; +use crate::server::APPLICATION_JSON; +use crate::types::{AddTokensOnSuiAction, BridgeAction}; +use crate::utils::get_eth_signer_client; +use crate::utils::EthSigner; +use ethers::types::Address as EthAddress; +use move_core_types::language_storage::StructTag; +use rand::rngs::SmallRng; +use rand::{Rng, SeedableRng}; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use std::collections::HashSet; +use std::fs::File; +use std::fs::{self, DirBuilder}; +use std::io::{Read, Write}; +use std::path::Path; +use std::path::PathBuf; +use std::process::Command; +use std::str::FromStr; +use sui_json_rpc_types::SuiEvent; +use sui_json_rpc_types::SuiTransactionBlockResponse; +use sui_json_rpc_types::SuiTransactionBlockResponseOptions; +use sui_json_rpc_types::SuiTransactionBlockResponseQuery; +use sui_json_rpc_types::TransactionFilter; +use sui_sdk::wallet_context::WalletContext; +use sui_test_transaction_builder::TestTransactionBuilder; +use sui_types::bridge::{ + BridgeChainId, BRIDGE_MODULE_NAME, BRIDGE_REGISTER_FOREIGN_TOKEN_FUNCTION_NAME, +}; +use sui_types::committee::TOTAL_VOTING_POWER; +use sui_types::crypto::get_key_pair; +use sui_types::digests::TransactionDigest; +use sui_types::programmable_transaction_builder::ProgrammableTransactionBuilder; +use sui_types::transaction::{ObjectArg, TransactionData}; +use sui_types::BRIDGE_PACKAGE_ID; +use sui_types::SUI_BRIDGE_OBJECT_ID; +use tokio::join; +use tokio::task::JoinHandle; + +use tracing::error; +use tracing::info; + +use crate::config::{BridgeNodeConfig, EthConfig, SuiConfig}; +use crate::node::run_bridge_node; +use crate::sui_client::SuiBridgeClient; +use crate::BRIDGE_ENABLE_PROTOCOL_VERSION; +use ethers::prelude::*; +use std::process::Child; +use sui_config::local_ip_utils::get_available_port; +use sui_json_rpc_types::SuiObjectDataOptions; +use sui_sdk::SuiClient; +use sui_types::base_types::SuiAddress; +use sui_types::bridge::{MoveTypeBridgeMessageKey, MoveTypeBridgeRecord}; +use sui_types::collection_types::LinkedTableNode; +use sui_types::crypto::EncodeDecodeBase64; +use sui_types::crypto::KeypairTraits; +use sui_types::dynamic_field::{DynamicFieldName, Field}; +use sui_types::object::Object; +use sui_types::TypeTag; +use tempfile::tempdir; +use test_cluster::TestCluster; +use test_cluster::TestClusterBuilder; + +const BRIDGE_COMMITTEE_NAME: &str = "BridgeCommittee"; +const SUI_BRIDGE_NAME: &str = "SuiBridge"; +const BRIDGE_CONFIG_NAME: &str = "BridgeConfig"; +const BRIDGE_LIMITER_NAME: &str = "BridgeLimiter"; +const BRIDGE_VAULT_NAME: &str = "BridgeVault"; +const BTC_NAME: &str = "BTC"; +const ETH_NAME: &str = "ETH"; +const USDC_NAME: &str = "USDC"; +const USDT_NAME: &str = "USDT"; + +pub const TEST_PK: &str = "0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356"; + +/// A helper struct that holds TestCluster and other Bridge related +/// structs that are needed for testing. +pub struct BridgeTestCluster { + pub test_cluster: TestCluster, + eth_environment: EthBridgeEnvironment, + bridge_node_handles: Option>>, + approved_governance_actions_for_next_start: Option>>, + bridge_tx_cursor: Option, +} + +pub struct BridgeTestClusterBuilder { + with_eth_env: bool, + with_bridge_cluster: bool, + approved_governance_actions: Option>>, +} + +impl Default for BridgeTestClusterBuilder { + fn default() -> Self { + Self::new() + } +} + +impl BridgeTestClusterBuilder { + pub fn new() -> Self { + BridgeTestClusterBuilder { + with_eth_env: false, + with_bridge_cluster: false, + approved_governance_actions: None, + } + } + + pub fn with_eth_env(mut self, with_eth_env: bool) -> Self { + self.with_eth_env = with_eth_env; + self + } + + pub fn with_bridge_cluster(mut self, with_bridge_cluster: bool) -> Self { + self.with_bridge_cluster = with_bridge_cluster; + self + } + + pub fn with_approved_governance_actions( + mut self, + approved_governance_actions: Vec>, + ) -> Self { + self.approved_governance_actions = Some(approved_governance_actions); + self + } + + pub async fn build(self) -> BridgeTestCluster { + init_all_struct_tags(); + let mut bridge_keys = vec![]; + let mut bridge_keys_copy = vec![]; + for _ in 0..=3 { + let (_, kp): (_, BridgeAuthorityKeyPair) = get_key_pair(); + bridge_keys.push(kp.copy()); + bridge_keys_copy.push(kp); + } + let start_cluster_task = tokio::task::spawn(Self::start_test_cluster(bridge_keys)); + let start_eth_env_task = tokio::task::spawn(Self::start_eth_env(bridge_keys_copy)); + let (start_cluster_res, start_eth_env_res) = join!(start_cluster_task, start_eth_env_task); + let test_cluster = start_cluster_res.unwrap(); + let eth_environment = start_eth_env_res.unwrap(); + + let mut bridge_node_handles = None; + if self.with_bridge_cluster { + let approved_governace_actions = self + .approved_governance_actions + .clone() + .unwrap_or(vec![vec![], vec![], vec![], vec![]]); + bridge_node_handles = Some( + start_bridge_cluster(&test_cluster, ð_environment, approved_governace_actions) + .await, + ); + } + + BridgeTestCluster { + test_cluster, + eth_environment, + bridge_node_handles, + approved_governance_actions_for_next_start: self.approved_governance_actions, + bridge_tx_cursor: None, + } + } + + async fn start_test_cluster(bridge_keys: Vec) -> TestCluster { + let test_cluster: test_cluster::TestCluster = TestClusterBuilder::new() + .with_protocol_version(BRIDGE_ENABLE_PROTOCOL_VERSION.into()) + .build_with_bridge(bridge_keys, true) + .await; + info!("Test cluster built"); + test_cluster + .trigger_reconfiguration_if_not_yet_and_assert_bridge_committee_initialized() + .await; + info!("Bridge committee is finalized"); + test_cluster + } + + async fn start_eth_env(bridge_keys: Vec) -> EthBridgeEnvironment { + let anvil_port = get_available_port("127.0.0.1"); + let anvil_url = format!("http://127.0.0.1:{anvil_port}"); + let mut eth_environment = EthBridgeEnvironment::new(&anvil_url, anvil_port) + .await + .unwrap(); + // Give anvil a bit of time to start + tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; + let (eth_signer, eth_pk_hex) = eth_environment + .get_signer(TEST_PK) + .await + .unwrap_or_else(|e| panic!("Failed to get eth signer from anvil at {anvil_url}: {e}")); + let deployed_contracts = + deploy_sol_contract(&anvil_url, eth_signer, bridge_keys, eth_pk_hex).await; + info!("Deployed contracts: {:?}", deployed_contracts); + eth_environment.contracts = Some(deployed_contracts); + eth_environment + } +} + +impl BridgeTestCluster { + pub async fn get_eth_signer_and_private_key(&self) -> anyhow::Result<(EthSigner, String)> { + self.eth_environment.get_signer(TEST_PK).await + } + + pub async fn get_eth_signer_and_address(&self) -> anyhow::Result<(EthSigner, EthAddress)> { + let (eth_signer, _) = self.get_eth_signer_and_private_key().await?; + let eth_address = eth_signer.address(); + Ok((eth_signer, eth_address)) + } + + pub async fn sui_bridge_client(&self) -> anyhow::Result { + SuiBridgeClient::new(&self.test_cluster.fullnode_handle.rpc_url).await + } + + pub fn sui_client(&self) -> SuiClient { + self.test_cluster.fullnode_handle.sui_client.clone() + } + + pub fn sui_user_address(&self) -> SuiAddress { + self.test_cluster.get_address_0() + } + + pub fn contracts(&self) -> &DeployedSolContracts { + self.eth_environment.contracts() + } + + pub fn sui_bridge_address(&self) -> String { + self.eth_environment.contracts().sui_bridge_addrress_hex() + } + + pub fn wallet_mut(&mut self) -> &mut WalletContext { + self.test_cluster.wallet_mut() + } + + pub fn wallet(&mut self) -> &WalletContext { + &self.test_cluster.wallet + } + + pub fn bridge_authority_key(&self, index: usize) -> BridgeAuthorityKeyPair { + self.test_cluster.bridge_authority_keys.as_ref().unwrap()[index].copy() + } + + pub fn sui_rpc_url(&self) -> String { + self.test_cluster.fullnode_handle.rpc_url.clone() + } + + pub fn eth_rpc_url(&self) -> String { + self.eth_environment.rpc_url.clone() + } + + pub async fn get_mut_bridge_arg(&self) -> Option { + self.test_cluster.get_mut_bridge_arg().await + } + + pub async fn test_transaction_builder_with_sender( + &self, + sender: SuiAddress, + ) -> TestTransactionBuilder { + self.test_cluster + .test_transaction_builder_with_sender(sender) + .await + } + + pub async fn wait_for_bridge_cluster_to_be_up(&self, timeout_sec: u64) { + self.test_cluster + .wait_for_bridge_cluster_to_be_up(timeout_sec) + .await; + } + + pub async fn sign_and_execute_transaction( + &self, + tx_data: &TransactionData, + ) -> SuiTransactionBlockResponse { + self.test_cluster + .sign_and_execute_transaction(tx_data) + .await + } + + pub fn set_approved_governance_actions_for_next_start( + &mut self, + approved_governance_actions: Vec>, + ) { + self.approved_governance_actions_for_next_start = Some(approved_governance_actions); + } + + pub async fn start_bridge_cluster(&mut self) { + assert!(self.bridge_node_handles.is_none()); + let approved_governace_actions = self + .approved_governance_actions_for_next_start + .clone() + .unwrap_or(vec![vec![], vec![], vec![], vec![]]); + self.bridge_node_handles = Some( + start_bridge_cluster( + &self.test_cluster, + &self.eth_environment, + approved_governace_actions, + ) + .await, + ); + } + + /// Returns new bridge transaction. It advanaces the stored tx digest cursor. + /// When `assert_success` is true, it asserts all transactions are successful. + pub async fn new_bridge_transactions( + &mut self, + assert_success: bool, + ) -> Vec { + let resps = self + .sui_client() + .read_api() + .query_transaction_blocks( + SuiTransactionBlockResponseQuery { + filter: Some(TransactionFilter::InputObject(SUI_BRIDGE_OBJECT_ID)), + options: Some(SuiTransactionBlockResponseOptions::full_content()), + }, + self.bridge_tx_cursor, + None, + false, + ) + .await + .unwrap(); + self.bridge_tx_cursor = resps.next_cursor; + + for tx in &resps.data { + if assert_success { + assert!(tx.status_ok().unwrap()); + } + let events = &tx.events.as_ref().unwrap().data; + if events + .iter() + .any(|e| &e.type_ == TokenTransferApproved.get().unwrap()) + { + assert!(events + .iter() + .any(|e| &e.type_ == TokenTransferClaimed.get().unwrap() + || &e.type_ == TokenTransferApproved.get().unwrap())); + } else if events + .iter() + .any(|e| &e.type_ == TokenTransferAlreadyClaimed.get().unwrap()) + { + assert!(events + .iter() + .all(|e| &e.type_ == TokenTransferAlreadyClaimed.get().unwrap() + || &e.type_ == TokenTransferAlreadyApproved.get().unwrap())); + } + // TODO: check for other events e.g. TokenRegistrationEvent, NewTokenEvent etc + } + resps.data + } + + /// Returns events that are emitted in new bridge transaction and match `event_types`. + /// It advanaces the stored tx digest cursor. + /// See `new_bridge_transactions` for `assert_success`. + pub async fn new_bridge_events( + &mut self, + event_types: HashSet, + assert_success: bool, + ) -> Vec { + let txes = self.new_bridge_transactions(assert_success).await; + let events = txes + .iter() + .flat_map(|tx| { + tx.events + .as_ref() + .unwrap() + .data + .iter() + .filter(|e| event_types.contains(&e.type_)) + .cloned() + }) + .collect(); + events + } +} + +pub async fn publish_coins_return_add_coins_on_sui_action( + wallet_context: &mut WalletContext, + bridge_arg: ObjectArg, + publish_coin_responses: Vec, + token_ids: Vec, + token_prices: Vec, + nonce: u64, +) -> BridgeAction { + assert!(token_ids.len() == publish_coin_responses.len()); + assert!(token_prices.len() == publish_coin_responses.len()); + let sender = wallet_context.active_address().unwrap(); + let rgp = wallet_context.get_reference_gas_price().await.unwrap(); + let mut token_type_names = vec![]; + for response in publish_coin_responses { + let object_changes = response.object_changes.unwrap(); + let mut tc = None; + let mut type_ = None; + let mut uc = None; + let mut metadata = None; + for object_change in &object_changes { + if let o @ sui_json_rpc_types::ObjectChange::Created { object_type, .. } = object_change + { + if object_type.name.as_str().starts_with("TreasuryCap") { + assert!(tc.is_none() && type_.is_none()); + tc = Some(o.clone()); + type_ = Some(object_type.type_params.first().unwrap().clone()); + } else if object_type.name.as_str().starts_with("UpgradeCap") { + assert!(uc.is_none()); + uc = Some(o.clone()); + } else if object_type.name.as_str().starts_with("CoinMetadata") { + assert!(metadata.is_none()); + metadata = Some(o.clone()); + } + } + } + let (tc, type_, uc, metadata) = + (tc.unwrap(), type_.unwrap(), uc.unwrap(), metadata.unwrap()); + + // register with the bridge + let mut builder = ProgrammableTransactionBuilder::new(); + let bridge_arg = builder.obj(bridge_arg).unwrap(); + let uc_arg = builder + .obj(ObjectArg::ImmOrOwnedObject(uc.object_ref())) + .unwrap(); + let tc_arg = builder + .obj(ObjectArg::ImmOrOwnedObject(tc.object_ref())) + .unwrap(); + let metadata_arg = builder + .obj(ObjectArg::ImmOrOwnedObject(metadata.object_ref())) + .unwrap(); + builder.programmable_move_call( + BRIDGE_PACKAGE_ID, + BRIDGE_MODULE_NAME.into(), + BRIDGE_REGISTER_FOREIGN_TOKEN_FUNCTION_NAME.into(), + vec![type_.clone()], + vec![bridge_arg, tc_arg, uc_arg, metadata_arg], + ); + let pt = builder.finish(); + let gas = wallet_context + .get_one_gas_object_owned_by_address(sender) + .await + .unwrap() + .unwrap(); + let tx = TransactionData::new_programmable(sender, vec![gas], pt, 1_000_000_000, rgp); + let signed_tx = wallet_context.sign_transaction(&tx); + let _ = wallet_context + .execute_transaction_must_succeed(signed_tx) + .await; + + token_type_names.push(type_); + } + + BridgeAction::AddTokensOnSuiAction(AddTokensOnSuiAction { + nonce, + chain_id: BridgeChainId::SuiCustom, + native: false, + token_ids, + token_type_names, + token_prices, + }) +} + +pub async fn wait_for_server_to_be_up(server_url: String, timeout_sec: u64) -> anyhow::Result<()> { + let now = std::time::Instant::now(); + loop { + if let Ok(true) = reqwest::Client::new() + .get(server_url.clone()) + .header(reqwest::header::ACCEPT, APPLICATION_JSON) + .send() + .await + .map(|res| res.status().is_success()) + { + break; + } + if now.elapsed().as_secs() > timeout_sec { + anyhow::bail!("Server is not up and running after {} seconds", timeout_sec); + } + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + } + Ok(()) +} + +pub async fn get_eth_signer_client_e2e_test_only( + eth_rpc_url: &str, +) -> anyhow::Result<(EthSigner, String)> { + // This private key is derived from the default anvil setting. + // Mnemonic: test test test test test test test test test test test junk + // Derivation path: m/44'/60'/0'/0/ + // DO NOT USE IT ANYWHERE ELSE EXCEPT FOR RUNNING AUTOMATIC INTEGRATION TESTING + let url = eth_rpc_url.to_string(); + let private_key_0 = "0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356"; + let signer_0 = get_eth_signer_client(&url, private_key_0).await?; + Ok((signer_0, private_key_0.to_string())) +} + +#[allow(dead_code)] +#[derive(Debug, Clone)] +pub struct DeployedSolContracts { + pub sui_bridge: EthAddress, + pub bridge_committee: EthAddress, + pub bridge_limiter: EthAddress, + pub bridge_vault: EthAddress, + pub bridge_config: EthAddress, + pub btc: EthAddress, + pub eth: EthAddress, + pub usdc: EthAddress, + pub usdt: EthAddress, +} + +impl DeployedSolContracts { + pub fn eth_adress_to_hex(addr: EthAddress) -> String { + format!("{:x}", addr) + } + + pub fn sui_bridge_addrress_hex(&self) -> String { + Self::eth_adress_to_hex(self.sui_bridge) + } +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct SolDeployConfig { + committee_member_stake: Vec, + committee_members: Vec, + min_committee_stake_required: u64, + source_chain_id: u64, + supported_chain_ids: Vec, + supported_chain_limits_in_dollars: Vec, + supported_tokens: Vec, + token_prices: Vec, +} + +pub(crate) async fn deploy_sol_contract( + anvil_url: &str, + eth_signer: EthSigner, + bridge_authority_keys: Vec, + eth_private_key_hex: String, +) -> DeployedSolContracts { + let sol_path = format!("{}/../../bridge/evm", env!("CARGO_MANIFEST_DIR")); + + // Write the deploy config to a temp file then provide it to the forge late + let deploy_config_path = tempfile::tempdir() + .unwrap() + .into_path() + .join("sol_deploy_config.json"); + let node_len = bridge_authority_keys.len(); + let stake = TOTAL_VOTING_POWER / (node_len as u64); + let committee_members = bridge_authority_keys + .iter() + .map(|k| { + format!( + "0x{:x}", + BridgeAuthorityPublicKeyBytes::from(&k.public).to_eth_address() + ) + }) + .collect::>(); + let committee_member_stake = vec![stake; node_len]; + let deploy_config = SolDeployConfig { + committee_member_stake: committee_member_stake.clone(), + committee_members: committee_members.clone(), + min_committee_stake_required: 10000, + source_chain_id: 12, + supported_chain_ids: vec![1, 2, 3], + supported_chain_limits_in_dollars: vec![ + 1000000000000000, + 1000000000000000, + 1000000000000000, + ], + supported_tokens: vec![], // this is set up in the deploy script + token_prices: vec![12800, 432518900, 25969600, 10000, 10000], + }; + + let serialized_config = serde_json::to_string_pretty(&deploy_config).unwrap(); + tracing::debug!( + "Serialized config written to {:?}: {:?}", + deploy_config_path, + serialized_config + ); + let mut file = File::create(deploy_config_path.clone()).unwrap(); + file.write_all(serialized_config.as_bytes()).unwrap(); + + // override for the deploy script + std::env::set_var("OVERRIDE_CONFIG_PATH", deploy_config_path.to_str().unwrap()); + std::env::set_var("PRIVATE_KEY", eth_private_key_hex); + std::env::set_var("ETHERSCAN_API_KEY", "n/a"); + + // We provide a unique out path for each run to avoid conflicts + let mut rng = SmallRng::from_entropy(); + let random_number = rng.gen::(); + let forge_out_path = PathBuf::from(format!("out-{random_number}")); + let _dir = TempDir::new( + PathBuf::from(sol_path.clone()) + .join(forge_out_path.clone()) + .as_path(), + ) + .unwrap(); + std::env::set_var("FOUNDRY_OUT", forge_out_path.to_str().unwrap()); + + info!("Deploying solidity contracts"); + Command::new("forge") + .current_dir(sol_path.clone()) + .arg("clean") + .status() + .expect("Failed to execute `forge clean`"); + + let mut child = Command::new("forge") + .current_dir(sol_path) + .arg("script") + .arg("script/deploy_bridge.s.sol") + .arg("--fork-url") + .arg(anvil_url) + .arg("--broadcast") + .arg("--ffi") + .arg("--chain") + .arg("31337") + .stdout(std::process::Stdio::piped()) // Capture stdout + .stderr(std::process::Stdio::piped()) // Capture stderr + .spawn() + .unwrap(); + + let mut stdout = child.stdout.take().expect("Failed to open stdout"); + let mut stderr = child.stderr.take().expect("Failed to open stderr"); + + // Read stdout/stderr to String + let mut s = String::new(); + stdout.read_to_string(&mut s).unwrap(); + let mut e = String::new(); + stderr.read_to_string(&mut e).unwrap(); + + // Wait for the child process to finish and collect its status + let status = child.wait().unwrap(); + if status.success() { + info!("Solidity contract deployment finished successfully"); + } else { + error!( + "Solidity contract deployment exited with code: {:?}", + status.code() + ); + } + println!("Stdout: {}", s); + println!("Stdout: {}", e); + + let mut deployed_contracts = BTreeMap::new(); + // Process the stdout to parse contract addresses + for line in s.lines() { + if line.contains("[Deployed]") { + let replaced_line = line.replace("[Deployed]", ""); + let trimmed_line = replaced_line.trim(); + let parts: Vec<&str> = trimmed_line.split(':').collect(); + if parts.len() == 2 { + let contract_name = parts[0].to_string().trim().to_string(); + let contract_address = EthAddress::from_str(parts[1].to_string().trim()).unwrap(); + deployed_contracts.insert(contract_name, contract_address); + } + } + } + + let contracts = DeployedSolContracts { + sui_bridge: *deployed_contracts.get(SUI_BRIDGE_NAME).unwrap(), + bridge_committee: *deployed_contracts.get(BRIDGE_COMMITTEE_NAME).unwrap(), + bridge_config: *deployed_contracts.get(BRIDGE_CONFIG_NAME).unwrap(), + bridge_limiter: *deployed_contracts.get(BRIDGE_LIMITER_NAME).unwrap(), + bridge_vault: *deployed_contracts.get(BRIDGE_VAULT_NAME).unwrap(), + btc: *deployed_contracts.get(BTC_NAME).unwrap(), + eth: *deployed_contracts.get(ETH_NAME).unwrap(), + usdc: *deployed_contracts.get(USDC_NAME).unwrap(), + usdt: *deployed_contracts.get(USDT_NAME).unwrap(), + }; + let eth_bridge_committee = + EthBridgeCommittee::new(contracts.bridge_committee, eth_signer.clone().into()); + for (i, (m, s)) in committee_members + .iter() + .zip(committee_member_stake.iter()) + .enumerate() + { + let eth_address = EthAddress::from_str(m).unwrap(); + assert_eq!( + eth_bridge_committee + .committee_index(eth_address) + .await + .unwrap(), + i as u8 + ); + assert_eq!( + eth_bridge_committee + .committee_stake(eth_address) + .await + .unwrap(), + *s as u16 + ); + assert!(!eth_bridge_committee.blocklist(eth_address).await.unwrap()); + } + contracts +} + +#[derive(Debug)] +pub(crate) struct EthBridgeEnvironment { + pub rpc_url: String, + process: Child, + contracts: Option, +} + +impl EthBridgeEnvironment { + async fn new(anvil_url: &str, anvil_port: u16) -> anyhow::Result { + // Start eth node with anvil + let eth_environment_process = std::process::Command::new("anvil") + .arg("--port") + .arg(anvil_port.to_string()) + .arg("--block-time") + .arg("1") // 1 second block time + .arg("--slots-in-an-epoch") + .arg("3") // 3 slots in an epoch + .spawn() + .expect("Failed to start anvil"); + + Ok(EthBridgeEnvironment { + rpc_url: anvil_url.to_string(), + process: eth_environment_process, + contracts: None, + }) + } + + pub(crate) async fn get_signer( + &self, + private_key: &str, + ) -> anyhow::Result<(EthSigner, String)> { + let signer = get_eth_signer_client(&self.rpc_url, private_key).await?; + Ok((signer, private_key.to_string())) + } + + pub(crate) fn contracts(&self) -> &DeployedSolContracts { + self.contracts.as_ref().unwrap() + } +} + +impl Drop for EthBridgeEnvironment { + fn drop(&mut self) { + self.process.kill().unwrap(); + } +} + +pub(crate) async fn start_bridge_cluster( + test_cluster: &TestCluster, + eth_environment: &EthBridgeEnvironment, + approved_governance_actions: Vec>, +) -> Vec> { + let bridge_authority_keys = test_cluster + .bridge_authority_keys + .as_ref() + .unwrap() + .iter() + .map(|k| k.copy()) + .collect::>(); + let bridge_server_ports = test_cluster.bridge_server_ports.as_ref().unwrap(); + assert_eq!(bridge_authority_keys.len(), bridge_server_ports.len()); + assert_eq!( + bridge_authority_keys.len(), + approved_governance_actions.len() + ); + + let eth_bridge_contract_address = eth_environment + .contracts + .as_ref() + .unwrap() + .sui_bridge_addrress_hex(); + + let mut handles = vec![]; + for (i, ((kp, server_listen_port), approved_governance_actions)) in bridge_authority_keys + .iter() + .zip(bridge_server_ports.iter()) + .zip(approved_governance_actions.into_iter()) + .enumerate() + { + // prepare node config (server + client) + let tmp_dir = tempdir().unwrap().into_path().join(i.to_string()); + std::fs::create_dir_all(tmp_dir.clone()).unwrap(); + let db_path = tmp_dir.join("client_db"); + // write authority key to file + let authority_key_path = tmp_dir.join("bridge_authority_key"); + let base64_encoded = kp.encode_base64(); + std::fs::write(authority_key_path.clone(), base64_encoded).unwrap(); + + let client_sui_address = SuiAddress::from(kp.public()); + let sender_address = test_cluster.get_address_0(); + // send some gas to this address + test_cluster + .transfer_sui_must_exceed(sender_address, client_sui_address, 1000000000) + .await; + + let config = BridgeNodeConfig { + server_listen_port: *server_listen_port, + metrics_port: get_available_port("127.0.0.1"), + bridge_authority_key_path_base64_raw: authority_key_path, + approved_governance_actions, + run_client: true, + db_path: Some(db_path), + eth: EthConfig { + eth_rpc_url: eth_environment.rpc_url.clone(), + eth_bridge_proxy_address: eth_bridge_contract_address.clone(), + eth_bridge_chain_id: BridgeChainId::EthCustom as u8, + eth_contracts_start_block_fallback: Some(0), + eth_contracts_start_block_override: None, + }, + sui: SuiConfig { + sui_rpc_url: test_cluster.fullnode_handle.rpc_url.clone(), + sui_bridge_chain_id: BridgeChainId::SuiCustom as u8, + bridge_client_key_path_base64_sui_key: None, + bridge_client_gas_object: None, + sui_bridge_module_last_processed_event_id_override: None, + }, + }; + // Spawn bridge node in memory + let config_clone = config.clone(); + handles.push(run_bridge_node(config_clone).await.unwrap()); + } + handles +} + +pub(crate) async fn get_signatures( + sui_bridge_client: &SuiBridgeClient, + nonce: u64, + sui_chain_id: u8, + sui_client: &SuiClient, + message_type: u8, +) -> Vec { + // Now collect sigs from the bridge record and submit to eth to claim + let summary = sui_bridge_client.get_bridge_summary().await.unwrap(); + let records_id = summary.bridge_records_id; + let key = serde_json::json!( + { + // u64 is represented as string + "bridge_seq_num": nonce.to_string(), + "message_type": message_type, + "source_chain": sui_chain_id, + } + ); + let status_object_id = sui_client.read_api().get_dynamic_field_object(records_id, + DynamicFieldName { + type_: TypeTag::from_str("0x000000000000000000000000000000000000000000000000000000000000000b::message::BridgeMessageKey").unwrap(), + value: key.clone(), + }, + ).await.unwrap().into_object().unwrap().object_id; + + let object_resp = sui_client + .read_api() + .get_object_with_options( + status_object_id, + SuiObjectDataOptions::full_content().with_bcs(), + ) + .await + .unwrap(); + + let object: Object = object_resp.into_object().unwrap().try_into().unwrap(); + let record: Field< + MoveTypeBridgeMessageKey, + LinkedTableNode, + > = object.to_rust().unwrap(); + let sigs = record.value.value.verified_signatures.unwrap(); + + sigs.into_iter() + .map(|sig: Vec| Bytes::from(sig)) + .collect() +} + +/// A simple struct to create a temporary directory that +/// will be removed when it goes out of scope. +struct TempDir { + path: PathBuf, +} + +impl TempDir { + fn new(dir_path: &Path) -> std::io::Result { + DirBuilder::new().recursive(true).create(dir_path)?; + Ok(TempDir { + path: dir_path.to_path_buf(), + }) + } +} + +impl Drop for TempDir { + fn drop(&mut self) { + fs::remove_dir_all(&self.path).unwrap(); + } +} diff --git a/crates/sui-bridge/src/encoding.rs b/crates/sui-bridge/src/encoding.rs new file mode 100644 index 0000000000000..3e3d38c449611 --- /dev/null +++ b/crates/sui-bridge/src/encoding.rs @@ -0,0 +1,1009 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::types::AddTokensOnEvmAction; +use crate::types::AddTokensOnSuiAction; +use crate::types::AssetPriceUpdateAction; +use crate::types::BlocklistCommitteeAction; +use crate::types::BridgeAction; +use crate::types::BridgeActionType; +use crate::types::EmergencyAction; +use crate::types::EthToSuiBridgeAction; +use crate::types::EvmContractUpgradeAction; +use crate::types::LimitUpdateAction; +use crate::types::SuiToEthBridgeAction; +use enum_dispatch::enum_dispatch; +use ethers::types::Address as EthAddress; +use sui_types::base_types::SUI_ADDRESS_LENGTH; + +pub const TOKEN_TRANSFER_MESSAGE_VERSION: u8 = 1; +pub const COMMITTEE_BLOCKLIST_MESSAGE_VERSION: u8 = 1; +pub const EMERGENCY_BUTTON_MESSAGE_VERSION: u8 = 1; +pub const LIMIT_UPDATE_MESSAGE_VERSION: u8 = 1; +pub const ASSET_PRICE_UPDATE_MESSAGE_VERSION: u8 = 1; +pub const EVM_CONTRACT_UPGRADE_MESSAGE_VERSION: u8 = 1; +pub const ADD_TOKENS_ON_SUI_MESSAGE_VERSION: u8 = 1; +pub const ADD_TOKENS_ON_EVM_MESSAGE_VERSION: u8 = 1; + +pub const BRIDGE_MESSAGE_PREFIX: &[u8] = b"SUI_BRIDGE_MESSAGE"; + +/// Encoded bridge message consists of the following fields: +/// 1. Message type (1 byte) +/// 2. Message version (1 byte) +/// 3. Nonce (8 bytes in big endian) +/// 4. Chain id (1 byte) +/// 4. Payload (variable length) +#[enum_dispatch] +pub trait BridgeMessageEncoding { + /// Convert the entire message to bytes + fn as_bytes(&self) -> Vec; + /// Convert the payload piece to bytes + fn as_payload_bytes(&self) -> Vec; +} + +impl BridgeMessageEncoding for SuiToEthBridgeAction { + fn as_bytes(&self) -> Vec { + let mut bytes = Vec::new(); + let e = &self.sui_bridge_event; + // Add message type + bytes.push(BridgeActionType::TokenTransfer as u8); + // Add message version + bytes.push(TOKEN_TRANSFER_MESSAGE_VERSION); + // Add nonce + bytes.extend_from_slice(&e.nonce.to_be_bytes()); + // Add source chain id + bytes.push(e.sui_chain_id as u8); + + // Add payload bytes + bytes.extend_from_slice(&self.as_payload_bytes()); + + bytes + } + + fn as_payload_bytes(&self) -> Vec { + let mut bytes = Vec::new(); + let e = &self.sui_bridge_event; + + // Add source address length + bytes.push(SUI_ADDRESS_LENGTH as u8); + // Add source address + bytes.extend_from_slice(&e.sui_address.to_vec()); + // Add dest chain id + bytes.push(e.eth_chain_id as u8); + // Add dest address length + bytes.push(EthAddress::len_bytes() as u8); + // Add dest address + bytes.extend_from_slice(e.eth_address.as_bytes()); + + // Add token id + bytes.push(e.token_id); + + // Add token amount + bytes.extend_from_slice(&e.amount_sui_adjusted.to_be_bytes()); + + bytes + } +} + +impl BridgeMessageEncoding for EthToSuiBridgeAction { + fn as_bytes(&self) -> Vec { + let mut bytes = Vec::new(); + let e = &self.eth_bridge_event; + // Add message type + bytes.push(BridgeActionType::TokenTransfer as u8); + // Add message version + bytes.push(TOKEN_TRANSFER_MESSAGE_VERSION); + // Add nonce + bytes.extend_from_slice(&e.nonce.to_be_bytes()); + // Add source chain id + bytes.push(e.eth_chain_id as u8); + + // Add payload bytes + bytes.extend_from_slice(&self.as_payload_bytes()); + + bytes + } + + fn as_payload_bytes(&self) -> Vec { + let mut bytes = Vec::new(); + let e = &self.eth_bridge_event; + + // Add source address length + bytes.push(EthAddress::len_bytes() as u8); + // Add source address + bytes.extend_from_slice(e.eth_address.as_bytes()); + // Add dest chain id + bytes.push(e.sui_chain_id as u8); + // Add dest address length + bytes.push(SUI_ADDRESS_LENGTH as u8); + // Add dest address + bytes.extend_from_slice(&e.sui_address.to_vec()); + + // Add token id + bytes.push(e.token_id); + + // Add token amount + bytes.extend_from_slice(&e.sui_adjusted_amount.to_be_bytes()); + + bytes + } +} + +impl BridgeMessageEncoding for BlocklistCommitteeAction { + fn as_bytes(&self) -> Vec { + let mut bytes = Vec::new(); + // Add message type + bytes.push(BridgeActionType::UpdateCommitteeBlocklist as u8); + // Add message version + bytes.push(COMMITTEE_BLOCKLIST_MESSAGE_VERSION); + // Add nonce + bytes.extend_from_slice(&self.nonce.to_be_bytes()); + // Add chain id + bytes.push(self.chain_id as u8); + + // Add payload bytes + bytes.extend_from_slice(&self.as_payload_bytes()); + + bytes + } + + fn as_payload_bytes(&self) -> Vec { + let mut bytes = Vec::new(); + + // Add blocklist type + bytes.push(self.blocklist_type as u8); + // Add length of updated members. + // Unwrap: It should not overflow given what we have today. + bytes.push(u8::try_from(self.blocklisted_members.len()).unwrap()); + + // Add list of updated members + // Members are represented as pubkey dervied evm addresses (20 bytes) + let members_bytes = self + .blocklisted_members + .iter() + .map(|m| m.to_eth_address().to_fixed_bytes().to_vec()) + .collect::>(); + for members_bytes in members_bytes { + bytes.extend_from_slice(&members_bytes); + } + + bytes + } +} + +impl BridgeMessageEncoding for EmergencyAction { + fn as_bytes(&self) -> Vec { + let mut bytes = Vec::new(); + // Add message type + bytes.push(BridgeActionType::EmergencyButton as u8); + // Add message version + bytes.push(EMERGENCY_BUTTON_MESSAGE_VERSION); + // Add nonce + bytes.extend_from_slice(&self.nonce.to_be_bytes()); + // Add chain id + bytes.push(self.chain_id as u8); + + // Add payload bytes + bytes.extend_from_slice(&self.as_payload_bytes()); + + bytes + } + + fn as_payload_bytes(&self) -> Vec { + vec![self.action_type as u8] + } +} + +impl BridgeMessageEncoding for LimitUpdateAction { + fn as_bytes(&self) -> Vec { + let mut bytes = Vec::new(); + // Add message type + bytes.push(BridgeActionType::LimitUpdate as u8); + // Add message version + bytes.push(LIMIT_UPDATE_MESSAGE_VERSION); + // Add nonce + bytes.extend_from_slice(&self.nonce.to_be_bytes()); + // Add chain id + bytes.push(self.chain_id as u8); + + // Add payload bytes + bytes.extend_from_slice(&self.as_payload_bytes()); + + bytes + } + + fn as_payload_bytes(&self) -> Vec { + let mut bytes = Vec::new(); + // Add sending chain id + bytes.push(self.sending_chain_id as u8); + // Add new usd limit + bytes.extend_from_slice(&self.new_usd_limit.to_be_bytes()); + bytes + } +} + +impl BridgeMessageEncoding for AssetPriceUpdateAction { + fn as_bytes(&self) -> Vec { + let mut bytes = Vec::new(); + // Add message type + bytes.push(BridgeActionType::AssetPriceUpdate as u8); + // Add message version + bytes.push(EMERGENCY_BUTTON_MESSAGE_VERSION); + // Add nonce + bytes.extend_from_slice(&self.nonce.to_be_bytes()); + // Add chain id + bytes.push(self.chain_id as u8); + + // Add payload bytes + bytes.extend_from_slice(&self.as_payload_bytes()); + + bytes + } + + fn as_payload_bytes(&self) -> Vec { + let mut bytes = Vec::new(); + // Add token id + bytes.push(self.token_id); + // Add new usd limit + bytes.extend_from_slice(&self.new_usd_price.to_be_bytes()); + bytes + } +} + +impl BridgeMessageEncoding for EvmContractUpgradeAction { + fn as_bytes(&self) -> Vec { + let mut bytes = Vec::new(); + // Add message type + bytes.push(BridgeActionType::EvmContractUpgrade as u8); + // Add message version + bytes.push(EVM_CONTRACT_UPGRADE_MESSAGE_VERSION); + // Add nonce + bytes.extend_from_slice(&self.nonce.to_be_bytes()); + // Add chain id + bytes.push(self.chain_id as u8); + + // Add payload bytes + bytes.extend_from_slice(&self.as_payload_bytes()); + + bytes + } + + fn as_payload_bytes(&self) -> Vec { + ethers::abi::encode(&[ + ethers::abi::Token::Address(self.proxy_address), + ethers::abi::Token::Address(self.new_impl_address), + ethers::abi::Token::Bytes(self.call_data.clone()), + ]) + } +} + +impl BridgeMessageEncoding for AddTokensOnSuiAction { + fn as_bytes(&self) -> Vec { + let mut bytes = Vec::new(); + // Add message type + bytes.push(BridgeActionType::AddTokensOnSui as u8); + // Add message version + bytes.push(ADD_TOKENS_ON_SUI_MESSAGE_VERSION); + // Add nonce + bytes.extend_from_slice(&self.nonce.to_be_bytes()); + // Add chain id + bytes.push(self.chain_id as u8); + + // Add payload bytes + bytes.extend_from_slice(&self.as_payload_bytes()); + + bytes + } + + fn as_payload_bytes(&self) -> Vec { + let mut bytes = Vec::new(); + // Add native + bytes.push(self.native as u8); + // Add token ids + // Unwrap: bcs serialization should not fail + bytes.extend_from_slice(&bcs::to_bytes(&self.token_ids).unwrap()); + + // Add token type names + // Unwrap: bcs serialization should not fail + bytes.extend_from_slice( + &bcs::to_bytes( + &self + .token_type_names + .iter() + .map(|m| m.to_canonical_string(false)) + .collect::>(), + ) + .unwrap(), + ); + + // Add token prices + // Unwrap: bcs serialization should not fail + bytes.extend_from_slice(&bcs::to_bytes(&self.token_prices).unwrap()); + + bytes + } +} + +impl BridgeMessageEncoding for AddTokensOnEvmAction { + fn as_bytes(&self) -> Vec { + let mut bytes = Vec::new(); + // Add message type + bytes.push(BridgeActionType::AddTokensOnEvm as u8); + // Add message version + bytes.push(ADD_TOKENS_ON_EVM_MESSAGE_VERSION); + // Add nonce + bytes.extend_from_slice(&self.nonce.to_be_bytes()); + // Add chain id + bytes.push(self.chain_id as u8); + + // Add payload bytes + bytes.extend_from_slice(&self.as_payload_bytes()); + + bytes + } + + fn as_payload_bytes(&self) -> Vec { + let mut bytes = Vec::new(); + // Add native + bytes.push(self.native as u8); + // Add token ids + // Unwrap: bcs serialization should not fail + bytes.push(u8::try_from(self.token_ids.len()).unwrap()); + for token_id in &self.token_ids { + bytes.push(*token_id); + } + + // Add token addresses + // Unwrap: bcs serialization should not fail + bytes.push(u8::try_from(self.token_addresses.len()).unwrap()); + for token_address in &self.token_addresses { + bytes.extend_from_slice(&token_address.to_fixed_bytes()); + } + + // Add token sui decimals + // Unwrap: bcs serialization should not fail + bytes.push(u8::try_from(self.token_sui_decimals.len()).unwrap()); + for token_sui_decimal in &self.token_sui_decimals { + bytes.push(*token_sui_decimal); + } + + // Add token prices + // Unwrap: bcs serialization should not fail + bytes.push(u8::try_from(self.token_prices.len()).unwrap()); + for token_price in &self.token_prices { + bytes.extend_from_slice(&token_price.to_be_bytes()); + } + bytes + } +} + +impl BridgeAction { + /// Convert to message bytes to verify in Move and Solidity + pub fn to_bytes(&self) -> Vec { + let mut bytes = Vec::new(); + // Add prefix + bytes.extend_from_slice(BRIDGE_MESSAGE_PREFIX); + // Add bytes from message itself + bytes.extend_from_slice(&self.as_bytes()); + bytes + } +} + +#[cfg(test)] +mod tests { + use crate::abi::EthToSuiTokenBridgeV1; + use crate::crypto::BridgeAuthorityKeyPair; + use crate::crypto::BridgeAuthorityPublicKeyBytes; + use crate::crypto::BridgeAuthoritySignInfo; + use crate::events::EmittedSuiToEthTokenBridgeV1; + use crate::types::BlocklistType; + use crate::types::EmergencyActionType; + use crate::types::USD_MULTIPLIER; + use ethers::abi::ParamType; + use ethers::types::{Address as EthAddress, TxHash}; + use fastcrypto::encoding::Encoding; + use fastcrypto::encoding::Hex; + use fastcrypto::hash::HashFunction; + use fastcrypto::hash::Keccak256; + use fastcrypto::traits::ToFromBytes; + use prometheus::Registry; + use std::str::FromStr; + use sui_types::base_types::{SuiAddress, TransactionDigest}; + use sui_types::bridge::BridgeChainId; + use sui_types::bridge::TOKEN_ID_BTC; + use sui_types::bridge::TOKEN_ID_USDC; + use sui_types::TypeTag; + + use super::*; + + #[test] + fn test_bridge_message_encoding() -> anyhow::Result<()> { + telemetry_subscribers::init_for_testing(); + let registry = Registry::new(); + mysten_metrics::init_metrics(®istry); + let nonce = 54321u64; + let sui_tx_digest = TransactionDigest::random(); + let sui_chain_id = BridgeChainId::SuiTestnet; + let sui_tx_event_index = 1u16; + let eth_chain_id = BridgeChainId::EthSepolia; + let sui_address = SuiAddress::random_for_testing_only(); + let eth_address = EthAddress::random(); + let token_id = TOKEN_ID_USDC; + let amount_sui_adjusted = 1_000_000; + + let sui_bridge_event = EmittedSuiToEthTokenBridgeV1 { + nonce, + sui_chain_id, + eth_chain_id, + sui_address, + eth_address, + token_id, + amount_sui_adjusted, + }; + + let encoded_bytes = BridgeAction::SuiToEthBridgeAction(SuiToEthBridgeAction { + sui_tx_digest, + sui_tx_event_index, + sui_bridge_event, + }) + .to_bytes(); + + // Construct the expected bytes + let prefix_bytes = BRIDGE_MESSAGE_PREFIX.to_vec(); // len: 18 + let message_type = vec![BridgeActionType::TokenTransfer as u8]; // len: 1 + let message_version = vec![TOKEN_TRANSFER_MESSAGE_VERSION]; // len: 1 + let nonce_bytes = nonce.to_be_bytes().to_vec(); // len: 8 + let source_chain_id_bytes = vec![sui_chain_id as u8]; // len: 1 + + let sui_address_length_bytes = vec![SUI_ADDRESS_LENGTH as u8]; // len: 1 + let sui_address_bytes = sui_address.to_vec(); // len: 32 + let dest_chain_id_bytes = vec![eth_chain_id as u8]; // len: 1 + let eth_address_length_bytes = vec![EthAddress::len_bytes() as u8]; // len: 1 + let eth_address_bytes = eth_address.as_bytes().to_vec(); // len: 20 + + let token_id_bytes = vec![token_id]; // len: 1 + let token_amount_bytes = amount_sui_adjusted.to_be_bytes().to_vec(); // len: 8 + + let mut combined_bytes = Vec::new(); + combined_bytes.extend_from_slice(&prefix_bytes); + combined_bytes.extend_from_slice(&message_type); + combined_bytes.extend_from_slice(&message_version); + combined_bytes.extend_from_slice(&nonce_bytes); + combined_bytes.extend_from_slice(&source_chain_id_bytes); + combined_bytes.extend_from_slice(&sui_address_length_bytes); + combined_bytes.extend_from_slice(&sui_address_bytes); + combined_bytes.extend_from_slice(&dest_chain_id_bytes); + combined_bytes.extend_from_slice(ð_address_length_bytes); + combined_bytes.extend_from_slice(ð_address_bytes); + combined_bytes.extend_from_slice(&token_id_bytes); + combined_bytes.extend_from_slice(&token_amount_bytes); + + assert_eq!(combined_bytes, encoded_bytes); + + // Assert fixed length + // TODO: for each action type add a test to assert the length + assert_eq!( + combined_bytes.len(), + 18 + 1 + 1 + 8 + 1 + 1 + 32 + 1 + 20 + 1 + 1 + 8 + ); + Ok(()) + } + + #[test] + fn test_bridge_message_encoding_regression_emitted_sui_to_eth_token_bridge_v1( + ) -> anyhow::Result<()> { + telemetry_subscribers::init_for_testing(); + let registry = Registry::new(); + mysten_metrics::init_metrics(®istry); + let sui_tx_digest = TransactionDigest::random(); + let sui_tx_event_index = 1u16; + + let nonce = 10u64; + let sui_chain_id = BridgeChainId::SuiTestnet; + let eth_chain_id = BridgeChainId::EthSepolia; + let sui_address = SuiAddress::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000064", + ) + .unwrap(); + let eth_address = + EthAddress::from_str("0x00000000000000000000000000000000000000c8").unwrap(); + let token_id = TOKEN_ID_USDC; + let amount_sui_adjusted = 12345; + + let sui_bridge_event = EmittedSuiToEthTokenBridgeV1 { + nonce, + sui_chain_id, + eth_chain_id, + sui_address, + eth_address, + token_id, + amount_sui_adjusted, + }; + let encoded_bytes = BridgeAction::SuiToEthBridgeAction(SuiToEthBridgeAction { + sui_tx_digest, + sui_tx_event_index, + sui_bridge_event, + }) + .to_bytes(); + assert_eq!( + encoded_bytes, + Hex::decode("5355495f4252494447455f4d4553534147450001000000000000000a012000000000000000000000000000000000000000000000000000000000000000640b1400000000000000000000000000000000000000c8030000000000003039").unwrap(), + ); + + let hash = Keccak256::digest(encoded_bytes).digest; + assert_eq!( + hash.to_vec(), + Hex::decode("6ab34c52b6264cbc12fe8c3874f9b08f8481d2e81530d136386646dbe2f8baf4") + .unwrap(), + ); + Ok(()) + } + + #[test] + fn test_bridge_message_encoding_blocklist_update_v1() { + telemetry_subscribers::init_for_testing(); + let registry = Registry::new(); + mysten_metrics::init_metrics(®istry); + + let pub_key_bytes = BridgeAuthorityPublicKeyBytes::from_bytes( + &Hex::decode("02321ede33d2c2d7a8a152f275a1484edef2098f034121a602cb7d767d38680aa4") + .unwrap(), + ) + .unwrap(); + let blocklist_action = BridgeAction::BlocklistCommitteeAction(BlocklistCommitteeAction { + nonce: 129, + chain_id: BridgeChainId::SuiCustom, + blocklist_type: BlocklistType::Blocklist, + blocklisted_members: vec![pub_key_bytes.clone()], + }); + let bytes = blocklist_action.to_bytes(); + /* + 5355495f4252494447455f4d455353414745: prefix + 01: msg type + 01: msg version + 0000000000000081: nonce + 03: chain id + 00: blocklist type + 01: length of updated members + [ + 68b43fd906c0b8f024a18c56e06744f7c6157c65 + ]: blocklisted members abi-encoded + */ + assert_eq!(bytes, Hex::decode("5355495f4252494447455f4d4553534147450101000000000000008102000168b43fd906c0b8f024a18c56e06744f7c6157c65").unwrap()); + + let pub_key_bytes_2 = BridgeAuthorityPublicKeyBytes::from_bytes( + &Hex::decode("027f1178ff417fc9f5b8290bd8876f0a157a505a6c52db100a8492203ddd1d4279") + .unwrap(), + ) + .unwrap(); + // its evem address: 0xacaef39832cb995c4e049437a3e2ec6a7bad1ab5 + let blocklist_action = BridgeAction::BlocklistCommitteeAction(BlocklistCommitteeAction { + nonce: 68, + chain_id: BridgeChainId::SuiCustom, + blocklist_type: BlocklistType::Unblocklist, + blocklisted_members: vec![pub_key_bytes.clone(), pub_key_bytes_2.clone()], + }); + let bytes = blocklist_action.to_bytes(); + /* + 5355495f4252494447455f4d455353414745: prefix + 01: msg type + 01: msg version + 0000000000000044: nonce + 02: chain id + 01: blocklist type + 02: length of updated members + [ + 68b43fd906c0b8f024a18c56e06744f7c6157c65 + acaef39832cb995c4e049437a3e2ec6a7bad1ab5 + ]: blocklisted members abi-encoded + */ + assert_eq!(bytes, Hex::decode("5355495f4252494447455f4d4553534147450101000000000000004402010268b43fd906c0b8f024a18c56e06744f7c6157c65acaef39832cb995c4e049437a3e2ec6a7bad1ab5").unwrap()); + + let blocklist_action = BridgeAction::BlocklistCommitteeAction(BlocklistCommitteeAction { + nonce: 49, + chain_id: BridgeChainId::EthCustom, + blocklist_type: BlocklistType::Blocklist, + blocklisted_members: vec![pub_key_bytes.clone()], + }); + let bytes = blocklist_action.to_bytes(); + /* + 5355495f4252494447455f4d455353414745: prefix + 01: msg type + 01: msg version + 0000000000000031: nonce + 0c: chain id + 00: blocklist type + 01: length of updated members + [ + 68b43fd906c0b8f024a18c56e06744f7c6157c65 + ]: blocklisted members abi-encoded + */ + assert_eq!(bytes, Hex::decode("5355495f4252494447455f4d455353414745010100000000000000310c000168b43fd906c0b8f024a18c56e06744f7c6157c65").unwrap()); + + let blocklist_action = BridgeAction::BlocklistCommitteeAction(BlocklistCommitteeAction { + nonce: 94, + chain_id: BridgeChainId::EthSepolia, + blocklist_type: BlocklistType::Unblocklist, + blocklisted_members: vec![pub_key_bytes.clone(), pub_key_bytes_2.clone()], + }); + let bytes = blocklist_action.to_bytes(); + /* + 5355495f4252494447455f4d455353414745: prefix + 01: msg type + 01: msg version + 000000000000005e: nonce + 0b: chain id + 01: blocklist type + 02: length of updated members + [ + 00000000000000000000000068b43fd906c0b8f024a18c56e06744f7c6157c65 + 000000000000000000000000acaef39832cb995c4e049437a3e2ec6a7bad1ab5 + ]: blocklisted members abi-encoded + */ + assert_eq!(bytes, Hex::decode("5355495f4252494447455f4d4553534147450101000000000000005e0b010268b43fd906c0b8f024a18c56e06744f7c6157c65acaef39832cb995c4e049437a3e2ec6a7bad1ab5").unwrap()); + } + + #[test] + fn test_bridge_message_encoding_emergency_action() { + let action = BridgeAction::EmergencyAction(EmergencyAction { + nonce: 55, + chain_id: BridgeChainId::SuiCustom, + action_type: EmergencyActionType::Pause, + }); + let bytes = action.to_bytes(); + /* + 5355495f4252494447455f4d455353414745: prefix + 02: msg type + 01: msg version + 0000000000000037: nonce + 03: chain id + 00: action type + */ + assert_eq!( + bytes, + Hex::decode("5355495f4252494447455f4d455353414745020100000000000000370200").unwrap() + ); + + let action = BridgeAction::EmergencyAction(EmergencyAction { + nonce: 56, + chain_id: BridgeChainId::EthSepolia, + action_type: EmergencyActionType::Unpause, + }); + let bytes = action.to_bytes(); + /* + 5355495f4252494447455f4d455353414745: prefix + 02: msg type + 01: msg version + 0000000000000038: nonce + 0b: chain id + 01: action type + */ + assert_eq!( + bytes, + Hex::decode("5355495f4252494447455f4d455353414745020100000000000000380b01").unwrap() + ); + } + + #[test] + fn test_bridge_message_encoding_limit_update_action() { + let action = BridgeAction::LimitUpdateAction(LimitUpdateAction { + nonce: 15, + chain_id: BridgeChainId::SuiCustom, + sending_chain_id: BridgeChainId::EthCustom, + new_usd_limit: 1_000_000 * USD_MULTIPLIER, // $1M USD + }); + let bytes = action.to_bytes(); + /* + 5355495f4252494447455f4d455353414745: prefix + 03: msg type + 01: msg version + 000000000000000f: nonce + 03: chain id + 0c: sending chain id + 00000002540be400: new usd limit + */ + assert_eq!( + bytes, + Hex::decode( + "5355495f4252494447455f4d4553534147450301000000000000000f020c00000002540be400" + ) + .unwrap() + ); + } + + #[test] + fn test_bridge_message_encoding_asset_price_update_action() { + let action = BridgeAction::AssetPriceUpdateAction(AssetPriceUpdateAction { + nonce: 266, + chain_id: BridgeChainId::SuiCustom, + token_id: TOKEN_ID_BTC, + new_usd_price: 100_000 * USD_MULTIPLIER, // $100k USD + }); + let bytes = action.to_bytes(); + /* + 5355495f4252494447455f4d455353414745: prefix + 04: msg type + 01: msg version + 000000000000010a: nonce + 03: chain id + 01: token id + 000000003b9aca00: new usd price + */ + assert_eq!( + bytes, + Hex::decode( + "5355495f4252494447455f4d4553534147450401000000000000010a0201000000003b9aca00" + ) + .unwrap() + ); + } + + #[test] + fn test_bridge_message_encoding_evm_contract_upgrade_action() { + // Calldata with only the function selector and no parameters: `function initializeV2()` + let function_signature = "initializeV2()"; + let selector = &Keccak256::digest(function_signature).digest[0..4]; + let call_data = selector.to_vec(); + assert_eq!(Hex::encode(call_data.clone()), "5cd8a76b"); + + let action = BridgeAction::EvmContractUpgradeAction(EvmContractUpgradeAction { + nonce: 123, + chain_id: BridgeChainId::EthCustom, + proxy_address: EthAddress::repeat_byte(6), + new_impl_address: EthAddress::repeat_byte(9), + call_data, + }); + /* + 5355495f4252494447455f4d455353414745: prefix + 05: msg type + 01: msg version + 000000000000007b: nonce + 0c: chain id + 0000000000000000000000000606060606060606060606060606060606060606: proxy address + 0000000000000000000000000909090909090909090909090909090909090909: new impl address + + 0000000000000000000000000000000000000000000000000000000000000060 + 0000000000000000000000000000000000000000000000000000000000000004 + 5cd8a76b00000000000000000000000000000000000000000000000000000000: call data + */ + assert_eq!(Hex::encode(action.to_bytes().clone()), "5355495f4252494447455f4d4553534147450501000000000000007b0c00000000000000000000000006060606060606060606060606060606060606060000000000000000000000000909090909090909090909090909090909090909000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000045cd8a76b00000000000000000000000000000000000000000000000000000000"); + + // Calldata with one parameter: `function newMockFunction(bool)` + let function_signature = "newMockFunction(bool)"; + let selector = &Keccak256::digest(function_signature).digest[0..4]; + let mut call_data = selector.to_vec(); + call_data.extend(ethers::abi::encode(&[ethers::abi::Token::Bool(true)])); + assert_eq!( + Hex::encode(call_data.clone()), + "417795ef0000000000000000000000000000000000000000000000000000000000000001" + ); + let action = BridgeAction::EvmContractUpgradeAction(EvmContractUpgradeAction { + nonce: 123, + chain_id: BridgeChainId::EthCustom, + proxy_address: EthAddress::repeat_byte(6), + new_impl_address: EthAddress::repeat_byte(9), + call_data, + }); + /* + 5355495f4252494447455f4d455353414745: prefix + 05: msg type + 01: msg version + 000000000000007b: nonce + 0c: chain id + 0000000000000000000000000606060606060606060606060606060606060606: proxy address + 0000000000000000000000000909090909090909090909090909090909090909: new impl address + + 0000000000000000000000000000000000000000000000000000000000000060 + 0000000000000000000000000000000000000000000000000000000000000024 + 417795ef00000000000000000000000000000000000000000000000000000000 + 0000000100000000000000000000000000000000000000000000000000000000: call data + */ + assert_eq!(Hex::encode(action.to_bytes().clone()), "5355495f4252494447455f4d4553534147450501000000000000007b0c0000000000000000000000000606060606060606060606060606060606060606000000000000000000000000090909090909090909090909090909090909090900000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000024417795ef000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000"); + + // Calldata with two parameters: `function newerMockFunction(bool, uint8)` + let function_signature = "newMockFunction(bool,uint8)"; + let selector = &Keccak256::digest(function_signature).digest[0..4]; + let mut call_data = selector.to_vec(); + call_data.extend(ethers::abi::encode(&[ + ethers::abi::Token::Bool(true), + ethers::abi::Token::Uint(42u8.into()), + ])); + assert_eq!( + Hex::encode(call_data.clone()), + "be8fc25d0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002a" + ); + let action = BridgeAction::EvmContractUpgradeAction(EvmContractUpgradeAction { + nonce: 123, + chain_id: BridgeChainId::EthCustom, + proxy_address: EthAddress::repeat_byte(6), + new_impl_address: EthAddress::repeat_byte(9), + call_data, + }); + /* + 5355495f4252494447455f4d455353414745: prefix + 05: msg type + 01: msg version + 000000000000007b: nonce + 0c: chain id + 0000000000000000000000000606060606060606060606060606060606060606: proxy address + 0000000000000000000000000909090909090909090909090909090909090909: new impl address + + 0000000000000000000000000000000000000000000000000000000000000060 + 0000000000000000000000000000000000000000000000000000000000000044 + be8fc25d00000000000000000000000000000000000000000000000000000000 + 0000000100000000000000000000000000000000000000000000000000000000 + 0000002a00000000000000000000000000000000000000000000000000000000: call data + */ + assert_eq!(Hex::encode(action.to_bytes().clone()), "5355495f4252494447455f4d4553534147450501000000000000007b0c0000000000000000000000000606060606060606060606060606060606060606000000000000000000000000090909090909090909090909090909090909090900000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000044be8fc25d0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002a00000000000000000000000000000000000000000000000000000000"); + + // Empty calldate + let action = BridgeAction::EvmContractUpgradeAction(EvmContractUpgradeAction { + nonce: 123, + chain_id: BridgeChainId::EthCustom, + proxy_address: EthAddress::repeat_byte(6), + new_impl_address: EthAddress::repeat_byte(9), + call_data: vec![], + }); + /* + 5355495f4252494447455f4d455353414745: prefix + 05: msg type + 01: msg version + 000000000000007b: nonce + 0c: chain id + 0000000000000000000000000606060606060606060606060606060606060606: proxy address + 0000000000000000000000000909090909090909090909090909090909090909: new impl address + + 0000000000000000000000000000000000000000000000000000000000000060 + 0000000000000000000000000000000000000000000000000000000000000000: call data + */ + let data = action.to_bytes(); + assert_eq!(Hex::encode(data.clone()), "5355495f4252494447455f4d4553534147450501000000000000007b0c0000000000000000000000000606060606060606060606060606060606060606000000000000000000000000090909090909090909090909090909090909090900000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000"); + let types = vec![ParamType::Address, ParamType::Address, ParamType::Bytes]; + // Ensure that the call data (start from bytes 29) can be decoded + ethers::abi::decode(&types, &data[29..]).unwrap(); + } + + #[test] + fn test_bridge_message_encoding_regression_eth_to_sui_token_bridge_v1() -> anyhow::Result<()> { + telemetry_subscribers::init_for_testing(); + let registry = Registry::new(); + mysten_metrics::init_metrics(®istry); + let eth_tx_hash = TxHash::random(); + let eth_event_index = 1u16; + + let nonce = 10u64; + let sui_chain_id = BridgeChainId::SuiTestnet; + let eth_chain_id = BridgeChainId::EthSepolia; + let sui_address = SuiAddress::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000064", + ) + .unwrap(); + let eth_address = + EthAddress::from_str("0x00000000000000000000000000000000000000c8").unwrap(); + let token_id = TOKEN_ID_USDC; + let sui_adjusted_amount = 12345; + + let eth_bridge_event = EthToSuiTokenBridgeV1 { + nonce, + sui_chain_id, + eth_chain_id, + sui_address, + eth_address, + token_id, + sui_adjusted_amount, + }; + let encoded_bytes = BridgeAction::EthToSuiBridgeAction(EthToSuiBridgeAction { + eth_tx_hash, + eth_event_index, + eth_bridge_event, + }) + .to_bytes(); + + assert_eq!( + encoded_bytes, + Hex::decode("5355495f4252494447455f4d4553534147450001000000000000000a0b1400000000000000000000000000000000000000c801200000000000000000000000000000000000000000000000000000000000000064030000000000003039").unwrap(), + ); + + let hash = Keccak256::digest(encoded_bytes).digest; + assert_eq!( + hash.to_vec(), + Hex::decode("b352508c301a37bb1b68a75dd0fc42b6f692b2650818631c8f8a4d4d3e5bef46") + .unwrap(), + ); + Ok(()) + } + + #[test] + fn test_bridge_message_encoding_regression_add_coins_on_sui() -> anyhow::Result<()> { + telemetry_subscribers::init_for_testing(); + + let action = BridgeAction::AddTokensOnSuiAction(AddTokensOnSuiAction { + nonce: 0, + chain_id: BridgeChainId::SuiCustom, + native: false, + token_ids: vec![1, 2, 3, 4], + token_type_names: vec![ + TypeTag::from_str("0x9b5e13bcd0cb23ff25c07698e89d48056c745338d8c9dbd033a4172b87027073::btc::BTC").unwrap(), + TypeTag::from_str("0x7970d71c03573f540a7157f0d3970e117effa6ae16cefd50b45c749670b24e6a::eth::ETH").unwrap(), + TypeTag::from_str("0x500e429a24478405d5130222b20f8570a746b6bc22423f14b4d4e6a8ea580736::usdc::USDC").unwrap(), + TypeTag::from_str("0x46bfe51da1bd9511919a92eb1154149b36c0f4212121808e13e3e5857d607a9c::usdt::USDT").unwrap(), + ], + token_prices: vec![ + 500_000_000u64, + 30_000_000u64, + 1_000u64, + 1_000u64, + ] + }); + let encoded_bytes = action.to_bytes(); + + assert_eq!( + Hex::encode(encoded_bytes), + "5355495f4252494447455f4d4553534147450601000000000000000002000401020304044a396235653133626364306362323366663235633037363938653839643438303536633734353333386438633964626430333361343137326238373032373037333a3a6274633a3a4254434a373937306437316330333537336635343061373135376630643339373065313137656666613661653136636566643530623435633734393637306232346536613a3a6574683a3a4554484c353030653432396132343437383430356435313330323232623230663835373061373436623662633232343233663134623464346536613865613538303733363a3a757364633a3a555344434c343662666535316461316264393531313931396139326562313135343134396233366330663432313231323138303865313365336535383537643630376139633a3a757364743a3a55534454040065cd1d0000000080c3c90100000000e803000000000000e803000000000000", + ); + Ok(()) + } + + #[test] + fn test_bridge_message_encoding_regression_add_coins_on_evm() -> anyhow::Result<()> { + let action = BridgeAction::AddTokensOnEvmAction(crate::types::AddTokensOnEvmAction { + nonce: 0, + chain_id: BridgeChainId::EthCustom, + native: true, + token_ids: vec![99, 100, 101], + token_addresses: vec![ + EthAddress::from_str("0x6B175474E89094C44Da98b954EedeAC495271d0F").unwrap(), + EthAddress::from_str("0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84").unwrap(), + EthAddress::from_str("0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72").unwrap(), + ], + token_sui_decimals: vec![5, 6, 7], + token_prices: vec![1_000_000_000, 2_000_000_000, 3_000_000_000], + }); + let encoded_bytes = action.to_bytes(); + + assert_eq!( + Hex::encode(encoded_bytes), + "5355495f4252494447455f4d455353414745070100000000000000000c0103636465036b175474e89094c44da98b954eedeac495271d0fae7ab96520de3a18e5e111b5eaab095312d7fe84c18360217d8f7ab5e7c516566761ea12ce7f9d720305060703000000003b9aca00000000007735940000000000b2d05e00", + ); + // To generate regression test for sol contracts + let keys = get_bridge_encoding_regression_test_keys(); + for key in keys { + let pub_key = key.public.as_bytes(); + println!("pub_key: {:?}", Hex::encode(pub_key)); + println!( + "sig: {:?}", + Hex::encode( + BridgeAuthoritySignInfo::new(&action, &key) + .signature + .as_bytes() + ) + ); + } + Ok(()) + } + + fn get_bridge_encoding_regression_test_keys() -> Vec { + vec![ + BridgeAuthorityKeyPair::from_bytes( + &Hex::decode("e42c82337ce12d4a7ad6cd65876d91b2ab6594fd50cdab1737c91773ba7451db") + .unwrap(), + ) + .unwrap(), + BridgeAuthorityKeyPair::from_bytes( + &Hex::decode("1aacd610da3d0cc691a04b83b01c34c6c65cda0fe8d502df25ff4b3185c85687") + .unwrap(), + ) + .unwrap(), + BridgeAuthorityKeyPair::from_bytes( + &Hex::decode("53e7baf8378fbc62692e3056c2e10c6666ef8b5b3a53914830f47636d1678140") + .unwrap(), + ) + .unwrap(), + BridgeAuthorityKeyPair::from_bytes( + &Hex::decode("08b5350a091faabd5f25b6e290bfc3f505d43208775b9110dfed5ee6c7a653f0") + .unwrap(), + ) + .unwrap(), + ] + } +} diff --git a/crates/sui-bridge/src/error.rs b/crates/sui-bridge/src/error.rs index 7f1242f9c75ee..d996e58f0da9a 100644 --- a/crates/sui-bridge/src/error.rs +++ b/crates/sui-bridge/src/error.rs @@ -33,6 +33,8 @@ pub enum BridgeError { TransientProviderError(String), // Ethereum provider error ProviderError(String), + // TokenId is unknown + UnknownTokenId(u8), // Invalid BridgeCommittee InvalidBridgeCommittee(String), // Invalid Bridge authority signature @@ -43,6 +45,8 @@ pub enum BridgeError { InvalidAuthorityUrl(BridgeAuthorityPublicKeyBytes), // Invalid Bridge Client request InvalidBridgeClientRequest(String), + // Invalid ChainId + InvalidChainId, // Message is signed by mismatched authority MismatchedAuthoritySigner, // Signature is over a mismatched action diff --git a/crates/sui-bridge/src/eth_client.rs b/crates/sui-bridge/src/eth_client.rs index a97f236dfdab3..836c1c5bcb709 100644 --- a/crates/sui-bridge/src/eth_client.rs +++ b/crates/sui-bridge/src/eth_client.rs @@ -148,7 +148,7 @@ where assert!(logs.iter().all(|log| log.address == address)); let tasks = logs.into_iter().map(|log| self.get_log_tx_details(log)); - let results = futures::future::join_all(tasks) + futures::future::join_all(tasks) .await .into_iter() .collect::, _>>() @@ -158,8 +158,7 @@ where filter, e ) - })?; - Ok(results) + }) } /// This function converts a `Log` to `EthLog`, to make sure the `block_num`, `tx_hash` and `log_index_in_tx` @@ -213,7 +212,7 @@ where } } let log_index_in_tx = log_index_in_tx.ok_or(BridgeError::ProviderError(format!( - "Couldn't find matching log {:?} in transaction {}", + "Couldn't find matching log: {:?} in transaction {}", log, tx_hash )))?; diff --git a/crates/sui-bridge/src/eth_transaction_builder.rs b/crates/sui-bridge/src/eth_transaction_builder.rs new file mode 100644 index 0000000000000..a6882c7727948 --- /dev/null +++ b/crates/sui-bridge/src/eth_transaction_builder.rs @@ -0,0 +1,180 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::abi::{ + eth_bridge_committee, eth_committee_upgradeable_contract, eth_sui_bridge, EthBridgeCommittee, + EthBridgeLimiter, EthCommitteeUpgradeableContract, +}; +use crate::abi::{eth_bridge_config, eth_bridge_limiter, EthBridgeConfig}; +use crate::error::{BridgeError, BridgeResult}; +use crate::types::{ + AddTokensOnEvmAction, AssetPriceUpdateAction, BlocklistCommitteeAction, + BridgeCommitteeValiditySignInfo, EvmContractUpgradeAction, LimitUpdateAction, + VerifiedCertifiedBridgeAction, +}; +use crate::utils::EthSigner; +use crate::{ + abi::EthSuiBridge, + types::{BridgeAction, EmergencyAction}, +}; +use ethers::prelude::*; +use ethers::types::Address as EthAddress; + +pub async fn build_eth_transaction( + contract_address: EthAddress, + signer: EthSigner, + action: VerifiedCertifiedBridgeAction, +) -> BridgeResult> { + if !action.is_governace_action() { + return Err(BridgeError::ActionIsNotGovernanceAction( + action.data().clone(), + )); + } + // TODO: Check chain id? + let sigs = action.auth_sig(); + match action.data() { + BridgeAction::SuiToEthBridgeAction(_) => { + unreachable!() + } + BridgeAction::EthToSuiBridgeAction(_) => { + unreachable!() + } + BridgeAction::EmergencyAction(action) => { + build_emergency_op_approve_transaction(contract_address, signer, action.clone(), sigs) + .await + } + BridgeAction::BlocklistCommitteeAction(action) => { + build_committee_blocklist_approve_transaction( + contract_address, + signer, + action.clone(), + sigs, + ) + .await + } + BridgeAction::LimitUpdateAction(action) => { + build_limit_update_approve_transaction(contract_address, signer, action.clone(), sigs) + .await + } + BridgeAction::AssetPriceUpdateAction(action) => { + build_asset_price_update_approve_transaction( + contract_address, + signer, + action.clone(), + sigs, + ) + .await + } + BridgeAction::EvmContractUpgradeAction(action) => { + build_evm_upgrade_transaction(signer, action.clone(), sigs).await + } + BridgeAction::AddTokensOnSuiAction(_) => { + unreachable!(); + } + BridgeAction::AddTokensOnEvmAction(action) => { + build_add_tokens_on_evm_transaction(contract_address, signer, action.clone(), sigs) + .await + } + } +} + +pub async fn build_emergency_op_approve_transaction( + contract_address: EthAddress, + signer: EthSigner, + action: EmergencyAction, + sigs: &BridgeCommitteeValiditySignInfo, +) -> BridgeResult> { + let contract = EthSuiBridge::new(contract_address, signer.into()); + + let message: eth_sui_bridge::Message = action.clone().into(); + let signatures = sigs + .signatures + .values() + .map(|sig| Bytes::from(sig.as_ref().to_vec())) + .collect::>(); + Ok(contract.execute_emergency_op_with_signatures(signatures, message)) +} + +pub async fn build_committee_blocklist_approve_transaction( + contract_address: EthAddress, + signer: EthSigner, + action: BlocklistCommitteeAction, + sigs: &BridgeCommitteeValiditySignInfo, +) -> BridgeResult> { + let contract = EthBridgeCommittee::new(contract_address, signer.into()); + + let message: eth_bridge_committee::Message = action.clone().into(); + let signatures = sigs + .signatures + .values() + .map(|sig| Bytes::from(sig.as_ref().to_vec())) + .collect::>(); + Ok(contract.update_blocklist_with_signatures(signatures, message)) +} + +pub async fn build_limit_update_approve_transaction( + contract_address: EthAddress, + signer: EthSigner, + action: LimitUpdateAction, + sigs: &BridgeCommitteeValiditySignInfo, +) -> BridgeResult> { + let contract = EthBridgeLimiter::new(contract_address, signer.into()); + + let message: eth_bridge_limiter::Message = action.clone().into(); + let signatures = sigs + .signatures + .values() + .map(|sig| Bytes::from(sig.as_ref().to_vec())) + .collect::>(); + Ok(contract.update_limit_with_signatures(signatures, message)) +} + +pub async fn build_asset_price_update_approve_transaction( + contract_address: EthAddress, + signer: EthSigner, + action: AssetPriceUpdateAction, + sigs: &BridgeCommitteeValiditySignInfo, +) -> BridgeResult> { + let contract = EthBridgeConfig::new(contract_address, signer.into()); + let message: eth_bridge_config::Message = action.clone().into(); + let signatures = sigs + .signatures + .values() + .map(|sig| Bytes::from(sig.as_ref().to_vec())) + .collect::>(); + Ok(contract.update_token_price_with_signatures(signatures, message)) +} + +pub async fn build_add_tokens_on_evm_transaction( + contract_address: EthAddress, + signer: EthSigner, + action: AddTokensOnEvmAction, + sigs: &BridgeCommitteeValiditySignInfo, +) -> BridgeResult> { + let contract = EthBridgeConfig::new(contract_address, signer.into()); + let message: eth_bridge_config::Message = action.clone().into(); + let signatures = sigs + .signatures + .values() + .map(|sig| Bytes::from(sig.as_ref().to_vec())) + .collect::>(); + Ok(contract.add_tokens_with_signatures(signatures, message)) +} + +pub async fn build_evm_upgrade_transaction( + signer: EthSigner, + action: EvmContractUpgradeAction, + sigs: &BridgeCommitteeValiditySignInfo, +) -> BridgeResult> { + let contract_address = action.proxy_address; + let contract = EthCommitteeUpgradeableContract::new(contract_address, signer.into()); + let message: eth_committee_upgradeable_contract::Message = action.clone().into(); + let signatures = sigs + .signatures + .values() + .map(|sig| Bytes::from(sig.as_ref().to_vec())) + .collect::>(); + Ok(contract.upgrade_with_signatures(signatures, message)) +} + +// TODO: add tests for eth transaction building diff --git a/crates/sui-bridge/src/events.rs b/crates/sui-bridge/src/events.rs index ed4c3f447d0cc..d7ba79c75af8b 100644 --- a/crates/sui-bridge/src/events.rs +++ b/crates/sui-bridge/src/events.rs @@ -6,40 +6,82 @@ //! Bridge module. We rely on structures in this file to decode //! the bcs content of the emitted events. -use std::str::FromStr; +#![allow(non_upper_case_globals)] +use crate::crypto::BridgeAuthorityPublicKey; use crate::error::BridgeError; use crate::error::BridgeResult; -use crate::sui_transaction_builder::get_bridge_package_id; use crate::types::BridgeAction; -use crate::types::BridgeActionType; -use crate::types::BridgeChainId; use crate::types::SuiToEthBridgeAction; -use crate::types::TokenId; use ethers::types::Address as EthAddress; use fastcrypto::encoding::Encoding; use fastcrypto::encoding::Hex; use move_core_types::language_storage::StructTag; use once_cell::sync::OnceCell; use serde::{Deserialize, Serialize}; +use std::str::FromStr; use sui_json_rpc_types::SuiEvent; use sui_types::base_types::SuiAddress; +use sui_types::bridge::BridgeChainId; +use sui_types::bridge::MoveTypeBridgeMessageKey; +use sui_types::bridge::MoveTypeCommitteeMember; +use sui_types::bridge::MoveTypeCommitteeMemberRegistration; +use sui_types::collection_types::VecMap; +use sui_types::crypto::ToFromBytes; use sui_types::digests::TransactionDigest; +use sui_types::BRIDGE_PACKAGE_ID; -// This is the event structure defined and emitted in Move +// `TokendDepositedEvent` emitted in bridge.move #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] -pub struct MoveTokenBridgeEvent { - pub message_type: u8, +pub struct MoveTokenDepositedEvent { pub seq_num: u64, pub source_chain: u8, pub sender_address: Vec, pub target_chain: u8, pub target_address: Vec, pub token_type: u8, - pub amount: u64, + pub amount_sui_adjusted: u64, +} + +// `TokenTransferApproved` emitted in bridge.move +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct MoveTokenTransferApproved { + pub message_key: MoveTypeBridgeMessageKey, +} + +// `TokenTransferClaimed` emitted in bridge.move +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct MoveTokenTransferClaimed { + pub message_key: MoveTypeBridgeMessageKey, +} + +// `TokenTransferAlreadyApproved` emitted in bridge.move +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct MoveTokenTransferAlreadyApproved { + pub message_key: MoveTypeBridgeMessageKey, +} + +// `TokenTransferAlreadyClaimed` emitted in bridge.move +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct MoveTokenTransferAlreadyClaimed { + pub message_key: MoveTypeBridgeMessageKey, +} + +// `CommitteeUpdateEvent` emitted in committee.move +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct MoveCommitteeUpdateEvent { + pub members: VecMap, MoveTypeCommitteeMember>, + pub stake_participation_percentage: u64, } -// Sanitized version of MoveTokenBridgeEvent +// `BlocklistValidatorEvent` emitted in committee.move +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct MoveBlocklistValidatorEvent { + pub blocklisted: bool, + pub public_keys: Vec>, +} + +// Sanitized version of MoveTokenDepositedEvent #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Hash)] pub struct EmittedSuiToEthTokenBridgeV1 { pub nonce: u64, @@ -47,66 +89,193 @@ pub struct EmittedSuiToEthTokenBridgeV1 { pub eth_chain_id: BridgeChainId, pub sui_address: SuiAddress, pub eth_address: EthAddress, - pub token_id: TokenId, - pub amount: u64, + pub token_id: u8, + // The amount of tokens deposited with decimal points on Sui side + pub amount_sui_adjusted: u64, } -impl TryFrom for EmittedSuiToEthTokenBridgeV1 { +// Sanitized version of MoveTokenTransferApproved +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Hash)] +pub struct TokenTransferApproved { + pub nonce: u64, + pub source_chain: BridgeChainId, +} + +impl TryFrom for TokenTransferApproved { type Error = BridgeError; - fn try_from(event: MoveTokenBridgeEvent) -> BridgeResult { - if event.message_type != BridgeActionType::TokenTransfer as u8 { - return Err(BridgeError::Generic(format!( - "Failed to convert MoveTokenBridgeEvent to EmittedSuiToEthTokenBridgeV1. Expected message type {}, got {}", - BridgeActionType::TokenTransfer as u8, - event.message_type - ))); - } - let token_id = TokenId::try_from(event.token_type).map_err(|_e| { + fn try_from(event: MoveTokenTransferApproved) -> BridgeResult { + let source_chain = BridgeChainId::try_from(event.message_key.source_chain).map_err(|_e| { BridgeError::Generic(format!( - "Failed to convert MoveTokenBridgeEvent to EmittedSuiToEthTokenBridgeV1. Failed to convert token type {} to TokenId", - event.token_type, + "Failed to convert MoveTokenTransferApproved to TokenTransferApproved. Failed to convert source chain {} to BridgeChainId", + event.message_key.source_chain, + )) + })?; + Ok(Self { + nonce: event.message_key.bridge_seq_num, + source_chain, + }) + } +} + +// Sanitized version of MoveTokenTransferClaimed +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Hash)] +pub struct TokenTransferClaimed { + pub nonce: u64, + pub source_chain: BridgeChainId, +} + +impl TryFrom for TokenTransferClaimed { + type Error = BridgeError; + + fn try_from(event: MoveTokenTransferClaimed) -> BridgeResult { + let source_chain = BridgeChainId::try_from(event.message_key.source_chain).map_err(|_e| { + BridgeError::Generic(format!( + "Failed to convert MoveTokenTransferClaimed to TokenTransferClaimed. Failed to convert source chain {} to BridgeChainId", + event.message_key.source_chain, )) })?; + Ok(Self { + nonce: event.message_key.bridge_seq_num, + source_chain, + }) + } +} + +// Sanitized version of MoveTokenTransferAlreadyApproved +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Hash)] +pub struct TokenTransferAlreadyApproved { + pub nonce: u64, + pub source_chain: BridgeChainId, +} +impl TryFrom for TokenTransferAlreadyApproved { + type Error = BridgeError; + + fn try_from(event: MoveTokenTransferAlreadyApproved) -> BridgeResult { + let source_chain = BridgeChainId::try_from(event.message_key.source_chain).map_err(|_e| { + BridgeError::Generic(format!( + "Failed to convert MoveTokenTransferAlreadyApproved to TokenTransferAlreadyApproved. Failed to convert source chain {} to BridgeChainId", + event.message_key.source_chain, + )) + })?; + Ok(Self { + nonce: event.message_key.bridge_seq_num, + source_chain, + }) + } +} + +// Sanitized version of MoveTokenTransferAlreadyClaimed +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Hash)] +pub struct TokenTransferAlreadyClaimed { + pub nonce: u64, + pub source_chain: BridgeChainId, +} + +impl TryFrom for TokenTransferAlreadyClaimed { + type Error = BridgeError; + + fn try_from(event: MoveTokenTransferAlreadyClaimed) -> BridgeResult { + let source_chain = BridgeChainId::try_from(event.message_key.source_chain).map_err(|_e| { + BridgeError::Generic(format!( + "Failed to convert MoveTokenTransferAlreadyClaimed to TokenTransferAlreadyClaimed. Failed to convert source chain {} to BridgeChainId", + event.message_key.source_chain, + )) + })?; + Ok(Self { + nonce: event.message_key.bridge_seq_num, + source_chain, + }) + } +} + +// Sanitized version of MoveCommitteeUpdateEvent +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct CommitteeUpdate { + pub members: Vec, + pub stake_participation_percentage: u64, +} + +impl TryFrom for CommitteeUpdate { + type Error = BridgeError; + + fn try_from(event: MoveCommitteeUpdateEvent) -> BridgeResult { + let members = event + .members + .contents + .into_iter() + .map(|v| v.value) + .collect(); + Ok(Self { + members, + stake_participation_percentage: event.stake_participation_percentage, + }) + } +} + +// Sanitized version of MoveBlocklistValidatorEvent +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct BlocklistValidatorEvent { + pub blocklisted: bool, + pub public_keys: Vec, +} + +impl TryFrom for BlocklistValidatorEvent { + type Error = BridgeError; + + fn try_from(event: MoveBlocklistValidatorEvent) -> BridgeResult { + let public_keys = event.public_keys.into_iter().map(|bytes| + BridgeAuthorityPublicKey::from_bytes(&bytes).map_err(|e| + BridgeError::Generic(format!("Failed to convert MoveBlocklistValidatorEvent to BlocklistValidatorEvent. Failed to convert public key to BridgeAuthorityPublicKey: {:?}", e)) + ) + ).collect::>>()?; + Ok(Self { + blocklisted: event.blocklisted, + public_keys, + }) + } +} + +impl TryFrom for EmittedSuiToEthTokenBridgeV1 { + type Error = BridgeError; + + fn try_from(event: MoveTokenDepositedEvent) -> BridgeResult { + let token_id = event.token_type; let sui_chain_id = BridgeChainId::try_from(event.source_chain).map_err(|_e| { BridgeError::Generic(format!( - "Failed to convert MoveTokenBridgeEvent to EmittedSuiToEthTokenBridgeV1. Failed to convert source chain {} to BridgeChainId", + "Failed to convert MoveTokenDepositedEvent to EmittedSuiToEthTokenBridgeV1. Failed to convert source chain {} to BridgeChainId", event.token_type, )) })?; let eth_chain_id = BridgeChainId::try_from(event.target_chain).map_err(|_e| { BridgeError::Generic(format!( - "Failed to convert MoveTokenBridgeEvent to EmittedSuiToEthTokenBridgeV1. Failed to convert target chain {} to BridgeChainId", + "Failed to convert MoveTokenDepositedEvent to EmittedSuiToEthTokenBridgeV1. Failed to convert target chain {} to BridgeChainId", event.token_type, )) })?; match sui_chain_id { - BridgeChainId::SuiMainnet - | BridgeChainId::SuiTestnet - | BridgeChainId::SuiDevnet - | BridgeChainId::SuiLocalTest => {} + BridgeChainId::SuiMainnet | BridgeChainId::SuiTestnet | BridgeChainId::SuiCustom => {} _ => { return Err(BridgeError::Generic(format!( - "Failed to convert MoveTokenBridgeEvent to EmittedSuiToEthTokenBridgeV1. Invalid source chain {}", + "Failed to convert MoveTokenDepositedEvent to EmittedSuiToEthTokenBridgeV1. Invalid source chain {}", event.source_chain ))); } } match eth_chain_id { - BridgeChainId::EthMainnet | BridgeChainId::EthSepolia | BridgeChainId::EthLocalTest => { - } + BridgeChainId::EthMainnet | BridgeChainId::EthSepolia | BridgeChainId::EthCustom => {} _ => { return Err(BridgeError::Generic(format!( - "Failed to convert MoveTokenBridgeEvent to EmittedSuiToEthTokenBridgeV1. Invalid target chain {}", + "Failed to convert MoveTokenDepositedEvent to EmittedSuiToEthTokenBridgeV1. Invalid target chain {}", event.target_chain ))); } } let sui_address = SuiAddress::from_bytes(event.sender_address) - .map_err(|e| BridgeError::Generic(format!("Failed to convert MoveTokenBridgeEvent to EmittedSuiToEthTokenBridgeV1. Failed to convert sender_address to SuiAddress: {:?}", e)))?; + .map_err(|e| BridgeError::Generic(format!("Failed to convert MoveTokenDepositedEvent to EmittedSuiToEthTokenBridgeV1. Failed to convert sender_address to SuiAddress: {:?}", e)))?; let eth_address = EthAddress::from_str(&Hex::encode(&event.target_address))?; Ok(Self { @@ -116,23 +285,25 @@ impl TryFrom for EmittedSuiToEthTokenBridgeV1 { sui_address, eth_address, token_id, - amount: event.amount, + amount_sui_adjusted: event.amount_sui_adjusted, }) } } -// TODO: update this once we have bridge package on sui framework -pub fn get_bridge_event_struct_tag() -> &'static str { - static BRIDGE_EVENT_STRUCT_TAG: OnceCell = OnceCell::new(); - BRIDGE_EVENT_STRUCT_TAG.get_or_init(|| { - let bridge_package_id = *get_bridge_package_id(); - format!("0x{}::bridge::TokenBridgeEvent", bridge_package_id.to_hex()) - }) -} - crate::declare_events!( - SuiToEthTokenBridgeV1(EmittedSuiToEthTokenBridgeV1) => (get_bridge_event_struct_tag(), MoveTokenBridgeEvent) - // Add new event types here. Format: EnumVariantName(Struct) => ("StructTagString", CorrespondingMoveStruct) + SuiToEthTokenBridgeV1(EmittedSuiToEthTokenBridgeV1) => ("bridge::TokenDepositedEvent", MoveTokenDepositedEvent), + TokenTransferApproved(TokenTransferApproved) => ("bridge::TokenTransferApproved", MoveTokenTransferApproved), + TokenTransferClaimed(TokenTransferClaimed) => ("bridge::TokenTransferClaimed", MoveTokenTransferClaimed), + TokenTransferAlreadyApproved(TokenTransferAlreadyApproved) => ("bridge::TokenTransferAlreadyApproved", MoveTokenTransferAlreadyApproved), + TokenTransferAlreadyClaimed(TokenTransferAlreadyClaimed) => ("bridge::TokenTransferAlreadyClaimed", MoveTokenTransferAlreadyClaimed), + // No need to define a sanitized event struct for MoveTypeCommitteeMemberRegistration + // because the info provided by validators could be invalid + CommitteeMemberRegistration(MoveTypeCommitteeMemberRegistration) => ("committee::CommitteeMemberRegistration", MoveTypeCommitteeMemberRegistration), + CommitteeUpdateEvent(CommitteeUpdate) => ("committee::CommitteeUpdateEvent", MoveCommitteeUpdateEvent), + BlocklistValidator(BlocklistValidatorEvent) => ("committee::CommitteeUpdateEvent", MoveBlocklistValidatorEvent), + + // Add new event types here. Format: + // EnumVariantName(Struct) => ("{module}::{event_struct}", CorrespondingMoveStruct) ); #[macro_export] @@ -144,12 +315,11 @@ macro_rules! declare_events { $($variant($type),)* } - #[allow(non_upper_case_globals)] - $(pub(crate) static $variant: OnceCell = OnceCell::new();)* + $(pub static $variant: OnceCell = OnceCell::new();)* pub(crate) fn init_all_struct_tags() { $($variant.get_or_init(|| { - StructTag::from_str($event_tag).unwrap() + StructTag::from_str(&format!("0x{}::{}", BRIDGE_PACKAGE_ID.to_hex(), $event_tag)).unwrap() });)* } @@ -185,50 +355,55 @@ impl SuiBridgeEvent { sui_bridge_event: event.clone(), })) } + SuiBridgeEvent::TokenTransferApproved(_event) => None, + SuiBridgeEvent::TokenTransferClaimed(_event) => None, + SuiBridgeEvent::TokenTransferAlreadyApproved(_event) => None, + SuiBridgeEvent::TokenTransferAlreadyClaimed(_event) => None, + SuiBridgeEvent::CommitteeMemberRegistration(_event) => None, + SuiBridgeEvent::CommitteeUpdateEvent(_event) => None, + SuiBridgeEvent::BlocklistValidator(_event) => None, } } } #[cfg(test)] pub mod tests { - use super::get_bridge_event_struct_tag; - use super::EmittedSuiToEthTokenBridgeV1; - use super::MoveTokenBridgeEvent; + use std::collections::HashSet; + + use super::*; + use crate::e2e_tests::test_utils::BridgeTestClusterBuilder; use crate::types::BridgeAction; - use crate::types::BridgeActionType; - use crate::types::BridgeChainId; use crate::types::SuiToEthBridgeAction; - use crate::types::TokenId; use ethers::types::Address as EthAddress; - use move_core_types::language_storage::StructTag; - use std::str::FromStr; use sui_json_rpc_types::SuiEvent; use sui_types::base_types::ObjectID; use sui_types::base_types::SuiAddress; + use sui_types::bridge::BridgeChainId; + use sui_types::bridge::TOKEN_ID_SUI; use sui_types::digests::TransactionDigest; use sui_types::event::EventID; use sui_types::Identifier; /// Returns a test SuiEvent and corresponding BridgeAction pub fn get_test_sui_event_and_action(identifier: Identifier) -> (SuiEvent, BridgeAction) { + init_all_struct_tags(); // Ensure all tags are initialized let sanitized_event = EmittedSuiToEthTokenBridgeV1 { nonce: 1, sui_chain_id: BridgeChainId::SuiTestnet, sui_address: SuiAddress::random_for_testing_only(), eth_chain_id: BridgeChainId::EthSepolia, eth_address: EthAddress::random(), - token_id: TokenId::Sui, - amount: 100, + token_id: TOKEN_ID_SUI, + amount_sui_adjusted: 100, }; - let emitted_event = MoveTokenBridgeEvent { - message_type: BridgeActionType::TokenTransfer as u8, + let emitted_event = MoveTokenDepositedEvent { seq_num: sanitized_event.nonce, source_chain: sanitized_event.sui_chain_id as u8, sender_address: sanitized_event.sui_address.to_vec(), target_chain: sanitized_event.eth_chain_id as u8, target_address: sanitized_event.eth_address.as_bytes().to_vec(), - token_type: sanitized_event.token_id as u8, - amount: sanitized_event.amount, + token_type: sanitized_event.token_id, + amount_sui_adjusted: sanitized_event.amount_sui_adjusted, }; let tx_digest = TransactionDigest::random(); @@ -239,8 +414,7 @@ pub mod tests { sui_bridge_event: sanitized_event.clone(), }); let event = SuiEvent { - // For this test to pass, match what is in events.rs - type_: StructTag::from_str(get_bridge_event_struct_tag()).unwrap(), + type_: SuiToEthTokenBridgeV1.get().unwrap().clone(), bcs: bcs::to_bytes(&emitted_event).unwrap(), id: EventID { tx_digest, @@ -257,4 +431,37 @@ pub mod tests { }; (event, bridge_action) } + + #[tokio::test] + async fn test_bridge_events_conversion() { + telemetry_subscribers::init_for_testing(); + init_all_struct_tags(); + let mut bridge_test_cluster = BridgeTestClusterBuilder::new() + .with_eth_env(true) + .with_bridge_cluster(false) + .build() + .await; + + let events = bridge_test_cluster + .new_bridge_events( + HashSet::from_iter([ + CommitteeMemberRegistration.get().unwrap().clone(), + CommitteeUpdateEvent.get().unwrap().clone(), + ]), + false, + ) + .await; + for event in events.iter() { + match SuiBridgeEvent::try_from_sui_event(event).unwrap().unwrap() { + SuiBridgeEvent::CommitteeMemberRegistration(_event) => (), + SuiBridgeEvent::CommitteeUpdateEvent(_event) => (), + _ => panic!( + "Expected CommitteeMemberRegistration or CommitteeUpdateEvent, got {:?}", + event + ), + } + } + + // TODO: trigger other events and make sure they are converted correctly + } } diff --git a/crates/sui-bridge/src/lib.rs b/crates/sui-bridge/src/lib.rs index 6d527f3ea3620..af6b299dc426b 100644 --- a/crates/sui-bridge/src/lib.rs +++ b/crates/sui-bridge/src/lib.rs @@ -6,9 +6,11 @@ pub mod action_executor; pub mod client; pub mod config; pub mod crypto; +pub mod encoding; pub mod error; pub mod eth_client; pub mod eth_syncer; +pub mod eth_transaction_builder; pub mod events; pub mod node; pub mod orchestrator; @@ -17,7 +19,9 @@ pub mod storage; pub mod sui_client; pub mod sui_syncer; pub mod sui_transaction_builder; +pub mod tools; pub mod types; +pub mod utils; #[cfg(test)] pub(crate) mod eth_mock_provider; @@ -26,8 +30,14 @@ pub(crate) mod eth_mock_provider; pub(crate) mod sui_mock_client; #[cfg(test)] -pub(crate) mod test_utils; +pub mod test_utils; +pub const BRIDGE_ENABLE_PROTOCOL_VERSION: u64 = 45; + +#[cfg(test)] +pub mod e2e_tests; + +// TODO: can we log the error very time it gets retried? #[macro_export] macro_rules! retry_with_max_elapsed_time { ($func:expr, $max_elapsed_time:expr) => {{ diff --git a/crates/sui-bridge/src/main.rs b/crates/sui-bridge/src/main.rs index 20e429969c53d..70792b8e961e5 100644 --- a/crates/sui-bridge/src/main.rs +++ b/crates/sui-bridge/src/main.rs @@ -43,5 +43,5 @@ async fn main() -> anyhow::Result<()> { .with_prom_registry(&prometheus_registry) .init(); - run_bridge_node(config).await + Ok(run_bridge_node(config).await?.await?) } diff --git a/crates/sui-bridge/src/node.rs b/crates/sui-bridge/src/node.rs index 1179dde2504cf..2531704166a87 100644 --- a/crates/sui-bridge/src/node.rs +++ b/crates/sui-bridge/src/node.rs @@ -6,21 +6,29 @@ use crate::{ client::bridge_authority_aggregator::BridgeAuthorityAggregator, config::{BridgeClientConfig, BridgeNodeConfig}, eth_syncer::EthSyncer, + events::init_all_struct_tags, orchestrator::BridgeOrchestrator, server::{handler::BridgeRequestHandler, run_server}, storage::BridgeOrchestratorTables, sui_syncer::SuiSyncer, }; +use ethers::types::Address as EthAddress; use std::{ collections::HashMap, net::{IpAddr, Ipv4Addr, SocketAddr}, sync::Arc, time::Duration, }; +use sui_types::{ + bridge::{BRIDGE_COMMITTEE_MODULE_NAME, BRIDGE_MODULE_NAME}, + event::EventID, + Identifier, +}; use tokio::task::JoinHandle; use tracing::info; -pub async fn run_bridge_node(config: BridgeNodeConfig) -> anyhow::Result<()> { +pub async fn run_bridge_node(config: BridgeNodeConfig) -> anyhow::Result> { + init_all_struct_tags(); let (server_config, client_config) = config.validate().await?; // Start Client @@ -35,7 +43,7 @@ pub async fn run_bridge_node(config: BridgeNodeConfig) -> anyhow::Result<()> { IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), server_config.server_listen_port, ); - run_server( + Ok(run_server( &socket_address, BridgeRequestHandler::new( server_config.key, @@ -43,10 +51,7 @@ pub async fn run_bridge_node(config: BridgeNodeConfig) -> anyhow::Result<()> { server_config.eth_client, server_config.approved_governance_actions, ), - ) - .await; - - Ok(()) + )) } // TODO: is there a way to clean up the overrides after it's stored in DB? @@ -55,70 +60,16 @@ async fn start_client_components( ) -> anyhow::Result>> { let store: std::sync::Arc = BridgeOrchestratorTables::new(&client_config.db_path.join("client")); - let stored_module_cursors = store - .get_sui_event_cursors(&client_config.sui_bridge_modules) - .map_err(|e| anyhow::anyhow!("Unable to get sui event cursors from storage: {e:?}"))?; - let mut sui_modules_to_watch = HashMap::new(); - for (module, cursor) in client_config - .sui_bridge_modules - .iter() - .zip(stored_module_cursors) - { - if client_config - .sui_bridge_modules_last_processed_event_id_override - .contains_key(module) - { - sui_modules_to_watch.insert( - module.clone(), - client_config.sui_bridge_modules_last_processed_event_id_override[module], - ); - info!( - "Overriding cursor for sui bridge module {} to {:?}. Stored cursor: {:?}", - module, - client_config.sui_bridge_modules_last_processed_event_id_override[module], - cursor - ); - } else if let Some(cursor) = cursor { - sui_modules_to_watch.insert(module.clone(), cursor); - } else { - return Err(anyhow::anyhow!( - "No cursor found for sui bridge module {} in storage or config override", - module - )); - } - } - - let stored_eth_cursors = store - .get_eth_event_cursors(&client_config.eth_bridge_contracts) - .map_err(|e| anyhow::anyhow!("Unable to get eth event cursors from storage: {e:?}"))?; - let mut eth_contracts_to_watch = HashMap::new(); - for (contract, cursor) in client_config - .eth_bridge_contracts - .iter() - .zip(stored_eth_cursors) - { - if client_config - .eth_bridge_contracts_start_block_override - .contains_key(contract) - { - eth_contracts_to_watch.insert( - *contract, - client_config.eth_bridge_contracts_start_block_override[contract], - ); - info!( - "Overriding cursor for eth bridge contract {} to {}. Stored cursor: {:?}", - contract, client_config.eth_bridge_contracts_start_block_override[contract], cursor - ); - } else if let Some(cursor) = cursor { - // +1: The stored value is the last block that was processed, so we start from the next block. - eth_contracts_to_watch.insert(*contract, cursor + 1); - } else { - return Err(anyhow::anyhow!( - "No cursor found for eth contract {} in storage or config override", - contract - )); - } - } + let sui_modules_to_watch = get_sui_modules_to_watch( + &store, + client_config.sui_bridge_module_last_processed_event_id_override, + ); + let eth_contracts_to_watch = get_eth_contracts_to_watch( + &store, + &client_config.eth_contracts, + client_config.eth_contracts_start_block_fallback, + client_config.eth_contracts_start_block_override, + ); let sui_client = client_config.sui_client.clone(); @@ -152,11 +103,415 @@ async fn start_client_components( client_config.key, client_config.sui_address, client_config.gas_object_ref.0, - ); + ) + .await; let orchestrator = BridgeOrchestrator::new(sui_client, sui_events_rx, eth_events_rx, store.clone()); - all_handles.extend(orchestrator.run(bridge_action_executor)); + all_handles.extend(orchestrator.run(bridge_action_executor).await); Ok(all_handles) } + +fn get_sui_modules_to_watch( + store: &std::sync::Arc, + sui_bridge_module_last_processed_event_id_override: Option, +) -> HashMap> { + let sui_bridge_modules = vec![ + BRIDGE_MODULE_NAME.to_owned(), + BRIDGE_COMMITTEE_MODULE_NAME.to_owned(), + ]; + if let Some(cursor) = sui_bridge_module_last_processed_event_id_override { + info!("Overriding cursor for sui bridge modules to {:?}", cursor); + return HashMap::from_iter( + sui_bridge_modules + .iter() + .map(|module| (module.clone(), Some(cursor))), + ); + } + + let sui_bridge_module_stored_cursor = store + .get_sui_event_cursors(&sui_bridge_modules) + .expect("Failed to get eth sui event cursors from storage"); + let mut sui_modules_to_watch = HashMap::new(); + for (module_identifier, cursor) in sui_bridge_modules + .iter() + .zip(sui_bridge_module_stored_cursor) + { + if cursor.is_none() { + info!( + "No cursor found for sui bridge module {} in storage or config override, query start from the beginning.", + module_identifier + ); + } + sui_modules_to_watch.insert(module_identifier.clone(), cursor); + } + sui_modules_to_watch +} + +fn get_eth_contracts_to_watch( + store: &std::sync::Arc, + eth_contracts: &[EthAddress], + eth_contracts_start_block_fallback: u64, + eth_contracts_start_block_override: Option, +) -> HashMap { + let stored_eth_cursors = store + .get_eth_event_cursors(eth_contracts) + .expect("Failed to get eth event cursors from storage"); + let mut eth_contracts_to_watch = HashMap::new(); + for (contract, stored_cursor) in eth_contracts.iter().zip(stored_eth_cursors) { + // start block precedence: + // eth_contracts_start_block_override > stored cursor > eth_contracts_start_block_fallback + match (eth_contracts_start_block_override, stored_cursor) { + (Some(override_), _) => { + eth_contracts_to_watch.insert(*contract, override_); + info!( + "Overriding cursor for eth bridge contract {} to {}. Stored cursor: {:?}", + contract, override_, stored_cursor + ); + } + (None, Some(stored_cursor)) => { + // +1: The stored value is the last block that was processed, so we start from the next block. + eth_contracts_to_watch.insert(*contract, stored_cursor + 1); + } + (None, None) => { + // If no cursor is found, start from the fallback block. + eth_contracts_to_watch.insert(*contract, eth_contracts_start_block_fallback); + } + } + } + eth_contracts_to_watch +} + +#[cfg(test)] +mod tests { + use ethers::types::Address as EthAddress; + + use super::*; + use crate::config::BridgeNodeConfig; + use crate::config::EthConfig; + use crate::config::SuiConfig; + use crate::e2e_tests::test_utils::wait_for_server_to_be_up; + use crate::e2e_tests::test_utils::BridgeTestCluster; + use crate::e2e_tests::test_utils::BridgeTestClusterBuilder; + use fastcrypto::secp256k1::Secp256k1KeyPair; + use sui_config::local_ip_utils::get_available_port; + use sui_types::base_types::SuiAddress; + use sui_types::bridge::BridgeChainId; + use sui_types::crypto::get_key_pair; + use sui_types::crypto::EncodeDecodeBase64; + use sui_types::crypto::KeypairTraits; + use sui_types::crypto::SuiKeyPair; + use sui_types::digests::TransactionDigest; + use sui_types::event::EventID; + use tempfile::tempdir; + + #[tokio::test] + async fn test_get_eth_contracts_to_watch() { + telemetry_subscribers::init_for_testing(); + let temp_dir = tempfile::tempdir().unwrap(); + let eth_contracts = vec![ + EthAddress::from_low_u64_be(1), + EthAddress::from_low_u64_be(2), + ]; + let store = BridgeOrchestratorTables::new(temp_dir.path()); + + // No override, no watermark found in DB, use fallback + let contracts = get_eth_contracts_to_watch(&store, ð_contracts, 10, None); + assert_eq!( + contracts, + vec![(eth_contracts[0], 10), (eth_contracts[1], 10)] + .into_iter() + .collect::>() + ); + + // no watermark found in DB, use override + let contracts = get_eth_contracts_to_watch(&store, ð_contracts, 10, Some(420)); + assert_eq!( + contracts, + vec![(eth_contracts[0], 420), (eth_contracts[1], 420)] + .into_iter() + .collect::>() + ); + + store + .update_eth_event_cursor(eth_contracts[0], 100) + .unwrap(); + store + .update_eth_event_cursor(eth_contracts[1], 102) + .unwrap(); + + // No override, found watermarks in DB, use +1 + let contracts = get_eth_contracts_to_watch(&store, ð_contracts, 10, None); + assert_eq!( + contracts, + vec![(eth_contracts[0], 101), (eth_contracts[1], 103)] + .into_iter() + .collect::>() + ); + + // use override + let contracts = get_eth_contracts_to_watch(&store, ð_contracts, 10, Some(200)); + assert_eq!( + contracts, + vec![(eth_contracts[0], 200), (eth_contracts[1], 200)] + .into_iter() + .collect::>() + ); + } + + #[tokio::test] + async fn test_get_sui_modules_to_watch() { + telemetry_subscribers::init_for_testing(); + let temp_dir = tempfile::tempdir().unwrap(); + + let store = BridgeOrchestratorTables::new(temp_dir.path()); + let bridge_module = BRIDGE_MODULE_NAME.to_owned(); + let committee_module = BRIDGE_COMMITTEE_MODULE_NAME.to_owned(); + // No override, no stored watermark, use None + let sui_modules_to_watch = get_sui_modules_to_watch(&store, None); + assert_eq!( + sui_modules_to_watch, + vec![ + (bridge_module.clone(), None), + (committee_module.clone(), None) + ] + .into_iter() + .collect::>() + ); + + // no stored watermark, use override + let override_cursor = EventID { + tx_digest: TransactionDigest::random(), + event_seq: 42, + }; + let sui_modules_to_watch = get_sui_modules_to_watch(&store, Some(override_cursor)); + assert_eq!( + sui_modules_to_watch, + vec![ + (bridge_module.clone(), Some(override_cursor)), + (committee_module.clone(), Some(override_cursor)) + ] + .into_iter() + .collect::>() + ); + + // No override, found stored watermark for `bridge` module, use stored watermark for `bridge` + // and None for `committee` + let stored_cursor = EventID { + tx_digest: TransactionDigest::random(), + event_seq: 100, + }; + store + .update_sui_event_cursor(bridge_module.clone(), stored_cursor) + .unwrap(); + let sui_modules_to_watch = get_sui_modules_to_watch(&store, None); + assert_eq!( + sui_modules_to_watch, + vec![ + (bridge_module.clone(), Some(stored_cursor)), + (committee_module.clone(), None) + ] + .into_iter() + .collect::>() + ); + + // found stored watermark, use override + let stored_cursor = EventID { + tx_digest: TransactionDigest::random(), + event_seq: 100, + }; + store + .update_sui_event_cursor(committee_module.clone(), stored_cursor) + .unwrap(); + let sui_modules_to_watch = get_sui_modules_to_watch(&store, Some(override_cursor)); + assert_eq!( + sui_modules_to_watch, + vec![ + (bridge_module.clone(), Some(override_cursor)), + (committee_module.clone(), Some(override_cursor)) + ] + .into_iter() + .collect::>() + ); + } + + #[tokio::test] + async fn test_starting_bridge_node() { + telemetry_subscribers::init_for_testing(); + let bridge_test_cluster = setup().await; + let kp = bridge_test_cluster.bridge_authority_key(0); + + // prepare node config (server only) + let tmp_dir = tempdir().unwrap().into_path(); + let authority_key_path = "test_starting_bridge_node_bridge_authority_key"; + let server_listen_port = get_available_port("127.0.0.1"); + let base64_encoded = kp.encode_base64(); + std::fs::write(tmp_dir.join(authority_key_path), base64_encoded).unwrap(); + + let config = BridgeNodeConfig { + server_listen_port, + metrics_port: get_available_port("127.0.0.1"), + bridge_authority_key_path_base64_raw: tmp_dir.join(authority_key_path), + sui: SuiConfig { + sui_rpc_url: bridge_test_cluster.sui_rpc_url(), + sui_bridge_chain_id: BridgeChainId::SuiCustom as u8, + bridge_client_key_path_base64_sui_key: None, + bridge_client_gas_object: None, + sui_bridge_module_last_processed_event_id_override: None, + }, + eth: EthConfig { + eth_rpc_url: bridge_test_cluster.eth_rpc_url(), + eth_bridge_proxy_address: bridge_test_cluster.sui_bridge_address(), + eth_bridge_chain_id: BridgeChainId::EthCustom as u8, + eth_contracts_start_block_fallback: None, + eth_contracts_start_block_override: None, + }, + approved_governance_actions: vec![], + run_client: false, + db_path: None, + }; + // Spawn bridge node in memory + let _handle = run_bridge_node(config).await.unwrap(); + + let server_url = format!("http://127.0.0.1:{}", server_listen_port); + // Now we expect to see the server to be up and running. + let res = wait_for_server_to_be_up(server_url, 5).await; + res.unwrap(); + } + + #[tokio::test] + async fn test_starting_bridge_node_with_client() { + telemetry_subscribers::init_for_testing(); + let bridge_test_cluster = setup().await; + let kp = bridge_test_cluster.bridge_authority_key(0); + + // prepare node config (server + client) + let tmp_dir = tempdir().unwrap().into_path(); + let db_path = tmp_dir.join("test_starting_bridge_node_with_client_db"); + let authority_key_path = "test_starting_bridge_node_with_client_bridge_authority_key"; + let server_listen_port = get_available_port("127.0.0.1"); + + let base64_encoded = kp.encode_base64(); + std::fs::write(tmp_dir.join(authority_key_path), base64_encoded).unwrap(); + + let client_sui_address = SuiAddress::from(kp.public()); + let sender_address = bridge_test_cluster.sui_user_address(); + // send some gas to this address + bridge_test_cluster + .test_cluster + .transfer_sui_must_exceed(sender_address, client_sui_address, 1000000000) + .await; + + let config = BridgeNodeConfig { + server_listen_port, + metrics_port: get_available_port("127.0.0.1"), + bridge_authority_key_path_base64_raw: tmp_dir.join(authority_key_path), + sui: SuiConfig { + sui_rpc_url: bridge_test_cluster.sui_rpc_url(), + sui_bridge_chain_id: BridgeChainId::SuiCustom as u8, + bridge_client_key_path_base64_sui_key: None, + bridge_client_gas_object: None, + sui_bridge_module_last_processed_event_id_override: Some(EventID { + tx_digest: TransactionDigest::random(), + event_seq: 0, + }), + }, + eth: EthConfig { + eth_rpc_url: bridge_test_cluster.eth_rpc_url(), + eth_bridge_proxy_address: bridge_test_cluster.sui_bridge_address(), + eth_bridge_chain_id: BridgeChainId::EthCustom as u8, + eth_contracts_start_block_fallback: Some(0), + eth_contracts_start_block_override: None, + }, + approved_governance_actions: vec![], + run_client: true, + db_path: Some(db_path), + }; + // Spawn bridge node in memory + let _handle = run_bridge_node(config).await.unwrap(); + + let server_url = format!("http://127.0.0.1:{}", server_listen_port); + // Now we expect to see the server to be up and running. + // client components are spawned earlier than server, so as long as the server is up, + // we know the client components are already running. + let res = wait_for_server_to_be_up(server_url, 5).await; + res.unwrap(); + } + + #[tokio::test] + async fn test_starting_bridge_node_with_client_and_separate_client_key() { + telemetry_subscribers::init_for_testing(); + let bridge_test_cluster = setup().await; + let kp = bridge_test_cluster.bridge_authority_key(0); + + // prepare node config (server + client) + let tmp_dir = tempdir().unwrap().into_path(); + let db_path = + tmp_dir.join("test_starting_bridge_node_with_client_and_separate_client_key_db"); + let authority_key_path = + "test_starting_bridge_node_with_client_and_separate_client_key_bridge_authority_key"; + let server_listen_port = get_available_port("127.0.0.1"); + + // prepare bridge authority key + let base64_encoded = kp.encode_base64(); + std::fs::write(tmp_dir.join(authority_key_path), base64_encoded).unwrap(); + + // prepare bridge client key + let (_, kp): (_, Secp256k1KeyPair) = get_key_pair(); + let kp = SuiKeyPair::from(kp); + let client_key_path = + "test_starting_bridge_node_with_client_and_separate_client_key_bridge_client_key"; + std::fs::write(tmp_dir.join(client_key_path), kp.encode_base64()).unwrap(); + let client_sui_address = SuiAddress::from(&kp.public()); + let sender_address = bridge_test_cluster.sui_user_address(); + // send some gas to this address + let gas_obj = bridge_test_cluster + .test_cluster + .transfer_sui_must_exceed(sender_address, client_sui_address, 1000000000) + .await; + + let config = BridgeNodeConfig { + server_listen_port, + metrics_port: get_available_port("127.0.0.1"), + bridge_authority_key_path_base64_raw: tmp_dir.join(authority_key_path), + sui: SuiConfig { + sui_rpc_url: bridge_test_cluster.sui_rpc_url(), + sui_bridge_chain_id: BridgeChainId::SuiCustom as u8, + bridge_client_key_path_base64_sui_key: Some(tmp_dir.join(client_key_path)), + bridge_client_gas_object: Some(gas_obj), + sui_bridge_module_last_processed_event_id_override: Some(EventID { + tx_digest: TransactionDigest::random(), + event_seq: 0, + }), + }, + eth: EthConfig { + eth_rpc_url: bridge_test_cluster.eth_rpc_url(), + eth_bridge_proxy_address: bridge_test_cluster.sui_bridge_address(), + eth_bridge_chain_id: BridgeChainId::EthCustom as u8, + eth_contracts_start_block_fallback: Some(0), + eth_contracts_start_block_override: Some(0), + }, + approved_governance_actions: vec![], + run_client: true, + db_path: Some(db_path), + }; + // Spawn bridge node in memory + let _handle = run_bridge_node(config).await.unwrap(); + + let server_url = format!("http://127.0.0.1:{}", server_listen_port); + // Now we expect to see the server to be up and running. + // client components are spawned earlier than server, so as long as the server is up, + // we know the client components are already running. + let res = wait_for_server_to_be_up(server_url, 5).await; + res.unwrap(); + } + + async fn setup() -> BridgeTestCluster { + BridgeTestClusterBuilder::new() + .with_eth_env(true) + .with_bridge_cluster(false) + .build() + .await + } +} diff --git a/crates/sui-bridge/src/orchestrator.rs b/crates/sui-bridge/src/orchestrator.rs index 451ecfb6d37b2..033c720bb35e3 100644 --- a/crates/sui-bridge/src/orchestrator.rs +++ b/crates/sui-bridge/src/orchestrator.rs @@ -48,7 +48,7 @@ where } } - pub fn run( + pub async fn run( self, bridge_action_executor: impl BridgeActionExecutorTrait, ) -> Vec> { @@ -66,11 +66,24 @@ where self.sui_events_rx, ))); let store_clone = self.store.clone(); + + // Re-submit pending actions to executor + let actions = store_clone + .get_all_pending_actions() + .into_values() + .collect::>(); + for action in actions { + submit_to_executor(&executor_sender, action) + .await + .expect("Submit to executor should not fail"); + } + task_handles.push(spawn_logged_monitored_task!(Self::run_eth_watcher( store_clone, executor_sender, self.eth_events_rx, ))); + // TODO: spawn bridge committee change watcher task task_handles } @@ -151,7 +164,6 @@ where continue; } - // TODO: skip events that are not already processed (in DB and on chain) let bridge_events = logs .iter() .map(EthBridgeEvent::try_from_eth_log) @@ -166,6 +178,7 @@ where } // Unwrap safe: checked above let bridge_event = opt_bridge_event.unwrap(); + info!("Observed Eth bridge event: {:?}", bridge_event); if let Some(action) = bridge_event.try_into_bridge_action(log.tx_hash, log.log_index_in_tx) @@ -197,12 +210,16 @@ where #[cfg(test)] mod tests { - use crate::{test_utils::get_test_log_and_action, types::BridgeActionDigest}; + use crate::{ + test_utils::{get_test_eth_to_sui_bridge_action, get_test_log_and_action}, + types::BridgeActionDigest, + }; use ethers::types::{Address as EthAddress, TxHash}; use prometheus::Registry; use std::str::FromStr; use super::*; + use crate::test_utils::get_test_sui_to_eth_bridge_action; use crate::{events::tests::get_test_sui_event_and_action, sui_mock_client::SuiMockClient}; #[tokio::test] @@ -221,7 +238,8 @@ mod tests { eth_events_rx, store.clone(), ) - .run(executor); + .run(executor) + .await; let identifier = Identifier::from_str("test_sui_watcher_task").unwrap(); let (sui_event, bridge_action) = get_test_sui_event_and_action(identifier.clone()); @@ -237,7 +255,7 @@ mod tests { bridge_action.digest() ); loop { - let actions = store.get_all_pending_actions().unwrap(); + let actions = store.get_all_pending_actions(); if actions.is_empty() { if start.elapsed().as_secs() > 5 { panic!("Timed out waiting for action to be written to WAL"); @@ -272,7 +290,8 @@ mod tests { eth_events_rx, store.clone(), ) - .run(executor); + .run(executor) + .await; let address = EthAddress::random(); let (log, bridge_action) = get_test_log_and_action(address, TxHash::random(), 10); let log_index_in_tx = 10; @@ -297,7 +316,7 @@ mod tests { ); let start = std::time::Instant::now(); loop { - let actions = store.get_all_pending_actions().unwrap(); + let actions = store.get_all_pending_actions(); if actions.is_empty() { if start.elapsed().as_secs() > 5 { panic!("Timed out waiting for action to be written to WAL"); @@ -316,6 +335,47 @@ mod tests { } } + #[tokio::test] + /// Test that when orchestrator starts, all pending actions are sent to executor + async fn test_resume_actions_in_pending_logs() { + let (_sui_events_tx, sui_events_rx, _eth_events_tx, eth_events_rx, sui_client, store) = + setup(); + let (executor, mut executor_requested_action_rx) = MockExecutor::new(); + + let action1 = get_test_sui_to_eth_bridge_action( + None, + Some(0), + Some(99), + Some(10000), + None, + None, + None, + ); + + let action2 = get_test_eth_to_sui_bridge_action(None, None, None); + store + .insert_pending_actions(&vec![action1.clone(), action2.clone()]) + .unwrap(); + + // start orchestrator + let _handles = BridgeOrchestrator::new( + Arc::new(sui_client), + sui_events_rx, + eth_events_rx, + store.clone(), + ) + .run(executor) + .await; + + // Executor should have received the action + let mut digests = std::collections::HashSet::new(); + digests.insert(executor_requested_action_rx.recv().await.unwrap()); + digests.insert(executor_requested_action_rx.recv().await.unwrap()); + assert!(digests.contains(&action1.digest())); + assert!(digests.contains(&action2.digest())); + assert_eq!(digests.len(), 2); + } + #[allow(clippy::type_complexity)] fn setup() -> ( mysten_metrics::metered_channel::Sender<(Identifier, Vec)>, @@ -329,9 +389,6 @@ mod tests { let registry = Registry::new(); mysten_metrics::init_metrics(®istry); - // TODO: remove once we don't rely on env var to get package id - std::env::set_var("BRIDGE_PACKAGE_ID", "0x0b"); - let temp_dir = tempfile::tempdir().unwrap(); let store = BridgeOrchestratorTables::new(temp_dir.path()); diff --git a/crates/sui-bridge/src/server/governance_verifier.rs b/crates/sui-bridge/src/server/governance_verifier.rs index 7be1323b0b32b..ecfc8bfc832a5 100644 --- a/crates/sui-bridge/src/server/governance_verifier.rs +++ b/crates/sui-bridge/src/server/governance_verifier.rs @@ -51,21 +51,20 @@ mod tests { use super::*; use crate::{ test_utils::get_test_sui_to_eth_bridge_action, - types::{ - BridgeAction, BridgeChainId, EmergencyAction, EmergencyActionType, LimitUpdateAction, - }, + types::{BridgeAction, EmergencyAction, EmergencyActionType, LimitUpdateAction}, }; + use sui_types::bridge::BridgeChainId; #[tokio::test] async fn test_governance_verifier() { let action_1 = BridgeAction::EmergencyAction(EmergencyAction { - chain_id: BridgeChainId::EthLocalTest, + chain_id: BridgeChainId::EthCustom, nonce: 1, action_type: EmergencyActionType::Pause, }); let action_2 = BridgeAction::LimitUpdateAction(LimitUpdateAction { - chain_id: BridgeChainId::EthLocalTest, - sending_chain_id: BridgeChainId::SuiLocalTest, + chain_id: BridgeChainId::EthCustom, + sending_chain_id: BridgeChainId::SuiCustom, nonce: 1, new_usd_limit: 10000, }); @@ -81,8 +80,8 @@ mod tests { ); let action_3 = BridgeAction::LimitUpdateAction(LimitUpdateAction { - chain_id: BridgeChainId::EthLocalTest, - sending_chain_id: BridgeChainId::SuiLocalTest, + chain_id: BridgeChainId::EthCustom, + sending_chain_id: BridgeChainId::SuiCustom, nonce: 2, new_usd_limit: 10000, }); @@ -92,7 +91,7 @@ mod tests { ); // Token transfer action is not allowed - let action_4 = get_test_sui_to_eth_bridge_action(None, None, None, None); + let action_4 = get_test_sui_to_eth_bridge_action(None, None, None, None, None, None, None); assert!(matches!( GovernanceVerifier::new(vec![action_1, action_2, action_4.clone()]).unwrap_err(), BridgeError::ActionIsNotGovernanceAction(..) diff --git a/crates/sui-bridge/src/server/handler.rs b/crates/sui-bridge/src/server/handler.rs index 6d201b0e25489..4fa1eced19229 100644 --- a/crates/sui-bridge/src/server/handler.rs +++ b/crates/sui-bridge/src/server/handler.rs @@ -271,7 +271,7 @@ impl BridgeRequestHandlerTrait for BridgeRequestHandler { .await .unwrap_or_else(|_| panic!("Server eth signing channel is closed")); let signed_action = rx - .blocking_recv() + .await .unwrap_or_else(|_| panic!("Server signing task's oneshot channel is dropped"))?; Ok(Json(signed_action)) } @@ -291,7 +291,7 @@ impl BridgeRequestHandlerTrait for BridgeRequestHandler { .await .unwrap_or_else(|_| panic!("Server sui signing channel is closed")); let signed_action = rx - .blocking_recv() + .await .unwrap_or_else(|_| panic!("Server signing task's oneshot channel is dropped"))?; Ok(Json(signed_action)) } @@ -309,7 +309,7 @@ impl BridgeRequestHandlerTrait for BridgeRequestHandler { .send((action, tx)) .await .unwrap_or_else(|_| panic!("Server governance action signing channel is closed")); - let signed_action = rx.blocking_recv().unwrap_or_else(|_| { + let signed_action = rx.await.unwrap_or_else(|_| { panic!("Server governance action task's oneshot channel is dropped") })?; Ok(Json(signed_action)) @@ -323,18 +323,16 @@ mod tests { use super::*; use crate::{ eth_mock_provider::EthMockProvider, - events::{init_all_struct_tags, MoveTokenBridgeEvent, SuiToEthTokenBridgeV1}, + events::{init_all_struct_tags, MoveTokenDepositedEvent, SuiToEthTokenBridgeV1}, sui_mock_client::SuiMockClient, test_utils::{ get_test_log_and_action, get_test_sui_to_eth_bridge_action, mock_last_finalized_block, }, - types::{ - BridgeActionType, BridgeChainId, EmergencyAction, EmergencyActionType, - LimitUpdateAction, TokenId, - }, + types::{EmergencyAction, EmergencyActionType, LimitUpdateAction}, }; use ethers::types::{Address as EthAddress, TransactionReceipt}; use sui_json_rpc_types::SuiEvent; + use sui_types::bridge::{BridgeChainId, TOKEN_ID_USDC}; use sui_types::{base_types::SuiAddress, crypto::get_key_pair}; #[tokio::test] @@ -362,8 +360,15 @@ mod tests { .await; assert!(entry_.unwrap().lock().await.is_none()); - let action = - get_test_sui_to_eth_bridge_action(Some(sui_tx_digest), Some(sui_event_idx), None, None); + let action = get_test_sui_to_eth_bridge_action( + Some(sui_tx_digest), + Some(sui_event_idx), + None, + None, + None, + None, + None, + ); let sig = BridgeAuthoritySignInfo::new(&action, &signer); let signed_action = SignedBridgeAction::new_from_data_and_sig(action.clone(), sig); entry.lock().await.replace(Ok(signed_action)); @@ -407,19 +412,16 @@ mod tests { // and BridgeEventNotActionable to be cached // Test `sign` caches Ok result - let emitted_event_1 = MoveTokenBridgeEvent { - message_type: BridgeActionType::TokenTransfer as u8, + let emitted_event_1 = MoveTokenDepositedEvent { seq_num: 1, - source_chain: BridgeChainId::SuiLocalTest as u8, + source_chain: BridgeChainId::SuiCustom as u8, sender_address: SuiAddress::random_for_testing_only().to_vec(), - target_chain: BridgeChainId::EthLocalTest as u8, + target_chain: BridgeChainId::EthCustom as u8, target_address: EthAddress::random().as_bytes().to_vec(), - token_type: TokenId::USDC as u8, - amount: 12345, + token_type: TOKEN_ID_USDC, + amount_sui_adjusted: 12345, }; - // TODO: remove once we don't rely on env var to get package id - std::env::set_var("BRIDGE_PACKAGE_ID", "0x0b"); init_all_struct_tags(); let mut sui_event_1 = SuiEvent::random_for_testing(); @@ -538,13 +540,13 @@ mod tests { #[tokio::test] async fn test_signer_with_governace_verifier() { let action_1 = BridgeAction::EmergencyAction(EmergencyAction { - chain_id: BridgeChainId::EthLocalTest, + chain_id: BridgeChainId::EthCustom, nonce: 1, action_type: EmergencyActionType::Pause, }); let action_2 = BridgeAction::LimitUpdateAction(LimitUpdateAction { - chain_id: BridgeChainId::EthLocalTest, - sending_chain_id: BridgeChainId::SuiLocalTest, + chain_id: BridgeChainId::EthCustom, + sending_chain_id: BridgeChainId::SuiCustom, nonce: 1, new_usd_limit: 10000, }); @@ -581,7 +583,7 @@ mod tests { // alter action_1 to action_3 let action_3 = BridgeAction::EmergencyAction(EmergencyAction { - chain_id: BridgeChainId::EthLocalTest, + chain_id: BridgeChainId::EthCustom, nonce: 1, action_type: EmergencyActionType::Unpause, }); @@ -598,7 +600,7 @@ mod tests { )); // Non governace action is not signable - let action_4 = get_test_sui_to_eth_bridge_action(None, None, None, None); + let action_4 = get_test_sui_to_eth_bridge_action(None, None, None, None, None, None, None); assert!(matches!( signer_with_cache.sign(action_4.clone()).await.unwrap_err(), BridgeError::ActionIsNotGovernanceAction(..) diff --git a/crates/sui-bridge/src/server/mock_handler.rs b/crates/sui-bridge/src/server/mock_handler.rs index 87adce061edf7..553281a568787 100644 --- a/crates/sui-bridge/src/server/mock_handler.rs +++ b/crates/sui-bridge/src/server/mock_handler.rs @@ -70,6 +70,12 @@ impl BridgeRequestMockHandler { } } +impl Default for BridgeRequestMockHandler { + fn default() -> Self { + Self::new() + } +} + #[async_trait] impl BridgeRequestHandlerTrait for BridgeRequestMockHandler { async fn handle_eth_tx_hash( diff --git a/crates/sui-bridge/src/server/mod.rs b/crates/sui-bridge/src/server/mod.rs index ee6c00525067d..3e929921d485d 100644 --- a/crates/sui-bridge/src/server/mod.rs +++ b/crates/sui-bridge/src/server/mod.rs @@ -2,15 +2,14 @@ // SPDX-License-Identifier: Apache-2.0 #![allow(clippy::inconsistent_digit_grouping)] - use crate::{ crypto::BridgeAuthorityPublicKeyBytes, error::BridgeError, server::handler::{BridgeRequestHandler, BridgeRequestHandlerTrait}, types::{ - AssetPriceUpdateAction, BlocklistCommitteeAction, BlocklistType, BridgeAction, - BridgeChainId, EmergencyAction, EmergencyActionType, EvmContractUpgradeAction, - LimitUpdateAction, SignedBridgeAction, TokenId, + AddTokensOnEvmAction, AddTokensOnSuiAction, AssetPriceUpdateAction, + BlocklistCommitteeAction, BlocklistType, BridgeAction, EmergencyAction, + EmergencyActionType, EvmContractUpgradeAction, LimitUpdateAction, SignedBridgeAction, }, }; use axum::{ @@ -23,8 +22,9 @@ use fastcrypto::{ encoding::{Encoding, Hex}, traits::ToFromBytes, }; -use std::net::SocketAddr; use std::sync::Arc; +use std::{net::SocketAddr, str::FromStr}; +use sui_types::{bridge::BridgeChainId, TypeTag}; pub mod governance_verifier; pub mod handler; @@ -48,12 +48,20 @@ pub const EVM_CONTRACT_UPGRADE_PATH_WITH_CALLDATA: &str = "/sign/upgrade_evm_contract/:chain_id/:nonce/:proxy_address/:new_impl_address/:calldata"; pub const EVM_CONTRACT_UPGRADE_PATH: &str = "/sign/upgrade_evm_contract/:chain_id/:nonce/:proxy_address/:new_impl_address"; - -pub async fn run_server(socket_address: &SocketAddr, handler: BridgeRequestHandler) { - axum::Server::bind(socket_address) - .serve(make_router(Arc::new(handler)).into_make_service()) - .await - .unwrap(); +pub const ADD_TOKENS_ON_SUI_PATH: &str = + "/sign/add_tokens_on_sui/:chain_id/:nonce/:native/:token_ids/:token_type_names/:token_prices"; +pub const ADD_TOKENS_ON_EVM_PATH: &str = + "/sign/add_tokens_on_evm/:chain_id/:nonce/:native/:token_ids/:token_addresses/:token_sui_decimals/:token_prices"; + +pub fn run_server( + socket_address: &SocketAddr, + handler: BridgeRequestHandler, +) -> tokio::task::JoinHandle<()> { + let service = axum::Server::bind(socket_address) + .serve(make_router(Arc::new(handler)).into_make_service()); + tokio::spawn(async move { + service.await.unwrap(); + }) } pub(crate) fn make_router( @@ -78,6 +86,8 @@ pub(crate) fn make_router( EVM_CONTRACT_UPGRADE_PATH_WITH_CALLDATA, get(handle_evm_contract_upgrade_with_calldata), ) + .route(ADD_TOKENS_ON_SUI_PATH, get(handle_add_tokens_on_sui)) + .route(ADD_TOKENS_ON_EVM_PATH, get(handle_add_tokens_on_evm)) .with_state(handler) } @@ -199,9 +209,6 @@ async fn handle_asset_price_update_action( let chain_id = BridgeChainId::try_from(chain_id).map_err(|err| { BridgeError::InvalidBridgeClientRequest(format!("Invalid chain id: {:?}", err)) })?; - let token_id = TokenId::try_from(token_id).map_err(|err| { - BridgeError::InvalidBridgeClientRequest(format!("Invalid token id: {:?}", err)) - })?; let action = BridgeAction::AssetPriceUpdateAction(AssetPriceUpdateAction { chain_id, nonce, @@ -262,8 +269,159 @@ async fn handle_evm_contract_upgrade( Ok(sig) } +async fn handle_add_tokens_on_sui( + Path((chain_id, nonce, native, token_ids, token_type_names, token_prices)): Path<( + u8, + u64, + u8, + String, + String, + String, + )>, + State(handler): State>, +) -> Result, BridgeError> { + let chain_id = BridgeChainId::try_from(chain_id).map_err(|err| { + BridgeError::InvalidBridgeClientRequest(format!("Invalid chain id: {:?}", err)) + })?; + + if !chain_id.is_sui_chain() { + return Err(BridgeError::InvalidBridgeClientRequest( + "handle_add_tokens_on_sui only expects Sui chain id".to_string(), + )); + } + + let native = match native { + 1 => true, + 0 => false, + _ => { + return Err(BridgeError::InvalidBridgeClientRequest(format!( + "Invalid native flag: {}", + native + ))) + } + }; + let token_ids = token_ids + .split(',') + .map(|s| { + s.parse::().map_err(|err| { + BridgeError::InvalidBridgeClientRequest(format!("Invalid token id: {:?}", err)) + }) + }) + .collect::, _>>()?; + let token_type_names = token_type_names + .split(',') + .map(|s| { + TypeTag::from_str(s).map_err(|err| { + BridgeError::InvalidBridgeClientRequest(format!( + "Invalid token type name: {:?}", + err + )) + }) + }) + .collect::, _>>()?; + let token_prices = token_prices + .split(',') + .map(|s| { + s.parse::().map_err(|err| { + BridgeError::InvalidBridgeClientRequest(format!("Invalid token price: {:?}", err)) + }) + }) + .collect::, _>>()?; + let action = BridgeAction::AddTokensOnSuiAction(AddTokensOnSuiAction { + chain_id, + nonce, + native, + token_ids, + token_type_names, + token_prices, + }); + let sig: Json = handler.handle_governance_action(action).await?; + Ok(sig) +} + +async fn handle_add_tokens_on_evm( + Path((chain_id, nonce, native, token_ids, token_addresses, token_sui_decimals, token_prices)): Path<( + u8, + u64, + u8, + String, + String, + String, + String, + )>, + State(handler): State>, +) -> Result, BridgeError> { + let chain_id = BridgeChainId::try_from(chain_id).map_err(|err| { + BridgeError::InvalidBridgeClientRequest(format!("Invalid chain id: {:?}", err)) + })?; + if chain_id.is_sui_chain() { + return Err(BridgeError::InvalidBridgeClientRequest( + "handle_add_tokens_on_evm does not expect Sui chain id".to_string(), + )); + } + + let native = match native { + 1 => true, + 0 => false, + _ => { + return Err(BridgeError::InvalidBridgeClientRequest(format!( + "Invalid native flag: {}", + native + ))) + } + }; + let token_ids = token_ids + .split(',') + .map(|s| { + s.parse::().map_err(|err| { + BridgeError::InvalidBridgeClientRequest(format!("Invalid token id: {:?}", err)) + }) + }) + .collect::, _>>()?; + let token_addresses = token_addresses + .split(',') + .map(|s| { + EthAddress::from_str(s).map_err(|err| { + BridgeError::InvalidBridgeClientRequest(format!("Invalid token address: {:?}", err)) + }) + }) + .collect::, _>>()?; + let token_sui_decimals = token_sui_decimals + .split(',') + .map(|s| { + s.parse::().map_err(|err| { + BridgeError::InvalidBridgeClientRequest(format!( + "Invalid token sui decimals: {:?}", + err + )) + }) + }) + .collect::, _>>()?; + let token_prices = token_prices + .split(',') + .map(|s| { + s.parse::().map_err(|err| { + BridgeError::InvalidBridgeClientRequest(format!("Invalid token price: {:?}", err)) + }) + }) + .collect::, _>>()?; + let action = BridgeAction::AddTokensOnEvmAction(AddTokensOnEvmAction { + chain_id, + nonce, + native, + token_ids, + token_addresses, + token_sui_decimals, + token_prices, + }); + let sig: Json = handler.handle_governance_action(action).await?; + Ok(sig) +} + #[cfg(test)] mod tests { + use sui_types::bridge::TOKEN_ID_BTC; + use super::*; use crate::client::bridge_client::BridgeClient; use crate::server::mock_handler::BridgeRequestMockHandler; @@ -281,7 +439,7 @@ mod tests { .unwrap(); let action = BridgeAction::BlocklistCommitteeAction(BlocklistCommitteeAction { nonce: 129, - chain_id: BridgeChainId::SuiLocalTest, + chain_id: BridgeChainId::SuiCustom, blocklist_type: BlocklistType::Blocklist, blocklisted_members: vec![pub_key_bytes.clone()], }); @@ -294,7 +452,7 @@ mod tests { let action = BridgeAction::EmergencyAction(EmergencyAction { nonce: 55, - chain_id: BridgeChainId::SuiLocalTest, + chain_id: BridgeChainId::SuiCustom, action_type: EmergencyActionType::Pause, }); client.request_sign_bridge_action(action).await.unwrap(); @@ -306,8 +464,8 @@ mod tests { let action = BridgeAction::LimitUpdateAction(LimitUpdateAction { nonce: 15, - chain_id: BridgeChainId::SuiLocalTest, - sending_chain_id: BridgeChainId::EthLocalTest, + chain_id: BridgeChainId::SuiCustom, + sending_chain_id: BridgeChainId::EthCustom, new_usd_limit: 1_000_000_0000, // $1M USD }); client.request_sign_bridge_action(action).await.unwrap(); @@ -319,8 +477,8 @@ mod tests { let action = BridgeAction::AssetPriceUpdateAction(AssetPriceUpdateAction { nonce: 266, - chain_id: BridgeChainId::SuiLocalTest, - token_id: TokenId::BTC, + chain_id: BridgeChainId::SuiCustom, + token_id: TOKEN_ID_BTC, new_usd_price: 100_000_0000, // $100k USD }); client.request_sign_bridge_action(action).await.unwrap(); @@ -332,7 +490,7 @@ mod tests { let action = BridgeAction::EvmContractUpgradeAction(EvmContractUpgradeAction { nonce: 123, - chain_id: BridgeChainId::EthLocalTest, + chain_id: BridgeChainId::EthCustom, proxy_address: EthAddress::repeat_byte(6), new_impl_address: EthAddress::repeat_byte(9), call_data: vec![], @@ -341,7 +499,7 @@ mod tests { let action = BridgeAction::EvmContractUpgradeAction(EvmContractUpgradeAction { nonce: 123, - chain_id: BridgeChainId::EthLocalTest, + chain_id: BridgeChainId::EthCustom, proxy_address: EthAddress::repeat_byte(6), new_impl_address: EthAddress::repeat_byte(9), call_data: vec![12, 34, 56], @@ -349,6 +507,45 @@ mod tests { client.request_sign_bridge_action(action).await.unwrap(); } + #[tokio::test] + async fn test_bridge_server_handle_add_tokens_on_sui_action_path() { + let client = setup(); + + let action = BridgeAction::AddTokensOnSuiAction(AddTokensOnSuiAction { + nonce: 266, + chain_id: BridgeChainId::SuiCustom, + native: false, + token_ids: vec![100, 101, 102], + token_type_names: vec![ + TypeTag::from_str("0x0000000000000000000000000000000000000000000000000000000000000abc::my_coin::MyCoin1").unwrap(), + TypeTag::from_str("0x0000000000000000000000000000000000000000000000000000000000000abc::my_coin::MyCoin2").unwrap(), + TypeTag::from_str("0x0000000000000000000000000000000000000000000000000000000000000abc::my_coin::MyCoin3").unwrap(), + ], + token_prices: vec![100_000_0000, 200_000_0000, 300_000_0000], + }); + client.request_sign_bridge_action(action).await.unwrap(); + } + + #[tokio::test] + async fn test_bridge_server_handle_add_tokens_on_evm_action_path() { + let client = setup(); + + let action = BridgeAction::AddTokensOnEvmAction(crate::types::AddTokensOnEvmAction { + nonce: 0, + chain_id: BridgeChainId::EthCustom, + native: false, + token_ids: vec![99, 100, 101], + token_addresses: vec![ + EthAddress::repeat_byte(1), + EthAddress::repeat_byte(2), + EthAddress::repeat_byte(3), + ], + token_sui_decimals: vec![5, 6, 7], + token_prices: vec![1_000_000_000, 2_000_000_000, 3_000_000_000], + }); + client.request_sign_bridge_action(action).await.unwrap(); + } + fn setup() -> BridgeClient { let mock = BridgeRequestMockHandler::new(); let (_handles, authorities, mut secrets) = diff --git a/crates/sui-bridge/src/storage.rs b/crates/sui-bridge/src/storage.rs index 3a1ede1b92001..12ae0454e970f 100644 --- a/crates/sui-bridge/src/storage.rs +++ b/crates/sui-bridge/src/storage.rs @@ -108,10 +108,8 @@ impl BridgeOrchestratorTables { .map_err(|e| BridgeError::StorageError(format!("Couldn't write batch: {:?}", e))) } - pub fn get_all_pending_actions( - &self, - ) -> BridgeResult> { - Ok(self.pending_actions.unbounded_iter().collect()) + pub fn get_all_pending_actions(&self) -> HashMap { + self.pending_actions.unbounded_iter().collect() } pub fn get_sui_event_cursors( @@ -151,12 +149,28 @@ mod tests { let temp_dir = tempfile::tempdir().unwrap(); let store = BridgeOrchestratorTables::new(temp_dir.path()); - let action1 = get_test_sui_to_eth_bridge_action(None, Some(0), Some(99), Some(10000)); + let action1 = get_test_sui_to_eth_bridge_action( + None, + Some(0), + Some(99), + Some(10000), + None, + None, + None, + ); - let action2 = get_test_sui_to_eth_bridge_action(None, Some(2), Some(100), Some(10000)); + let action2 = get_test_sui_to_eth_bridge_action( + None, + Some(2), + Some(100), + Some(10000), + None, + None, + None, + ); // in the beginning it's empty - let actions = store.get_all_pending_actions().unwrap(); + let actions = store.get_all_pending_actions(); assert!(actions.is_empty()); // remove non existing entry is ok @@ -166,7 +180,7 @@ mod tests { .insert_pending_actions(&vec![action1.clone(), action2.clone()]) .unwrap(); - let actions = store.get_all_pending_actions().unwrap(); + let actions = store.get_all_pending_actions(); assert_eq!( actions, HashMap::from_iter(vec![ @@ -177,7 +191,7 @@ mod tests { // insert an existing action is ok store.insert_pending_actions(&[action1.clone()]).unwrap(); - let actions = store.get_all_pending_actions().unwrap(); + let actions = store.get_all_pending_actions(); assert_eq!( actions, HashMap::from_iter(vec![ @@ -188,7 +202,7 @@ mod tests { // remove action 2 store.remove_pending_actions(&[action2.digest()]).unwrap(); - let actions = store.get_all_pending_actions().unwrap(); + let actions = store.get_all_pending_actions(); assert_eq!( actions, HashMap::from_iter(vec![(action1.digest(), action1.clone())]) @@ -196,7 +210,7 @@ mod tests { // remove action 1 store.remove_pending_actions(&[action1.digest()]).unwrap(); - let actions = store.get_all_pending_actions().unwrap(); + let actions = store.get_all_pending_actions(); assert!(actions.is_empty()); // update eth event cursor diff --git a/crates/sui-bridge/src/sui_client.rs b/crates/sui-bridge/src/sui_client.rs index 687fd2c89fba7..7b6f80d21b58b 100644 --- a/crates/sui-bridge/src/sui_client.rs +++ b/crates/sui-bridge/src/sui_client.rs @@ -4,18 +4,20 @@ // TODO remove when integrated #![allow(unused)] -use std::str::from_utf8; -use std::str::FromStr; -use std::time::Duration; - use anyhow::anyhow; use async_trait::async_trait; use axum::response::sse::Event; +use core::panic; use ethers::types::{Address, U256}; use fastcrypto::traits::KeyPair; use fastcrypto::traits::ToFromBytes; -use once_cell::sync::OnceCell; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::str::from_utf8; +use std::str::FromStr; +use std::time::Duration; +use sui_json_rpc_api::BridgeReadApiClient; +use sui_json_rpc_types::DevInspectResults; use sui_json_rpc_types::{EventFilter, Page, SuiData, SuiEvent}; use sui_json_rpc_types::{ EventPage, SuiObjectDataOptions, SuiTransactionBlockResponse, @@ -23,6 +25,15 @@ use sui_json_rpc_types::{ }; use sui_sdk::{SuiClient as SuiSdkClient, SuiClientBuilder}; use sui_types::base_types::ObjectRef; +use sui_types::base_types::SequenceNumber; +use sui_types::bridge::get_bridge; +use sui_types::bridge::BridgeCommitteeSummary; +use sui_types::bridge::BridgeInnerDynamicField; +use sui_types::bridge::BridgeRecordDyanmicField; +use sui_types::bridge::BridgeSummary; +use sui_types::bridge::BridgeTreasurySummary; +use sui_types::bridge::MoveTypeBridgeCommittee; +use sui_types::bridge::MoveTypeCommitteeMember; use sui_types::collection_types::LinkedTableNode; use sui_types::crypto::get_key_pair; use sui_types::dynamic_field::DynamicFieldName; @@ -32,65 +43,49 @@ use sui_types::error::UserInputError; use sui_types::event; use sui_types::gas_coin::GasCoin; use sui_types::object::{Object, Owner}; +use sui_types::transaction::Argument; +use sui_types::transaction::CallArg; +use sui_types::transaction::Command; +use sui_types::transaction::ObjectArg; +use sui_types::transaction::ProgrammableMoveCall; +use sui_types::transaction::ProgrammableTransaction; use sui_types::transaction::Transaction; +use sui_types::transaction::TransactionKind; use sui_types::TypeTag; +use sui_types::BRIDGE_PACKAGE_ID; +use sui_types::SUI_BRIDGE_OBJECT_ID; use sui_types::{ base_types::{ObjectID, SuiAddress}, digests::TransactionDigest, event::EventID, Identifier, }; +use sui_types::{bridge, parse_sui_type_tag}; use tap::TapFallible; +use tokio::sync::OnceCell; use tracing::{error, warn}; use crate::crypto::BridgeAuthorityPublicKey; use crate::error::{BridgeError, BridgeResult}; use crate::events::SuiBridgeEvent; use crate::retry_with_max_elapsed_time; -use crate::sui_transaction_builder::get_bridge_package_id; use crate::types::BridgeActionStatus; -use crate::types::BridgeInnerDynamicField; -use crate::types::BridgeRecordDynamicField; -use crate::types::MoveTypeBridgeMessageKey; -use crate::types::MoveTypeBridgeRecord; -use crate::types::{ - BridgeAction, BridgeAuthority, BridgeCommittee, MoveTypeBridgeCommittee, MoveTypeBridgeInner, - MoveTypeCommitteeMember, -}; - -// TODO: once we have bridge package on sui framework, we can hardcode the actual -// bridge dynamic field object id (not 0x9 or dynamic field wrapper) and update -// along with software upgrades. -// Or do we always retrieve from 0x9? We can figure this out before the first uggrade. -fn get_bridge_object_id() -> &'static ObjectID { - static BRIDGE_OBJ_ID: OnceCell = OnceCell::new(); - BRIDGE_OBJ_ID.get_or_init(|| { - let bridge_object_id = - std::env::var("BRIDGE_OBJECT_ID").expect("Expect BRIDGE_OBJECT_ID env var set"); - ObjectID::from_hex_literal(&bridge_object_id) - .expect("BRIDGE_OBJECT_ID must be a valid hex string") - }) -} - -// object id of BridgeRecord, this is wrapped in the bridge inner object. -// TODO: once we have bridge package on sui framework, we can hardcode the actual id. -fn get_bridge_record_id() -> &'static ObjectID { - static BRIDGE_RECORD_ID: OnceCell = OnceCell::new(); - BRIDGE_RECORD_ID.get_or_init(|| { - let bridge_record_id = - std::env::var("BRIDGE_RECORD_ID").expect("Expect BRIDGE_RECORD_ID env var set"); - ObjectID::from_hex_literal(&bridge_record_id) - .expect("BRIDGE_RECORD_ID must be a valid hex string") - }) -} +use crate::types::{BridgeAction, BridgeAuthority, BridgeCommittee}; pub struct SuiClient

{ inner: P, } -impl SuiClient { +pub type SuiBridgeClient = SuiClient; + +impl SuiBridgeClient { pub async fn new(rpc_url: &str) -> anyhow::Result { - let inner = SuiClientBuilder::default().build(rpc_url).await?; + let inner = SuiClientBuilder::default() + .build(rpc_url) + .await + .map_err(|e| { + anyhow!("Can't establish connection with Sui Rpc {rpc_url}. Error: {e}") + })?; let self_ = Self { inner }; self_.describe().await?; Ok(self_) @@ -115,13 +110,31 @@ where Ok(()) } + /// Get the mutable bridge object arg on chain. + // We retry a few times in case of errors. If it fails eventually, we panic. + // In generaly it's safe to call in the beginning of the program. + // After the first call, the result is cached since the value should never change. + pub async fn get_mutable_bridge_object_arg_must_succeed(&self) -> ObjectArg { + static ARG: OnceCell = OnceCell::const_new(); + *ARG.get_or_init(|| async move { + let Ok(Ok(bridge_object_arg)) = retry_with_max_elapsed_time!( + self.inner.get_mutable_bridge_object_arg(), + Duration::from_secs(30) + ) else { + panic!("Failed to get bridge object arg after retries"); + }; + bridge_object_arg + }) + .await + } + /// Query emitted Events that are defined in the given Move Module. pub async fn query_events_by_module( &self, package: ObjectID, module: Identifier, // cursor is exclusive - cursor: EventID, + cursor: Option, ) -> BridgeResult> { let filter = EventFilter::MoveEventModule { package, @@ -150,7 +163,7 @@ where let event = events .get(event_idx as usize) .ok_or(BridgeError::NoBridgeEventsInTxPosition)?; - if event.type_.address.as_ref() != get_bridge_package_id().as_ref() { + if event.type_.address.as_ref() != BRIDGE_PACKAGE_ID.as_ref() { return Err(BridgeError::BridgeEventInUnrecognizedSuiPackage); } let bridge_event = SuiBridgeEvent::try_from_sui_event(event)? @@ -161,22 +174,86 @@ where .ok_or(BridgeError::BridgeEventNotActionable) } - // TODO: expose this API to jsonrpc like system state query + pub async fn get_bridge_summary(&self) -> BridgeResult { + self.inner + .get_bridge_summary() + .await + .map_err(|e| BridgeError::InternalError(format!("Can't get bridge committee: {e}"))) + } + + pub async fn get_treasury_summary(&self) -> BridgeResult { + Ok(self.get_bridge_summary().await?.treasury) + } + + pub async fn get_token_id_map(&self) -> BridgeResult> { + self.get_bridge_summary() + .await? + .treasury + .id_token_type_map + .into_iter() + .map(|(id, name)| { + parse_sui_type_tag(&format!("0x{name}")) + .map(|name| (id, name)) + .map_err(|e| { + BridgeError::InternalError(format!( + "Failed to retrieve token id mapping: {e}, type name: {name}" + )) + }) + }) + .collect() + } + + pub async fn get_notional_values(&self) -> BridgeResult> { + let bridge_summary = self.get_bridge_summary().await?; + bridge_summary + .treasury + .id_token_type_map + .iter() + .map(|(id, type_name)| { + bridge_summary + .treasury + .supported_tokens + .iter() + .find_map(|(tn, metadata)| { + if type_name == tn { + Some((*id, metadata.notional_value)) + } else { + None + } + }) + .ok_or(BridgeError::InternalError( + "Error encountered when retrieving token notional values.".into(), + )) + }) + .collect() + } + + // TODO: cache this + pub async fn get_bridge_record_id(&self) -> BridgeResult { + self.inner + .get_bridge_summary() + .await + .map_err(|e| BridgeError::InternalError(format!("Can't get bridge committee: {e}"))) + .map(|bridge_summary| bridge_summary.bridge_records_id) + } + pub async fn get_bridge_committee(&self) -> BridgeResult { - let move_type_bridge_committee = - self.inner.get_bridge_committee().await.map_err(|e| { + let bridge_summary = + self.inner.get_bridge_summary().await.map_err(|e| { BridgeError::InternalError(format!("Can't get bridge committee: {e}")) })?; + let move_type_bridge_committee = bridge_summary.committee; + let mut authorities = vec![]; // TODO: move this to MoveTypeBridgeCommittee - for member in move_type_bridge_committee.members.contents { + for (_, member) in move_type_bridge_committee.members { let MoveTypeCommitteeMember { sui_address, bridge_pubkey_bytes, voting_power, http_rest_url, blocklisted, - } = member.value; + } = member; let pubkey = BridgeAuthorityPublicKey::from_bytes(&bridge_pubkey_bytes)?; let base_url = from_utf8(&http_rest_url).unwrap_or_else(|e| { warn!( @@ -195,6 +272,10 @@ where BridgeCommittee::new(authorities) } + pub async fn get_chain_identifier(&self) -> BridgeResult { + Ok(self.inner.get_chain_identifier().await?) + } + pub async fn execute_transaction_block_with_effects( &self, tx: sui_types::transaction::Transaction, @@ -202,23 +283,61 @@ where self.inner.execute_transaction_block_with_effects(tx).await } + // TODO: this function is very slow (seconds) in tests, we need to optimize it pub async fn get_token_transfer_action_onchain_status_until_success( &self, - action: &BridgeAction, + source_chain_id: u8, + seq_number: u64, ) -> BridgeActionStatus { + let now = std::time::Instant::now(); loop { + let bridge_object_arg = self.get_mutable_bridge_object_arg_must_succeed().await; let Ok(Ok(status)) = retry_with_max_elapsed_time!( - self.inner.get_token_transfer_action_onchain_status(action), + self.inner.get_token_transfer_action_onchain_status( + bridge_object_arg, + source_chain_id, + seq_number + ), Duration::from_secs(30) ) else { // TODO: add metrics and fire alert - error!("Failed to get action onchain status for: {:?}", action); + error!( + source_chain_id, + seq_number, "Failed to get token transfer action onchain status" + ); continue; }; return status; } } + pub async fn get_token_transfer_action_onchain_signatures_until_success( + &self, + source_chain_id: u8, + seq_number: u64, + ) -> Option>> { + let now = std::time::Instant::now(); + loop { + let bridge_object_arg = self.get_mutable_bridge_object_arg_must_succeed().await; + let Ok(Ok(sigs)) = retry_with_max_elapsed_time!( + self.inner.get_token_transfer_action_onchain_signatures( + bridge_object_arg, + source_chain_id, + seq_number + ), + Duration::from_secs(30) + ) else { + // TODO: add metrics and fire alert + error!( + source_chain_id, + seq_number, "Failed to get token transfer action onchain signatures" + ); + continue; + }; + return sigs; + } + } + pub async fn get_gas_data_panic_if_not_gas( &self, gas_object_id: ObjectID, @@ -236,7 +355,7 @@ pub trait SuiClientInner: Send + Sync { async fn query_events( &self, query: EventFilter, - cursor: EventID, + cursor: Option, ) -> Result; async fn get_events_by_tx_digest( @@ -248,7 +367,9 @@ pub trait SuiClientInner: Send + Sync { async fn get_latest_checkpoint_sequence_number(&self) -> Result; - async fn get_bridge_committee(&self) -> Result; + async fn get_mutable_bridge_object_arg(&self) -> Result; + + async fn get_bridge_summary(&self) -> Result; async fn execute_transaction_block_with_effects( &self, @@ -257,9 +378,18 @@ pub trait SuiClientInner: Send + Sync { async fn get_token_transfer_action_onchain_status( &self, - action: &BridgeAction, + bridge_object_arg: ObjectArg, + source_chain_id: u8, + seq_number: u64, ) -> Result; + async fn get_token_transfer_action_onchain_signatures( + &self, + bridge_object_arg: ObjectArg, + source_chain_id: u8, + seq_number: u64, + ) -> Result>>, BridgeError>; + async fn get_gas_data_panic_if_not_gas( &self, gas_object_id: ObjectID, @@ -273,10 +403,10 @@ impl SuiClientInner for SuiSdkClient { async fn query_events( &self, query: EventFilter, - cursor: EventID, + cursor: Option, ) -> Result { self.event_api() - .query_events(query, Some(cursor), None, false) + .query_events(query, cursor, None, false) .await } @@ -297,75 +427,136 @@ impl SuiClientInner for SuiSdkClient { .await } - // TODO: Add a test for this - async fn get_bridge_committee(&self) -> Result { - let object_id = *get_bridge_object_id(); - let bcs_bytes = self.read_api().get_move_object_bcs(object_id).await?; - let bridge_dynamic_field: BridgeInnerDynamicField = bcs::from_bytes(&bcs_bytes)?; - Ok(bridge_dynamic_field.value.committee) + async fn get_mutable_bridge_object_arg(&self) -> Result { + let initial_shared_version = self + .http() + .get_bridge_object_initial_shared_version() + .await?; + Ok(ObjectArg::SharedObject { + id: SUI_BRIDGE_OBJECT_ID, + initial_shared_version: SequenceNumber::from_u64(initial_shared_version), + mutable: true, + }) + } + + async fn get_bridge_summary(&self) -> Result { + self.http().get_latest_bridge().await.map_err(|e| e.into()) } async fn get_token_transfer_action_onchain_status( &self, - action: &BridgeAction, + bridge_object_arg: ObjectArg, + source_chain_id: u8, + seq_number: u64, ) -> Result { - match &action { - BridgeAction::SuiToEthBridgeAction(_) | BridgeAction::EthToSuiBridgeAction(_) => (), - _ => return Err(BridgeError::ActionIsNotTokenTransferAction), + let pt = ProgrammableTransaction { + inputs: vec![ + CallArg::Object(bridge_object_arg), + CallArg::Pure(bcs::to_bytes(&source_chain_id).unwrap()), + CallArg::Pure(bcs::to_bytes(&seq_number).unwrap()), + ], + commands: vec![Command::MoveCall(Box::new(ProgrammableMoveCall { + package: BRIDGE_PACKAGE_ID, + module: Identifier::new("bridge").unwrap(), + function: Identifier::new("get_token_transfer_action_status").unwrap(), + type_arguments: vec![], + arguments: vec![Argument::Input(0), Argument::Input(1), Argument::Input(2)], + }))], }; - let package_id = *get_bridge_package_id(); - let key = serde_json::json!( - { - // u64 is represented as string - "bridge_seq_num": action.seq_number().to_string(), - "message_type": action.action_type() as u8, - "source_chain": action.chain_id() as u8, - } - ); - let status_object_id = match self + let kind = TransactionKind::programmable(pt.clone()); + let resp = self .read_api() - .get_dynamic_field_object( - *get_bridge_record_id(), - DynamicFieldName { - type_: TypeTag::from_str(&format!( - "{:?}::message::BridgeMessageKey", - package_id - )) - .unwrap(), - value: key.clone(), - }, - ) - .await? - .into_object() - { - Ok(object) => object.object_id, - Err(SuiObjectResponseError::DynamicFieldNotFound { .. }) => { - return Ok(BridgeActionStatus::RecordNotFound) - } - other => { - return Err(BridgeError::Generic(format!( - "Can't get bridge action record dynamic field {:?}: {:?}", - key, other - ))) - } + .dev_inspect_transaction_block(SuiAddress::ZERO, kind, None, None, None) + .await?; + let DevInspectResults { + results, effects, .. + } = resp; + let Some(results) = results else { + return Err(BridgeError::Generic(format!( + "Can't get token transfer action status (empty results). effects: {:?}", + effects + ))); }; + let return_values = &results + .first() + .ok_or(BridgeError::Generic(format!( + "Can't get token transfer action status, results: {:?}", + results + )))? + .return_values; + let (value_bytes, _type_tag) = + return_values.first().ok_or(BridgeError::Generic(format!( + "Can't get token transfer action status, results: {:?}", + results + )))?; + let status = bcs::from_bytes::(value_bytes).map_err(|_e| { + BridgeError::Generic(format!( + "Can't parse token transfer action status as u8: {:?}", + results + )) + })?; + let status = BridgeActionStatus::try_from(status).map_err(|_e| { + BridgeError::Generic(format!( + "Can't parse token transfer action status as BridgeActionStatus: {:?}", + results + )) + })?; + + return Ok(status); + } - // get_dynamic_field_object does not return bcs, so we have to issue another query - let bcs_bytes = self + async fn get_token_transfer_action_onchain_signatures( + &self, + bridge_object_arg: ObjectArg, + source_chain_id: u8, + seq_number: u64, + ) -> Result>>, BridgeError> { + let pt = ProgrammableTransaction { + inputs: vec![ + CallArg::Object(bridge_object_arg), + CallArg::Pure(bcs::to_bytes(&source_chain_id).unwrap()), + CallArg::Pure(bcs::to_bytes(&seq_number).unwrap()), + ], + commands: vec![Command::MoveCall(Box::new(ProgrammableMoveCall { + package: BRIDGE_PACKAGE_ID, + module: Identifier::new("bridge").unwrap(), + function: Identifier::new("get_token_transfer_action_signatures").unwrap(), + type_arguments: vec![], + arguments: vec![Argument::Input(0), Argument::Input(1), Argument::Input(2)], + }))], + }; + let kind = TransactionKind::programmable(pt.clone()); + let resp = self .read_api() - .get_move_object_bcs(status_object_id) + .dev_inspect_transaction_block(SuiAddress::ZERO, kind, None, None, None) .await?; - let status_object: BridgeRecordDynamicField = bcs::from_bytes(&bcs_bytes)?; - - if status_object.value.value.claimed { - return Ok(BridgeActionStatus::Claimed); - } - - if status_object.value.value.verified_signatures.is_some() { - return Ok(BridgeActionStatus::Approved); - } - - return Ok(BridgeActionStatus::Pending); + let DevInspectResults { + results, effects, .. + } = resp; + let Some(results) = results else { + return Err(BridgeError::Generic(format!( + "Can't get token transfer action signatures (empty results). effects: {:?}", + effects + ))); + }; + let return_values = &results + .first() + .ok_or(BridgeError::Generic(format!( + "Can't get token transfer action signatures, results: {:?}", + results + )))? + .return_values; + let (value_bytes, _type_tag) = + return_values.first().ok_or(BridgeError::Generic(format!( + "Can't get token transfer action signatures, results: {:?}", + results + )))?; + bcs::from_bytes::>>>(value_bytes).map_err(|_e| { + BridgeError::Generic(format!( + "Can't parse token transfer action signatures as Option>>: {:?}", + results + )) + }) } async fn execute_transaction_block_with_effects( @@ -374,7 +565,7 @@ impl SuiClientInner for SuiSdkClient { ) -> Result { match self.quorum_driver_api().execute_transaction_block( tx, - SuiTransactionBlockResponseOptions::new().with_effects(), + SuiTransactionBlockResponseOptions::new().with_effects().with_events(), Some(sui_types::quorum_driver_types::ExecuteTransactionRequestType::WaitForEffectsCert), ).await { Ok(response) => Ok(response), @@ -413,14 +604,16 @@ impl SuiClientInner for SuiSdkClient { #[cfg(test)] mod tests { + use crate::crypto::BridgeAuthorityKeyPair; + use crate::BRIDGE_ENABLE_PROTOCOL_VERSION; use crate::{ - events::{EmittedSuiToEthTokenBridgeV1, MoveTokenBridgeEvent}, + events::{EmittedSuiToEthTokenBridgeV1, MoveTokenDepositedEvent}, sui_mock_client::SuiMockClient, test_utils::{ - bridge_token, get_test_sui_to_eth_bridge_action, mint_tokens, publish_bridge_package, - transfer_treasury_cap, + approve_action_with_validator_secrets, bridge_token, get_test_eth_to_sui_bridge_action, + get_test_sui_to_eth_bridge_action, }, - types::{BridgeActionType, BridgeChainId, SuiToEthBridgeAction, TokenId}, + types::{BridgeActionType, SuiToEthBridgeAction}, }; use ethers::{ abi::Token, @@ -432,6 +625,9 @@ mod tests { use move_core_types::account_address::AccountAddress; use prometheus::Registry; use std::{collections::HashSet, str::FromStr}; + use sui_json_rpc_types::SuiTransactionBlockEffectsAPI; + use sui_sdk::wallet_context; + use sui_types::bridge::{BridgeChainId, TOKEN_ID_SUI, TOKEN_ID_USDC}; use test_cluster::TestClusterBuilder; use super::*; @@ -456,23 +652,19 @@ mod tests { sui_address: SuiAddress::random_for_testing_only(), eth_chain_id: BridgeChainId::EthSepolia, eth_address: Address::random(), - token_id: TokenId::Sui, - amount: 100, + token_id: TOKEN_ID_SUI, + amount_sui_adjusted: 100, }; - let emitted_event_1 = MoveTokenBridgeEvent { - message_type: BridgeActionType::TokenTransfer as u8, + let emitted_event_1 = MoveTokenDepositedEvent { seq_num: sanitized_event_1.nonce, source_chain: sanitized_event_1.sui_chain_id as u8, sender_address: sanitized_event_1.sui_address.to_vec(), target_chain: sanitized_event_1.eth_chain_id as u8, target_address: sanitized_event_1.eth_address.as_bytes().to_vec(), - token_type: sanitized_event_1.token_id as u8, - amount: sanitized_event_1.amount, + token_type: sanitized_event_1.token_id, + amount_sui_adjusted: sanitized_event_1.amount_sui_adjusted, }; - // TODO: remove once we don't rely on env var to get package id - std::env::set_var("BRIDGE_PACKAGE_ID", "0x0b"); - let mut sui_event_1 = SuiEvent::random_for_testing(); sui_event_1.type_ = SuiToEthTokenBridgeV1.get().unwrap().clone(); sui_event_1.bcs = bcs::to_bytes(&emitted_event_1).unwrap(); @@ -555,61 +747,138 @@ mod tests { .unwrap_err(); } + // Test get_action_onchain_status. + // Use validator secrets to bridge USDC from Ethereum initially. + // TODO: we need an e2e test for this with published solidity contract and committee with BridgeNodes #[tokio::test] async fn test_get_action_onchain_status_for_sui_to_eth_transfer() { - let mut test_cluster = TestClusterBuilder::new().build().await; - let context = &mut test_cluster.wallet; - let sender = context.active_address().unwrap(); + telemetry_subscribers::init_for_testing(); + let mut bridge_keys = vec![]; + for _ in 0..=3 { + let (_, kp): (_, BridgeAuthorityKeyPair) = get_key_pair(); + bridge_keys.push(kp); + } + let mut test_cluster: test_cluster::TestCluster = TestClusterBuilder::new() + .with_protocol_version((BRIDGE_ENABLE_PROTOCOL_VERSION).into()) + .build_with_bridge(bridge_keys, true) + .await; - let treasury_caps = publish_bridge_package(context).await; let sui_client = SuiClient::new(&test_cluster.fullnode_handle.rpc_url) .await .unwrap(); + let bridge_authority_keys = test_cluster.bridge_authority_keys.take().unwrap(); - let action = get_test_sui_to_eth_bridge_action(None, None, None, None); + // Wait until committee is set up + test_cluster + .trigger_reconfiguration_if_not_yet_and_assert_bridge_committee_initialized() + .await; + let context = &mut test_cluster.wallet; + let sender = context.active_address().unwrap(); + let summary = sui_client.inner.get_bridge_summary().await.unwrap(); + let usdc_amount = 5000000; + let bridge_object_arg = sui_client + .get_mutable_bridge_object_arg_must_succeed() + .await; + let id_token_map = sui_client.get_token_id_map().await.unwrap(); + + // 1. Create a Eth -> Sui Transfer (recipient is sender address), approve with validator secrets and assert its status to be Claimed + let action = get_test_eth_to_sui_bridge_action(None, Some(usdc_amount), Some(sender)); + let usdc_object_ref = approve_action_with_validator_secrets( + context, + bridge_object_arg, + action.clone(), + &bridge_authority_keys, + Some(sender), + &id_token_map, + ) + .await + .unwrap(); let status = sui_client .inner - .get_token_transfer_action_onchain_status(&action) + .get_token_transfer_action_onchain_status( + bridge_object_arg, + action.chain_id() as u8, + action.seq_number(), + ) .await .unwrap(); - assert_eq!(status, BridgeActionStatus::RecordNotFound); + assert_eq!(status, BridgeActionStatus::Claimed); - // mint 1000 USDC - let amount = 1_000_000_000u64; - let (treasury_cap_obj_ref, usdc_coin_obj_ref) = mint_tokens( + // 2. Create a Sui -> Eth Transfer, approve with validator secrets and assert its status to be Approved + // We need to actually send tokens to bridge to initialize the record. + let eth_recv_address = EthAddress::random(); + let bridge_event = bridge_token( context, - treasury_caps[&TokenId::USDC], - amount, - TokenId::USDC, + eth_recv_address, + usdc_object_ref, + id_token_map.get(&TOKEN_ID_USDC).unwrap().clone(), + bridge_object_arg, ) .await; - - transfer_treasury_cap(context, treasury_cap_obj_ref, TokenId::USDC).await; - - let recv_address = EthAddress::random(); - let bridge_event = - bridge_token(context, recv_address, usdc_coin_obj_ref, TokenId::USDC).await; assert_eq!(bridge_event.nonce, 0); - assert_eq!(bridge_event.sui_chain_id, BridgeChainId::SuiLocalTest); - assert_eq!(bridge_event.eth_chain_id, BridgeChainId::EthLocalTest); - assert_eq!(bridge_event.eth_address, recv_address); + assert_eq!(bridge_event.sui_chain_id, BridgeChainId::SuiCustom); + assert_eq!(bridge_event.eth_chain_id, BridgeChainId::EthCustom); + assert_eq!(bridge_event.eth_address, eth_recv_address); assert_eq!(bridge_event.sui_address, sender); - assert_eq!(bridge_event.token_id, TokenId::USDC); - assert_eq!(bridge_event.amount, amount); - + assert_eq!(bridge_event.token_id, TOKEN_ID_USDC); + assert_eq!(bridge_event.amount_sui_adjusted, usdc_amount); + + let action = get_test_sui_to_eth_bridge_action( + None, + None, + Some(bridge_event.nonce), + Some(bridge_event.amount_sui_adjusted), + Some(bridge_event.sui_address), + Some(bridge_event.eth_address), + Some(TOKEN_ID_USDC), + ); let status = sui_client .inner - .get_token_transfer_action_onchain_status(&action) + .get_token_transfer_action_onchain_status( + bridge_object_arg, + action.chain_id() as u8, + action.seq_number(), + ) .await .unwrap(); + // At this point, the record is created and the status is Pending assert_eq!(status, BridgeActionStatus::Pending); - // TODO: run bridge committee and approve the action, then assert status is Approved - } + // Approve it and assert its status to be Approved + approve_action_with_validator_secrets( + context, + bridge_object_arg, + action.clone(), + &bridge_authority_keys, + None, + &id_token_map, + ) + .await; - #[tokio::test] - async fn test_get_action_onchain_status_for_eth_to_sui_transfer() { - // TODO: init an eth -> sui transfer, run bridge committee, approve the action, then assert status is Approved/Claimed + let status = sui_client + .inner + .get_token_transfer_action_onchain_status( + bridge_object_arg, + action.chain_id() as u8, + action.seq_number(), + ) + .await + .unwrap(); + assert_eq!(status, BridgeActionStatus::Approved); + + // 3. Create a random action and assert its status as NotFound + let action = + get_test_sui_to_eth_bridge_action(None, None, Some(100), None, None, None, None); + let status = sui_client + .inner + .get_token_transfer_action_onchain_status( + bridge_object_arg, + action.chain_id() as u8, + action.seq_number(), + ) + .await + .unwrap(); + assert_eq!(status, BridgeActionStatus::NotFound); } } diff --git a/crates/sui-bridge/src/sui_mock_client.rs b/crates/sui-bridge/src/sui_mock_client.rs index 43ef442ba64be..0a8d4dd37f477 100644 --- a/crates/sui-bridge/src/sui_mock_client.rs +++ b/crates/sui-bridge/src/sui_mock_client.rs @@ -4,6 +4,7 @@ //! A mock implementation of Sui JSON-RPC client. use crate::error::{BridgeError, BridgeResult}; +use crate::test_utils::DUMMY_MUTALBE_BRIDGE_OBJECT_ARG; use async_trait::async_trait; use std::collections::{HashMap, VecDeque}; use std::sync::{Arc, Mutex}; @@ -11,15 +12,17 @@ use sui_json_rpc_types::SuiTransactionBlockResponse; use sui_json_rpc_types::{EventFilter, EventPage, SuiEvent}; use sui_types::base_types::ObjectID; use sui_types::base_types::ObjectRef; +use sui_types::bridge::BridgeSummary; use sui_types::digests::TransactionDigest; use sui_types::event::EventID; use sui_types::gas_coin::GasCoin; use sui_types::object::Owner; +use sui_types::transaction::ObjectArg; use sui_types::transaction::Transaction; use sui_types::Identifier; use crate::sui_client::SuiClientInner; -use crate::types::{BridgeAction, BridgeActionDigest, BridgeActionStatus, MoveTypeBridgeCommittee}; +use crate::types::{BridgeAction, BridgeActionStatus}; /// Mock client used in test environments. #[allow(clippy::type_complexity)] @@ -28,15 +31,15 @@ pub struct SuiMockClient { // the top two fields do not change during tests so we don't need them to be Arc> chain_identifier: String, latest_checkpoint_sequence_number: u64, - events: Arc>>, - past_event_query_params: Arc>>, + events: Arc), EventPage>>>, + past_event_query_params: Arc)>>>, events_by_tx_digest: Arc, sui_sdk::error::Error>>>>, transaction_responses: Arc>>>, wildcard_transaction_response: Arc>>>, get_object_info: Arc>>, - onchain_status: Arc>>, + onchain_status: Arc>>, requested_transactions_tx: tokio::sync::broadcast::Sender, } @@ -67,7 +70,7 @@ impl SuiMockClient { self.events .lock() .unwrap() - .insert((package, module, cursor), events); + .insert((package, module, Some(cursor)), events); } pub fn add_events_by_tx_digest(&self, tx_digest: TransactionDigest, events: Vec) { @@ -99,7 +102,7 @@ impl SuiMockClient { self.onchain_status .lock() .unwrap() - .insert(action.digest(), status); + .insert((action.chain_id() as u8, action.seq_number()), status); } pub fn set_wildcard_transaction_response( @@ -132,7 +135,7 @@ impl SuiClientInner for SuiMockClient { async fn query_events( &self, query: EventFilter, - cursor: EventID, + cursor: Option, ) -> Result { let events = self.events.lock().unwrap(); match query { @@ -180,23 +183,48 @@ impl SuiClientInner for SuiMockClient { Ok(self.latest_checkpoint_sequence_number) } - async fn get_bridge_committee(&self) -> Result { - unimplemented!() + async fn get_mutable_bridge_object_arg(&self) -> Result { + Ok(DUMMY_MUTALBE_BRIDGE_OBJECT_ARG) + } + + async fn get_bridge_summary(&self) -> Result { + Ok(BridgeSummary { + bridge_version: 0, + message_version: 0, + chain_id: 0, + sequence_nums: vec![], + bridge_records_id: ObjectID::random(), + is_frozen: false, + limiter: Default::default(), + committee: Default::default(), + treasury: Default::default(), + }) } async fn get_token_transfer_action_onchain_status( &self, - action: &BridgeAction, + _bridge_object_arg: ObjectArg, + source_chain_id: u8, + seq_number: u64, ) -> Result { Ok(self .onchain_status .lock() .unwrap() - .get(&action.digest()) + .get(&(source_chain_id, seq_number)) .cloned() .unwrap_or(BridgeActionStatus::Pending)) } + async fn get_token_transfer_action_onchain_signatures( + &self, + _bridge_object_arg: ObjectArg, + _source_chain_id: u8, + _seq_number: u64, + ) -> Result>>, BridgeError> { + unimplemented!() + } + async fn execute_transaction_block_with_effects( &self, tx: Transaction, diff --git a/crates/sui-bridge/src/sui_syncer.rs b/crates/sui-bridge/src/sui_syncer.rs index ff214d156a9ae..6160124801720 100644 --- a/crates/sui-bridge/src/sui_syncer.rs +++ b/crates/sui-bridge/src/sui_syncer.rs @@ -1,30 +1,28 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -//! The SuiSyncer module is responsible for synchronizing Events emitted on Sui blockchain from -//! concerned bridge packages. +//! The SuiSyncer module is responsible for synchronizing Events emitted +//! on Sui blockchain from concerned modules of bridge package 0x9. use crate::{ error::BridgeResult, retry_with_max_elapsed_time, sui_client::{SuiClient, SuiClientInner}, - sui_transaction_builder::get_bridge_package_id, }; use mysten_metrics::spawn_logged_monitored_task; use std::{collections::HashMap, sync::Arc}; use sui_json_rpc_types::SuiEvent; +use sui_types::BRIDGE_PACKAGE_ID; use sui_types::{event::EventID, Identifier}; use tokio::{ task::JoinHandle, time::{self, Duration}, }; -// TODO: use the right package id -// const PACKAGE_ID: ObjectID = SUI_SYSTEM_PACKAGE_ID; const SUI_EVENTS_CHANNEL_SIZE: usize = 1000; /// Map from contract address to their start cursor (exclusive) -pub type SuiTargetModules = HashMap; +pub type SuiTargetModules = HashMap>; pub struct SuiSyncer { sui_client: Arc>, @@ -80,7 +78,7 @@ where // The module where interested events are defined. // Moudle is always of bridge package 0x9. module: Identifier, - mut cursor: EventID, + mut cursor: Option, events_sender: mysten_metrics::metered_channel::Sender<(Identifier, Vec)>, sui_client: Arc>, query_interval: Duration, @@ -91,7 +89,7 @@ where loop { interval.tick().await; let Ok(Ok(events)) = retry_with_max_elapsed_time!( - sui_client.query_events_by_module(*get_bridge_package_id(), module.clone(), cursor), + sui_client.query_events_by_module(BRIDGE_PACKAGE_ID, module.clone(), cursor), Duration::from_secs(10) ) else { tracing::error!("Failed to query events from sui client after retry"); @@ -100,16 +98,12 @@ where let len = events.data.len(); if len != 0 { - // Note: it's extremely critical to make sure the SuiEvents we send via this channel - // are complete per transaction level. Namely, we should never send a partial list - // of events for a transaction. Otherwise, we may end up missing events. - // See `sui_client.query_events_by_module` for how this is implemented. events_sender .send((module.clone(), events.data)) .await .expect("All Sui event channel receivers are closed"); if let Some(next) = events.next_cursor { - cursor = next; + cursor = Some(next); } tracing::info!(?module, ?cursor, "Observed {len} new Sui events"); } @@ -146,8 +140,8 @@ mod tests { add_event_response(&mock, module_bar.clone(), cursor, empty_events.clone()); let target_modules = HashMap::from_iter(vec![ - (module_foo.clone(), cursor), - (module_bar.clone(), cursor), + (module_foo.clone(), Some(cursor)), + (module_bar.clone(), Some(cursor)), ]); let interval = Duration::from_millis(200); let (_handles, mut events_rx) = SuiSyncer::new(client, target_modules) @@ -160,7 +154,7 @@ mod tests { // Module Foo has new events let mut event_1: SuiEvent = SuiEvent::random_for_testing(); - let package_id = *get_bridge_package_id(); + let package_id = BRIDGE_PACKAGE_ID; event_1.type_.address = package_id.into(); event_1.type_.module = module_foo.clone(); let module_foo_events_1: sui_json_rpc_types::Page = EventPage { @@ -223,11 +217,6 @@ mod tests { cursor: EventID, events: EventPage, ) { - mock.add_event_response( - *get_bridge_package_id(), - module.clone(), - cursor, - events.clone(), - ); + mock.add_event_response(BRIDGE_PACKAGE_ID, module.clone(), cursor, events.clone()); } } diff --git a/crates/sui-bridge/src/sui_transaction_builder.rs b/crates/sui-bridge/src/sui_transaction_builder.rs index 3e173576c462f..932e749f9fd58 100644 --- a/crates/sui-bridge/src/sui_transaction_builder.rs +++ b/crates/sui-bridge/src/sui_transaction_builder.rs @@ -1,115 +1,91 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -use std::{collections::HashMap, str::FromStr}; - -use fastcrypto::traits::ToFromBytes; +use fastcrypto::traits::{KeyPair, ToFromBytes}; use move_core_types::ident_str; -use once_cell::sync::OnceCell; -use sui_types::gas_coin::GAS; +use std::{collections::HashMap, str::FromStr}; +use sui_types::bridge::{ + BRIDGE_CREATE_ADD_TOKEN_ON_SUI_MESSAGE_FUNCTION_NAME, + BRIDGE_EXECUTE_SYSTEM_MESSAGE_FUNCTION_NAME, BRIDGE_MESSAGE_MODULE_NAME, BRIDGE_MODULE_NAME, +}; +use sui_types::transaction::CallArg; use sui_types::{ - base_types::{ObjectID, ObjectRef, SequenceNumber, SuiAddress}, + base_types::{ObjectRef, SuiAddress}, programmable_transaction_builder::ProgrammableTransactionBuilder, transaction::{ObjectArg, TransactionData}, TypeTag, }; +use sui_types::{Identifier, BRIDGE_PACKAGE_ID}; +use crate::crypto::BridgeAuthorityKeyPair; use crate::{ error::{BridgeError, BridgeResult}, - types::{BridgeAction, TokenId, VerifiedCertifiedBridgeAction}, + types::{BridgeAction, VerifiedCertifiedBridgeAction}, }; -// TODO: once we have bridge package on sui framework, we can hardcode the actual package id. -pub fn get_bridge_package_id() -> &'static ObjectID { - static BRIDGE_PACKAGE_ID: OnceCell = OnceCell::new(); - BRIDGE_PACKAGE_ID.get_or_init(|| match std::env::var("BRIDGE_PACKAGE_ID") { - Ok(id) => { - ObjectID::from_hex_literal(&id).expect("BRIDGE_PACKAGE_ID must be a valid hex string") - } - Err(_) => ObjectID::from_hex_literal("0x9").unwrap(), - }) -} - -// TODO: this should be hardcoded once we have bridge package on sui framework. -pub fn get_root_bridge_object_arg() -> &'static ObjectArg { - static ROOT_BRIDGE_OBJ_ID: OnceCell = OnceCell::new(); - ROOT_BRIDGE_OBJ_ID.get_or_init(|| { - let bridge_object_id = std::env::var("ROOT_BRIDGE_OBJECT_ID") - .expect("Expect ROOT_BRIDGE_OBJECT_ID env var set"); - let object_id = ObjectID::from_hex_literal(&bridge_object_id) - .expect("ROOT_BRIDGE_OBJECT_ID must be a valid hex string"); - let initial_shared_version = std::env::var("ROOT_BRIDGE_OBJECT_INITIAL_SHARED_VERSION") - .expect("Expect ROOT_BRIDGE_OBJECT_INITIAL_SHARED_VERSION env var set") - .parse::() - .expect("ROOT_BRIDGE_OBJECT_INITIAL_SHARED_VERSION must be a valid u64"); - ObjectArg::SharedObject { - id: object_id, - initial_shared_version: SequenceNumber::from_u64(initial_shared_version), - mutable: true, - } - }) -} - -// TODO: how do we generalize this thing more? -pub fn get_sui_token_type_tag(token_id: TokenId) -> TypeTag { - static TYPE_TAGS: OnceCell> = OnceCell::new(); - let type_tags = TYPE_TAGS.get_or_init(|| { - let package_id = get_bridge_package_id(); - let mut type_tags = HashMap::new(); - type_tags.insert(TokenId::Sui, GAS::type_tag()); - type_tags.insert( - TokenId::BTC, - TypeTag::from_str(&format!("{:?}::btc::BTC", package_id)).unwrap(), - ); - type_tags.insert( - TokenId::ETH, - TypeTag::from_str(&format!("{:?}::eth::ETH", package_id)).unwrap(), - ); - type_tags.insert( - TokenId::USDC, - TypeTag::from_str(&format!("{:?}::usdc::USDC", package_id)).unwrap(), - ); - type_tags.insert( - TokenId::USDT, - TypeTag::from_str(&format!("{:?}::usdt::USDT", package_id)).unwrap(), - ); - type_tags - }); - type_tags.get(&token_id).unwrap().clone() -} - // TODO: pass in gas price -pub fn build_transaction( +pub fn build_sui_transaction( client_address: SuiAddress, gas_object_ref: &ObjectRef, action: VerifiedCertifiedBridgeAction, + bridge_object_arg: ObjectArg, + sui_token_type_tags: &HashMap, ) -> BridgeResult { + // TODO: Check chain id? match action.data() { - BridgeAction::EthToSuiBridgeAction(_) => { - build_token_bridge_approve_transaction(client_address, gas_object_ref, action, true) - } - BridgeAction::SuiToEthBridgeAction(_) => { - build_token_bridge_approve_transaction(client_address, gas_object_ref, action, false) - } - BridgeAction::BlocklistCommitteeAction(_) => { - // TODO: handle this case - unimplemented!() - } - BridgeAction::EmergencyAction(_) => { - // TODO: handle this case - unimplemented!() - } - BridgeAction::LimitUpdateAction(_) => { - // TODO: handle this case - unimplemented!() - } - BridgeAction::AssetPriceUpdateAction(_) => { - // TODO: handle this case - unimplemented!() - } + BridgeAction::EthToSuiBridgeAction(_) => build_token_bridge_approve_transaction( + client_address, + gas_object_ref, + action, + true, + bridge_object_arg, + sui_token_type_tags, + ), + BridgeAction::SuiToEthBridgeAction(_) => build_token_bridge_approve_transaction( + client_address, + gas_object_ref, + action, + false, + bridge_object_arg, + sui_token_type_tags, + ), + BridgeAction::BlocklistCommitteeAction(_) => build_committee_blocklist_approve_transaction( + client_address, + gas_object_ref, + action, + bridge_object_arg, + ), + BridgeAction::EmergencyAction(_) => build_emergency_op_approve_transaction( + client_address, + gas_object_ref, + action, + bridge_object_arg, + ), + BridgeAction::LimitUpdateAction(_) => build_limit_update_approve_transaction( + client_address, + gas_object_ref, + action, + bridge_object_arg, + ), + BridgeAction::AssetPriceUpdateAction(_) => build_asset_price_update_approve_transaction( + client_address, + gas_object_ref, + action, + bridge_object_arg, + ), BridgeAction::EvmContractUpgradeAction(_) => { - // TODO: handle this case - unimplemented!() + // It does not need a Sui tranaction to execute EVM contract upgrade + unreachable!() + } + BridgeAction::AddTokensOnSuiAction(_) => build_add_tokens_on_sui_transaction( + client_address, + gas_object_ref, + action, + bridge_object_arg, + ), + BridgeAction::AddTokensOnEvmAction(_) => { + // It does not need a Sui tranaction to add tokens on EVM + unreachable!() } } } @@ -120,6 +96,8 @@ fn build_token_bridge_approve_transaction( gas_object_ref: &ObjectRef, action: VerifiedCertifiedBridgeAction, claim: bool, + bridge_object_arg: ObjectArg, + sui_token_type_tags: &HashMap, ) -> BridgeResult { let (bridge_action, sigs) = action.into_inner().into_data_and_sig(); let mut builder = ProgrammableTransactionBuilder::new(); @@ -135,7 +113,7 @@ fn build_token_bridge_approve_transaction( bridge_event.eth_chain_id, bridge_event.eth_address.to_fixed_bytes().to_vec(), bridge_event.token_id, - bridge_event.amount, + bridge_event.amount_sui_adjusted, ) } BridgeAction::EthToSuiBridgeAction(a) => { @@ -147,7 +125,7 @@ fn build_token_bridge_approve_transaction( bridge_event.sui_chain_id, bridge_event.sui_address.to_vec(), bridge_event.token_id, - bridge_event.amount, + bridge_event.sui_adjusted_amount, ) } _ => unreachable!(), @@ -168,11 +146,11 @@ fn build_token_bridge_approve_transaction( target, e )) })?; - let arg_token_type = builder.pure(token_type as u8).unwrap(); + let arg_token_type = builder.pure(token_type).unwrap(); let amount = builder.pure(amount).unwrap(); let arg_msg = builder.programmable_move_call( - *get_bridge_package_id(), + BRIDGE_PACKAGE_ID, ident_str!("message").to_owned(), ident_str!("create_token_bridge_message").to_owned(), vec![], @@ -187,8 +165,9 @@ fn build_token_bridge_approve_transaction( ], ); - // Unwrap: this should not fail - let arg_bridge = builder.obj(*get_root_bridge_object_arg()).unwrap(); + // Unwrap: these should not fail + let arg_bridge = builder.obj(bridge_object_arg).unwrap(); + let arg_clock = builder.input(CallArg::CLOCK_IMM).unwrap(); let mut sig_bytes = vec![]; for (_, sig) in sigs.signatures { @@ -202,20 +181,23 @@ fn build_token_bridge_approve_transaction( })?; builder.programmable_move_call( - *get_bridge_package_id(), - ident_str!("bridge").to_owned(), - ident_str!("approve_bridge_message").to_owned(), + BRIDGE_PACKAGE_ID, + sui_types::bridge::BRIDGE_MODULE_NAME.to_owned(), + ident_str!("approve_token_transfer").to_owned(), vec![], vec![arg_bridge, arg_msg, arg_signatures], ); if claim { builder.programmable_move_call( - *get_bridge_package_id(), - ident_str!("bridge").to_owned(), + BRIDGE_PACKAGE_ID, + sui_types::bridge::BRIDGE_MODULE_NAME.to_owned(), ident_str!("claim_and_transfer_token").to_owned(), - vec![get_sui_token_type_tag(token_type)], - vec![arg_bridge, source_chain, seq_num], + vec![sui_token_type_tags + .get(&token_type) + .ok_or(BridgeError::UnknownTokenId(token_type))? + .clone()], + vec![arg_bridge, arg_clock, source_chain, seq_num], ); } @@ -225,8 +207,761 @@ fn build_token_bridge_approve_transaction( client_address, vec![*gas_object_ref], pt, - 15_000_000, + 100_000_000, + // TODO: use reference gas price + 1500, + )) +} + +// TODO: pass in gas price +fn build_emergency_op_approve_transaction( + client_address: SuiAddress, + gas_object_ref: &ObjectRef, + action: VerifiedCertifiedBridgeAction, + bridge_object_arg: ObjectArg, +) -> BridgeResult { + let (bridge_action, sigs) = action.into_inner().into_data_and_sig(); + + let mut builder = ProgrammableTransactionBuilder::new(); + + let (source_chain, seq_num, action_type) = match bridge_action { + BridgeAction::EmergencyAction(a) => (a.chain_id, a.nonce, a.action_type), + _ => unreachable!(), + }; + + // Unwrap: these should not fail + let source_chain = builder.pure(source_chain as u8).unwrap(); + let seq_num = builder.pure(seq_num).unwrap(); + let action_type = builder.pure(action_type as u8).unwrap(); + let arg_bridge = builder.obj(bridge_object_arg).unwrap(); + + let arg_msg = builder.programmable_move_call( + BRIDGE_PACKAGE_ID, + ident_str!("message").to_owned(), + ident_str!("create_emergency_op_message").to_owned(), + vec![], + vec![source_chain, seq_num, action_type], + ); + + let mut sig_bytes = vec![]; + for (_, sig) in sigs.signatures { + sig_bytes.push(sig.as_bytes().to_vec()); + } + let arg_signatures = builder.pure(sig_bytes.clone()).map_err(|e| { + BridgeError::BridgeSerializationError(format!( + "Failed to serialize signatures: {:?}. Err: {:?}", + sig_bytes, e + )) + })?; + + builder.programmable_move_call( + BRIDGE_PACKAGE_ID, + ident_str!("bridge").to_owned(), + ident_str!("execute_system_message").to_owned(), + vec![], + vec![arg_bridge, arg_msg, arg_signatures], + ); + + let pt = builder.finish(); + + Ok(TransactionData::new_programmable( + client_address, + vec![*gas_object_ref], + pt, + 100_000_000, + // TODO: use reference gas price + 1500, + )) +} + +// TODO: pass in gas price +fn build_committee_blocklist_approve_transaction( + client_address: SuiAddress, + gas_object_ref: &ObjectRef, + action: VerifiedCertifiedBridgeAction, + bridge_object_arg: ObjectArg, +) -> BridgeResult { + let (bridge_action, sigs) = action.into_inner().into_data_and_sig(); + + let mut builder = ProgrammableTransactionBuilder::new(); + + let (source_chain, seq_num, blocklist_type, blocklisted_members) = match bridge_action { + BridgeAction::BlocklistCommitteeAction(a) => { + (a.chain_id, a.nonce, a.blocklist_type, a.blocklisted_members) + } + _ => unreachable!(), + }; + + // Unwrap: these should not fail + let source_chain = builder.pure(source_chain as u8).unwrap(); + let seq_num = builder.pure(seq_num).unwrap(); + let blocklist_type = builder.pure(blocklist_type as u8).unwrap(); + let blocklisted_members = blocklisted_members + .into_iter() + .map(|m| m.to_eth_address().as_bytes().to_vec()) + .collect::>(); + let blocklisted_members = builder.pure(blocklisted_members).unwrap(); + let arg_bridge = builder.obj(bridge_object_arg).unwrap(); + + let arg_msg = builder.programmable_move_call( + BRIDGE_PACKAGE_ID, + ident_str!("message").to_owned(), + ident_str!("create_blocklist_message").to_owned(), + vec![], + vec![source_chain, seq_num, blocklist_type, blocklisted_members], + ); + + let mut sig_bytes = vec![]; + for (_, sig) in sigs.signatures { + sig_bytes.push(sig.as_bytes().to_vec()); + } + let arg_signatures = builder.pure(sig_bytes.clone()).map_err(|e| { + BridgeError::BridgeSerializationError(format!( + "Failed to serialize signatures: {:?}. Err: {:?}", + sig_bytes, e + )) + })?; + + builder.programmable_move_call( + BRIDGE_PACKAGE_ID, + ident_str!("bridge").to_owned(), + ident_str!("execute_system_message").to_owned(), + vec![], + vec![arg_bridge, arg_msg, arg_signatures], + ); + + let pt = builder.finish(); + + Ok(TransactionData::new_programmable( + client_address, + vec![*gas_object_ref], + pt, + 100_000_000, + // TODO: use reference gas price + 1500, + )) +} + +// TODO: pass in gas price +fn build_limit_update_approve_transaction( + client_address: SuiAddress, + gas_object_ref: &ObjectRef, + action: VerifiedCertifiedBridgeAction, + bridge_object_arg: ObjectArg, +) -> BridgeResult { + let (bridge_action, sigs) = action.into_inner().into_data_and_sig(); + + let mut builder = ProgrammableTransactionBuilder::new(); + + let (receiving_chain_id, seq_num, sending_chain_id, new_usd_limit) = match bridge_action { + BridgeAction::LimitUpdateAction(a) => { + (a.chain_id, a.nonce, a.sending_chain_id, a.new_usd_limit) + } + _ => unreachable!(), + }; + + // Unwrap: these should not fail + let receiving_chain_id = builder.pure(receiving_chain_id as u8).unwrap(); + let seq_num = builder.pure(seq_num).unwrap(); + let sending_chain_id = builder.pure(sending_chain_id as u8).unwrap(); + let new_usd_limit = builder.pure(new_usd_limit).unwrap(); + let arg_bridge = builder.obj(bridge_object_arg).unwrap(); + + let arg_msg = builder.programmable_move_call( + BRIDGE_PACKAGE_ID, + ident_str!("message").to_owned(), + ident_str!("create_update_bridge_limit_message").to_owned(), + vec![], + vec![receiving_chain_id, seq_num, sending_chain_id, new_usd_limit], + ); + + let mut sig_bytes = vec![]; + for (_, sig) in sigs.signatures { + sig_bytes.push(sig.as_bytes().to_vec()); + } + let arg_signatures = builder.pure(sig_bytes.clone()).map_err(|e| { + BridgeError::BridgeSerializationError(format!( + "Failed to serialize signatures: {:?}. Err: {:?}", + sig_bytes, e + )) + })?; + + builder.programmable_move_call( + BRIDGE_PACKAGE_ID, + ident_str!("bridge").to_owned(), + ident_str!("execute_system_message").to_owned(), + vec![], + vec![arg_bridge, arg_msg, arg_signatures], + ); + + let pt = builder.finish(); + + Ok(TransactionData::new_programmable( + client_address, + vec![*gas_object_ref], + pt, + 100_000_000, + // TODO: use reference gas price + 1500, + )) +} + +// TODO: pass in gas price +fn build_asset_price_update_approve_transaction( + client_address: SuiAddress, + gas_object_ref: &ObjectRef, + action: VerifiedCertifiedBridgeAction, + bridge_object_arg: ObjectArg, +) -> BridgeResult { + let (bridge_action, sigs) = action.into_inner().into_data_and_sig(); + + let mut builder = ProgrammableTransactionBuilder::new(); + + let (source_chain, seq_num, token_id, new_usd_price) = match bridge_action { + BridgeAction::AssetPriceUpdateAction(a) => { + (a.chain_id, a.nonce, a.token_id, a.new_usd_price) + } + _ => unreachable!(), + }; + + // Unwrap: these should not fail + let source_chain = builder.pure(source_chain as u8).unwrap(); + let token_id = builder.pure(token_id).unwrap(); + let seq_num = builder.pure(seq_num).unwrap(); + let new_price = builder.pure(new_usd_price).unwrap(); + let arg_bridge = builder.obj(bridge_object_arg).unwrap(); + + let arg_msg = builder.programmable_move_call( + BRIDGE_PACKAGE_ID, + ident_str!("message").to_owned(), + ident_str!("create_update_asset_price_message").to_owned(), + vec![], + vec![token_id, source_chain, seq_num, new_price], + ); + + let mut sig_bytes = vec![]; + for (_, sig) in sigs.signatures { + sig_bytes.push(sig.as_bytes().to_vec()); + } + let arg_signatures = builder.pure(sig_bytes.clone()).map_err(|e| { + BridgeError::BridgeSerializationError(format!( + "Failed to serialize signatures: {:?}. Err: {:?}", + sig_bytes, e + )) + })?; + + builder.programmable_move_call( + BRIDGE_PACKAGE_ID, + ident_str!("bridge").to_owned(), + ident_str!("execute_system_message").to_owned(), + vec![], + vec![arg_bridge, arg_msg, arg_signatures], + ); + + let pt = builder.finish(); + + Ok(TransactionData::new_programmable( + client_address, + vec![*gas_object_ref], + pt, + 100_000_000, + // TODO: use reference gas price + 1500, + )) +} + +// TODO: pass in gas price +pub fn build_add_tokens_on_sui_transaction( + client_address: SuiAddress, + gas_object_ref: &ObjectRef, + action: VerifiedCertifiedBridgeAction, + bridge_object_arg: ObjectArg, +) -> BridgeResult { + let (bridge_action, sigs) = action.into_inner().into_data_and_sig(); + + let mut builder = ProgrammableTransactionBuilder::new(); + + let (source_chain, seq_num, native, token_ids, token_type_names, token_prices) = + match bridge_action { + BridgeAction::AddTokensOnSuiAction(a) => ( + a.chain_id, + a.nonce, + a.native, + a.token_ids, + a.token_type_names, + a.token_prices, + ), + _ => unreachable!(), + }; + let token_type_names = token_type_names + .iter() + .map(|type_name| type_name.to_canonical_string(false)) + .collect::>(); + let source_chain = builder.pure(source_chain as u8).unwrap(); + let seq_num = builder.pure(seq_num).unwrap(); + let native_token = builder.pure(native).unwrap(); + let token_ids = builder.pure(token_ids).unwrap(); + let token_type_names = builder.pure(token_type_names).unwrap(); + let token_prices = builder.pure(token_prices).unwrap(); + + let message_arg = builder.programmable_move_call( + BRIDGE_PACKAGE_ID, + BRIDGE_MESSAGE_MODULE_NAME.into(), + BRIDGE_CREATE_ADD_TOKEN_ON_SUI_MESSAGE_FUNCTION_NAME.into(), + vec![], + vec![ + source_chain, + seq_num, + native_token, + token_ids, + token_type_names, + token_prices, + ], + ); + + let bridge_arg = builder.obj(bridge_object_arg).unwrap(); + + let mut sig_bytes = vec![]; + for (_, sig) in sigs.signatures { + sig_bytes.push(sig.as_bytes().to_vec()); + } + let sigs_arg = builder.pure(sig_bytes.clone()).unwrap(); + + builder.programmable_move_call( + BRIDGE_PACKAGE_ID, + BRIDGE_MODULE_NAME.into(), + BRIDGE_EXECUTE_SYSTEM_MESSAGE_FUNCTION_NAME.into(), + vec![], + vec![bridge_arg, message_arg, sigs_arg], + ); + + let pt = builder.finish(); + + Ok(TransactionData::new_programmable( + client_address, + vec![*gas_object_ref], + pt, + 100_000_000, // TODO: use reference gas price 1500, )) } + +pub fn build_committee_register_transaction( + validator_address: SuiAddress, + gas_object_ref: &ObjectRef, + bridge_object_arg: ObjectArg, + bridge_key: BridgeAuthorityKeyPair, + bridge_url: &str, + ref_gas_price: u64, +) -> BridgeResult { + let mut builder = ProgrammableTransactionBuilder::new(); + let system_state = builder.obj(ObjectArg::SUI_SYSTEM_MUT).unwrap(); + let bridge = builder.obj(bridge_object_arg).unwrap(); + let pub_key = bridge_key.public().as_bytes().to_vec(); + let bridge_pubkey = builder + .input(CallArg::Pure(bcs::to_bytes(&pub_key).unwrap())) + .unwrap(); + let url = builder + .input(CallArg::Pure(bcs::to_bytes(bridge_url.as_bytes()).unwrap())) + .unwrap(); + builder.programmable_move_call( + BRIDGE_PACKAGE_ID, + BRIDGE_MODULE_NAME.into(), + Identifier::from_str("committee_registration").unwrap(), + vec![], + vec![bridge, system_state, bridge_pubkey, url], + ); + let data = TransactionData::new_programmable( + validator_address, + vec![*gas_object_ref], + builder.finish(), + 1000000000, + ref_gas_price, + ); + Ok(data) +} + +#[cfg(test)] +mod tests { + use crate::crypto::BridgeAuthorityKeyPair; + use crate::sui_client::SuiClient; + use crate::types::BridgeAction; + use crate::types::EmergencyAction; + use crate::types::EmergencyActionType; + use crate::types::*; + use crate::{ + crypto::BridgeAuthorityPublicKeyBytes, + test_utils::{ + approve_action_with_validator_secrets, bridge_token, get_test_eth_to_sui_bridge_action, + get_test_sui_to_eth_bridge_action, + }, + BRIDGE_ENABLE_PROTOCOL_VERSION, + }; + use ethers::types::Address as EthAddress; + use std::collections::HashMap; + use sui_types::bridge::{BridgeChainId, TOKEN_ID_BTC, TOKEN_ID_USDC}; + use sui_types::crypto::get_key_pair; + use sui_types::crypto::ToFromBytes; + use test_cluster::TestClusterBuilder; + + #[tokio::test] + async fn test_build_sui_transaction_for_token_transfer() { + telemetry_subscribers::init_for_testing(); + let mut bridge_keys = vec![]; + for _ in 0..=3 { + let (_, kp): (_, BridgeAuthorityKeyPair) = get_key_pair(); + bridge_keys.push(kp); + } + let mut test_cluster: test_cluster::TestCluster = TestClusterBuilder::new() + .with_protocol_version((BRIDGE_ENABLE_PROTOCOL_VERSION).into()) + .build_with_bridge(bridge_keys, true) + .await; + + let sui_client = SuiClient::new(&test_cluster.fullnode_handle.rpc_url) + .await + .unwrap(); + let bridge_authority_keys = test_cluster.bridge_authority_keys.take().unwrap(); + + // Note: We don't call `sui_client.get_bridge_committee` here because it will err if the committee + // is not initialized during the construction of `BridgeCommittee`. + test_cluster + .trigger_reconfiguration_if_not_yet_and_assert_bridge_committee_initialized() + .await; + let context = &mut test_cluster.wallet; + let sender = context.active_address().unwrap(); + let usdc_amount = 5000000; + let bridge_object_arg = sui_client + .get_mutable_bridge_object_arg_must_succeed() + .await; + let id_token_map = sui_client.get_token_id_map().await.unwrap(); + + // 1. Test Eth -> Sui Transfer approval + let action = get_test_eth_to_sui_bridge_action(None, Some(usdc_amount), Some(sender)); + // `approve_action_with_validator_secrets` covers transaction building + let usdc_object_ref = approve_action_with_validator_secrets( + context, + bridge_object_arg, + action.clone(), + &bridge_authority_keys, + Some(sender), + &id_token_map, + ) + .await + .unwrap(); + + // 2. Test Sui -> Eth Transfer approval + let bridge_event = bridge_token( + context, + EthAddress::random(), + usdc_object_ref, + id_token_map.get(&TOKEN_ID_USDC).unwrap().clone(), + bridge_object_arg, + ) + .await; + + let action = get_test_sui_to_eth_bridge_action( + None, + None, + Some(bridge_event.nonce), + Some(bridge_event.amount_sui_adjusted), + Some(bridge_event.sui_address), + Some(bridge_event.eth_address), + Some(TOKEN_ID_USDC), + ); + // `approve_action_with_validator_secrets` covers transaction building + approve_action_with_validator_secrets( + context, + bridge_object_arg, + action.clone(), + &bridge_authority_keys, + None, + &id_token_map, + ) + .await; + } + + #[tokio::test] + async fn test_build_sui_transaction_for_emergency_op() { + telemetry_subscribers::init_for_testing(); + let mut bridge_keys = vec![]; + for _ in 0..=3 { + let (_, kp): (_, BridgeAuthorityKeyPair) = get_key_pair(); + bridge_keys.push(kp); + } + let mut test_cluster: test_cluster::TestCluster = TestClusterBuilder::new() + .with_protocol_version((BRIDGE_ENABLE_PROTOCOL_VERSION).into()) + .build_with_bridge(bridge_keys, true) + .await; + let sui_client = SuiClient::new(&test_cluster.fullnode_handle.rpc_url) + .await + .unwrap(); + let bridge_authority_keys = test_cluster.bridge_authority_keys.take().unwrap(); + + // Wait until committee is set up + test_cluster + .trigger_reconfiguration_if_not_yet_and_assert_bridge_committee_initialized() + .await; + let summary = sui_client.get_bridge_summary().await.unwrap(); + assert!(!summary.is_frozen); + + let context = &mut test_cluster.wallet; + let bridge_object_arg = sui_client + .get_mutable_bridge_object_arg_must_succeed() + .await; + let id_token_map = sui_client.get_token_id_map().await.unwrap(); + + // 1. Pause + let action = BridgeAction::EmergencyAction(EmergencyAction { + nonce: 0, + chain_id: BridgeChainId::SuiCustom, + action_type: EmergencyActionType::Pause, + }); + // `approve_action_with_validator_secrets` covers transaction building + approve_action_with_validator_secrets( + context, + bridge_object_arg, + action.clone(), + &bridge_authority_keys, + None, + &id_token_map, + ) + .await; + let summary = sui_client.get_bridge_summary().await.unwrap(); + assert!(summary.is_frozen); + + // 2. Unpause + let action = BridgeAction::EmergencyAction(EmergencyAction { + nonce: 1, + chain_id: BridgeChainId::SuiCustom, + action_type: EmergencyActionType::Unpause, + }); + // `approve_action_with_validator_secrets` covers transaction building + approve_action_with_validator_secrets( + context, + bridge_object_arg, + action.clone(), + &bridge_authority_keys, + None, + &id_token_map, + ) + .await; + let summary = sui_client.get_bridge_summary().await.unwrap(); + assert!(!summary.is_frozen); + } + + #[tokio::test] + async fn test_build_sui_transaction_for_committee_blocklist() { + telemetry_subscribers::init_for_testing(); + let mut bridge_keys = vec![]; + for _ in 0..=3 { + let (_, kp): (_, BridgeAuthorityKeyPair) = get_key_pair(); + bridge_keys.push(kp); + } + let mut test_cluster: test_cluster::TestCluster = TestClusterBuilder::new() + .with_protocol_version((BRIDGE_ENABLE_PROTOCOL_VERSION).into()) + .build_with_bridge(bridge_keys, true) + .await; + let sui_client = SuiClient::new(&test_cluster.fullnode_handle.rpc_url) + .await + .unwrap(); + let bridge_authority_keys = test_cluster.bridge_authority_keys.take().unwrap(); + + // Wait until committee is set up + test_cluster + .trigger_reconfiguration_if_not_yet_and_assert_bridge_committee_initialized() + .await; + let committee = sui_client.get_bridge_summary().await.unwrap().committee; + let victim = committee.members.first().unwrap().clone().1; + for member in committee.members { + assert!(!member.1.blocklisted); + } + + let context = &mut test_cluster.wallet; + let bridge_object_arg = sui_client + .get_mutable_bridge_object_arg_must_succeed() + .await; + let id_token_map = sui_client.get_token_id_map().await.unwrap(); + + // 1. blocklist The victim + let action = BridgeAction::BlocklistCommitteeAction(BlocklistCommitteeAction { + nonce: 0, + chain_id: BridgeChainId::SuiCustom, + blocklist_type: BlocklistType::Blocklist, + blocklisted_members: vec![BridgeAuthorityPublicKeyBytes::from_bytes( + &victim.bridge_pubkey_bytes, + ) + .unwrap()], + }); + // `approve_action_with_validator_secrets` covers transaction building + approve_action_with_validator_secrets( + context, + bridge_object_arg, + action.clone(), + &bridge_authority_keys, + None, + &id_token_map, + ) + .await; + let committee = sui_client.get_bridge_summary().await.unwrap().committee; + for member in committee.members { + if member.1.bridge_pubkey_bytes == victim.bridge_pubkey_bytes { + assert!(member.1.blocklisted); + } else { + assert!(!member.1.blocklisted); + } + } + + // 2. unblocklist the victim + let action = BridgeAction::BlocklistCommitteeAction(BlocklistCommitteeAction { + nonce: 1, + chain_id: BridgeChainId::SuiCustom, + blocklist_type: BlocklistType::Unblocklist, + blocklisted_members: vec![BridgeAuthorityPublicKeyBytes::from_bytes( + &victim.bridge_pubkey_bytes, + ) + .unwrap()], + }); + // `approve_action_with_validator_secrets` covers transaction building + approve_action_with_validator_secrets( + context, + bridge_object_arg, + action.clone(), + &bridge_authority_keys, + None, + &id_token_map, + ) + .await; + let committee = sui_client.get_bridge_summary().await.unwrap().committee; + for member in committee.members { + assert!(!member.1.blocklisted); + } + } + + #[tokio::test] + async fn test_build_sui_transaction_for_limit_update() { + telemetry_subscribers::init_for_testing(); + let mut bridge_keys = vec![]; + for _ in 0..=3 { + let (_, kp): (_, BridgeAuthorityKeyPair) = get_key_pair(); + bridge_keys.push(kp); + } + let mut test_cluster: test_cluster::TestCluster = TestClusterBuilder::new() + .with_protocol_version((BRIDGE_ENABLE_PROTOCOL_VERSION).into()) + .build_with_bridge(bridge_keys, true) + .await; + let sui_client = SuiClient::new(&test_cluster.fullnode_handle.rpc_url) + .await + .unwrap(); + let bridge_authority_keys = test_cluster.bridge_authority_keys.take().unwrap(); + + // Wait until committee is set up + test_cluster + .trigger_reconfiguration_if_not_yet_and_assert_bridge_committee_initialized() + .await; + let transfer_limit = sui_client + .get_bridge_summary() + .await + .unwrap() + .limiter + .transfer_limit + .into_iter() + .map(|(s, d, l)| ((s, d), l)) + .collect::>(); + + let context = &mut test_cluster.wallet; + let bridge_object_arg = sui_client + .get_mutable_bridge_object_arg_must_succeed() + .await; + let id_token_map = sui_client.get_token_id_map().await.unwrap(); + + // update limit + let action = BridgeAction::LimitUpdateAction(LimitUpdateAction { + nonce: 0, + chain_id: BridgeChainId::SuiCustom, + sending_chain_id: BridgeChainId::EthCustom, + new_usd_limit: 6_666_666 * USD_MULTIPLIER, // $1M USD + }); + // `approve_action_with_validator_secrets` covers transaction building + approve_action_with_validator_secrets( + context, + bridge_object_arg, + action.clone(), + &bridge_authority_keys, + None, + &id_token_map, + ) + .await; + let new_transfer_limit = sui_client + .get_bridge_summary() + .await + .unwrap() + .limiter + .transfer_limit; + for limit in new_transfer_limit { + if limit.0 == BridgeChainId::EthCustom && limit.1 == BridgeChainId::SuiCustom { + assert_eq!(limit.2, 6_666_666 * USD_MULTIPLIER); + } else { + assert_eq!(limit.2, *transfer_limit.get(&(limit.0, limit.1)).unwrap()); + } + } + } + + #[tokio::test] + async fn test_build_sui_transaction_for_price_update() { + telemetry_subscribers::init_for_testing(); + let mut bridge_keys = vec![]; + for _ in 0..=3 { + let (_, kp): (_, BridgeAuthorityKeyPair) = get_key_pair(); + bridge_keys.push(kp); + } + let mut test_cluster: test_cluster::TestCluster = TestClusterBuilder::new() + .with_protocol_version((BRIDGE_ENABLE_PROTOCOL_VERSION).into()) + .build_with_bridge(bridge_keys, true) + .await; + let sui_client = SuiClient::new(&test_cluster.fullnode_handle.rpc_url) + .await + .unwrap(); + let bridge_authority_keys = test_cluster.bridge_authority_keys.take().unwrap(); + + // Note: We don't call `sui_client.get_bridge_committee` here because it will err if the committee + // is not initialized during the construction of `BridgeCommittee`. + test_cluster + .trigger_reconfiguration_if_not_yet_and_assert_bridge_committee_initialized() + .await; + let notional_values = sui_client.get_notional_values().await.unwrap(); + assert_ne!(notional_values[&TOKEN_ID_USDC], 69_000 * USD_MULTIPLIER); + + let context = &mut test_cluster.wallet; + let bridge_object_arg = sui_client + .get_mutable_bridge_object_arg_must_succeed() + .await; + let id_token_map = sui_client.get_token_id_map().await.unwrap(); + + // update price + let action = BridgeAction::AssetPriceUpdateAction(AssetPriceUpdateAction { + nonce: 0, + chain_id: BridgeChainId::SuiCustom, + token_id: TOKEN_ID_BTC, + new_usd_price: 69_000 * USD_MULTIPLIER, // $69k USD + }); + // `approve_action_with_validator_secrets` covers transaction building + approve_action_with_validator_secrets( + context, + bridge_object_arg, + action.clone(), + &bridge_authority_keys, + None, + &id_token_map, + ) + .await; + let new_notional_values = sui_client.get_notional_values().await.unwrap(); + for (token_id, price) in new_notional_values { + if token_id == TOKEN_ID_BTC { + assert_eq!(price, 69_000 * USD_MULTIPLIER); + } else { + assert_eq!(price, *notional_values.get(&token_id).unwrap()); + } + } + } +} diff --git a/crates/sui-bridge/src/test_utils.rs b/crates/sui-bridge/src/test_utils.rs index 420356aaf8846..dff3c33b64f44 100644 --- a/crates/sui-bridge/src/test_utils.rs +++ b/crates/sui-bridge/src/test_utils.rs @@ -5,17 +5,17 @@ use crate::abi::EthToSuiTokenBridgeV1; use crate::eth_mock_provider::EthMockProvider; use crate::events::SuiBridgeEvent; use crate::server::mock_handler::run_mock_server; -use crate::sui_transaction_builder::{ - get_bridge_package_id, get_root_bridge_object_arg, get_sui_token_type_tag, +use crate::sui_transaction_builder::build_sui_transaction; +use crate::types::{ + BridgeCommitteeValiditySignInfo, CertifiedBridgeAction, VerifiedCertifiedBridgeAction, }; -use crate::types::BridgeInnerDynamicField; use crate::{ crypto::{BridgeAuthorityKeyPair, BridgeAuthorityPublicKey, BridgeAuthoritySignInfo}, events::EmittedSuiToEthTokenBridgeV1, server::mock_handler::BridgeRequestMockHandler, types::{ - BridgeAction, BridgeAuthority, BridgeChainId, EthToSuiBridgeAction, SignedBridgeAction, - SuiToEthBridgeAction, TokenId, + BridgeAction, BridgeAuthority, EthToSuiBridgeAction, SignedBridgeAction, + SuiToEthBridgeAction, }, }; use ethers::abi::{long_signature, ParamType}; @@ -27,22 +27,30 @@ use ethers::types::{ use fastcrypto::encoding::{Encoding, Hex}; use fastcrypto::traits::KeyPair; use hex_literal::hex; -use std::collections::BTreeMap; +use move_core_types::language_storage::TypeTag; +use std::collections::{BTreeMap, HashMap}; use std::net::IpAddr; use std::net::Ipv4Addr; use std::net::SocketAddr; -use std::path::PathBuf; use sui_config::local_ip_utils; -use sui_json_rpc_types::ObjectChange; +use sui_json_rpc_types::SuiTransactionBlockEffectsAPI; use sui_sdk::wallet_context::WalletContext; use sui_test_transaction_builder::TestTransactionBuilder; use sui_types::base_types::ObjectRef; +use sui_types::base_types::SequenceNumber; +use sui_types::bridge::{BridgeChainId, TOKEN_ID_USDC}; use sui_types::object::Owner; use sui_types::transaction::{CallArg, ObjectArg}; -use sui_types::SUI_FRAMEWORK_PACKAGE_ID; use sui_types::{base_types::SuiAddress, crypto::get_key_pair, digests::TransactionDigest}; +use sui_types::{BRIDGE_PACKAGE_ID, SUI_BRIDGE_OBJECT_ID}; use tokio::task::JoinHandle; +pub const DUMMY_MUTALBE_BRIDGE_OBJECT_ARG: ObjectArg = ObjectArg::SharedObject { + id: SUI_BRIDGE_OBJECT_ID, + initial_shared_version: SequenceNumber::from_u64(1), + mutable: true, +}; + pub fn get_test_authority_and_key( voting_power: u64, port: u16, @@ -63,23 +71,47 @@ pub fn get_test_authority_and_key( (authority, pubkey, kp) } +// TODO: make a builder for this pub fn get_test_sui_to_eth_bridge_action( sui_tx_digest: Option, sui_tx_event_index: Option, nonce: Option, - amount: Option, + amount_sui_adjusted: Option, + sender_address: Option, + recipient_address: Option, + token_id: Option, ) -> BridgeAction { BridgeAction::SuiToEthBridgeAction(SuiToEthBridgeAction { sui_tx_digest: sui_tx_digest.unwrap_or_else(TransactionDigest::random), sui_tx_event_index: sui_tx_event_index.unwrap_or(0), sui_bridge_event: EmittedSuiToEthTokenBridgeV1 { nonce: nonce.unwrap_or_default(), - sui_chain_id: BridgeChainId::SuiLocalTest, - sui_address: SuiAddress::random_for_testing_only(), - eth_chain_id: BridgeChainId::EthLocalTest, + sui_chain_id: BridgeChainId::SuiCustom, + sui_address: sender_address.unwrap_or_else(SuiAddress::random_for_testing_only), + eth_chain_id: BridgeChainId::EthCustom, + eth_address: recipient_address.unwrap_or_else(EthAddress::random), + token_id: token_id.unwrap_or(TOKEN_ID_USDC), + amount_sui_adjusted: amount_sui_adjusted.unwrap_or(100_000), + }, + }) +} + +pub fn get_test_eth_to_sui_bridge_action( + nonce: Option, + amount: Option, + sui_address: Option, +) -> BridgeAction { + BridgeAction::EthToSuiBridgeAction(EthToSuiBridgeAction { + eth_tx_hash: TxHash::random(), + eth_event_index: 0, + eth_bridge_event: EthToSuiTokenBridgeV1 { + eth_chain_id: BridgeChainId::EthCustom, + nonce: nonce.unwrap_or_default(), + sui_chain_id: BridgeChainId::SuiCustom, + token_id: TOKEN_ID_USDC, + sui_adjusted_amount: amount.unwrap_or(100_000), + sui_address: sui_address.unwrap_or_else(SuiAddress::random_for_testing_only), eth_address: EthAddress::random(), - token_id: TokenId::Sui, - amount: amount.unwrap_or(100_000), }, }) } @@ -188,16 +220,16 @@ pub fn get_test_log_and_action( tx_hash: TxHash, event_index: u16, ) -> (Log, BridgeAction) { - let token_code = 3u8; - let amount = 10000000u64; + let token_id = 3u8; + let sui_adjusted_amount = 10000000u64; let source_address = EthAddress::random(); let sui_address: SuiAddress = SuiAddress::random_for_testing_only(); let target_address = Hex::decode(&sui_address.to_string()).unwrap(); // Note: must use `encode` rather than `encode_packged` let encoded = ethers::abi::encode(&[ - // u8 is encoded as u256 in abi standard - ethers::abi::Token::Uint(ethers::types::U256::from(token_code)), - ethers::abi::Token::Uint(ethers::types::U256::from(amount)), + // u8/u64 is encoded as u256 in abi standard + ethers::abi::Token::Uint(ethers::types::U256::from(token_id)), + ethers::abi::Token::Uint(ethers::types::U256::from(sui_adjusted_amount)), ethers::abi::Token::Address(source_address), ethers::abi::Token::Bytes(target_address.clone()), ]); @@ -205,7 +237,7 @@ pub fn get_test_log_and_action( address: contract_address, topics: vec![ long_signature( - "TokensBridgedToSui", + "TokensDeposited", &[ ParamType::Uint(8), ParamType::Uint(64), @@ -237,8 +269,8 @@ pub fn get_test_log_and_action( eth_chain_id: BridgeChainId::try_from(topic_1[topic_1.len() - 1]).unwrap(), nonce: u64::from_be_bytes(log.topics[2].as_ref()[24..32].try_into().unwrap()), sui_chain_id: BridgeChainId::try_from(topic_3[topic_3.len() - 1]).unwrap(), - token_id: TokenId::try_from(token_code).unwrap(), - amount, + token_id, + sui_adjusted_amount, sui_address, eth_address: source_address, }, @@ -246,191 +278,115 @@ pub fn get_test_log_and_action( (log, bridge_action) } -pub async fn publish_bridge_package(context: &WalletContext) -> BTreeMap { - let (sender, gas_object) = context.get_one_gas_object().await.unwrap().unwrap(); - let gas_price = context.get_reference_gas_price().await.unwrap(); - - let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - path.extend(["..", "..", "examples", "move", "bridge"]); - - let txn = context.sign_transaction( - &TestTransactionBuilder::new(sender, gas_object, gas_price) - .publish(path) - .build(), - ); - let resp = context.execute_transaction_must_succeed(txn).await; - let object_changes = resp.object_changes.unwrap(); - let package_id = object_changes - .iter() - .find(|change| matches!(change, ObjectChange::Published { .. })) - .map(|change| change.object_id()) - .unwrap(); - - let mut treasury_caps = BTreeMap::new(); - object_changes.iter().for_each(|change| { - if let ObjectChange::Created { object_type, .. } = change { - let object_type_str = object_type.to_string(); - if object_type_str.contains("TreasuryCap") { - if object_type_str.contains("BTC") { - treasury_caps.insert(TokenId::BTC, change.object_ref()); - } else if object_type_str.contains("ETH") { - treasury_caps.insert(TokenId::ETH, change.object_ref()); - } else if object_type_str.contains("USDC") { - treasury_caps.insert(TokenId::USDC, change.object_ref()); - } else if object_type_str.contains("USDT") { - treasury_caps.insert(TokenId::USDT, change.object_ref()); - } - } - } - }); - - let root_bridge_object_ref = object_changes - .iter() - .find(|change| match change { - ObjectChange::Created { - object_type, owner, .. - } => { - object_type.to_string().contains("Bridge") && matches!(owner, Owner::Shared { .. }) - } - _ => false, - }) - .map(|change| change.object_ref()) - .unwrap(); - - let bridge_inner_object_ref = object_changes - .iter() - .find(|change| match change { - ObjectChange::Created { object_type, .. } => { - object_type.to_string().contains("BridgeInner") - } - _ => false, - }) - .map(|change| change.object_ref()) - .unwrap(); - - let client = context.get_client().await.unwrap(); - let bcs_bytes = client - .read_api() - .get_move_object_bcs(bridge_inner_object_ref.0) - .await - .unwrap(); - let bridge_inner_object: BridgeInnerDynamicField = bcs::from_bytes(&bcs_bytes).unwrap(); - let bridge_record_id = bridge_inner_object.value.bridge_records.id; - - // TODO: remove once we don't rely on env var to get package id - std::env::set_var("BRIDGE_PACKAGE_ID", package_id.to_string()); - std::env::set_var("BRIDGE_RECORD_ID", bridge_record_id.to_string()); - std::env::set_var( - "ROOT_BRIDGE_OBJECT_ID", - root_bridge_object_ref.0.to_string(), - ); - std::env::set_var( - "ROOT_BRIDGE_OBJECT_INITIAL_SHARED_VERSION", - u64::from(root_bridge_object_ref.1).to_string(), - ); - std::env::set_var("BRIDGE_OBJECT_ID", bridge_inner_object_ref.0.to_string()); - - treasury_caps -} - -pub async fn mint_tokens( - context: &mut WalletContext, - treasury_cap_ref: ObjectRef, - amount: u64, - token_id: TokenId, -) -> (ObjectRef, ObjectRef) { - let rgp = context.get_reference_gas_price().await.unwrap(); - let sender = context.active_address().unwrap(); - let gas_obj_ref = context.get_one_gas_object().await.unwrap().unwrap().1; - let tx = TestTransactionBuilder::new(sender, gas_obj_ref, rgp) - .move_call( - SUI_FRAMEWORK_PACKAGE_ID, - "coin", - "mint_and_transfer", - vec![ - CallArg::Object(ObjectArg::ImmOrOwnedObject(treasury_cap_ref)), - CallArg::Pure(bcs::to_bytes(&amount).unwrap()), - CallArg::Pure(sender.to_vec()), - ], - ) - .with_type_args(vec![get_sui_token_type_tag(token_id)]) - .build(); - let signed_tn = context.sign_transaction(&tx); - let resp = context.execute_transaction_must_succeed(signed_tn).await; - let object_changes = resp.object_changes.unwrap(); - - let treasury_cap_obj_ref = object_changes - .iter() - .find(|change| matches!(change, ObjectChange::Mutated { object_type, .. } if object_type.to_string().contains("TreasuryCap"))) - .map(|change| change.object_ref()) - .unwrap(); - - let minted_coin_obj_ref = object_changes - .iter() - .find(|change| matches!(change, ObjectChange::Created { .. })) - .map(|change| change.object_ref()) - .unwrap(); - - (treasury_cap_obj_ref, minted_coin_obj_ref) -} - -pub async fn transfer_treasury_cap( - context: &mut WalletContext, - treasury_cap_ref: ObjectRef, - token_id: TokenId, -) { - let rgp = context.get_reference_gas_price().await.unwrap(); - let sender = context.active_address().unwrap(); - let gas_object = context.get_one_gas_object().await.unwrap().unwrap().1; - let tx = TestTransactionBuilder::new(sender, gas_object, rgp) - .move_call( - *get_bridge_package_id(), - "bridge", - "add_treasury_cap", - vec![ - CallArg::Object(*get_root_bridge_object_arg()), - CallArg::Object(ObjectArg::ImmOrOwnedObject(treasury_cap_ref)), - ], - ) - .with_type_args(vec![get_sui_token_type_tag(token_id)]) - .build(); - let signed_tn = context.sign_transaction(&tx); - context.execute_transaction_must_succeed(signed_tn).await; -} - pub async fn bridge_token( context: &mut WalletContext, recv_address: EthAddress, token_ref: ObjectRef, - token_id: TokenId, + token_type: TypeTag, + bridge_object_arg: ObjectArg, ) -> EmittedSuiToEthTokenBridgeV1 { let rgp = context.get_reference_gas_price().await.unwrap(); let sender = context.active_address().unwrap(); let gas_object = context.get_one_gas_object().await.unwrap().unwrap().1; let tx = TestTransactionBuilder::new(sender, gas_object, rgp) .move_call( - *get_bridge_package_id(), + BRIDGE_PACKAGE_ID, "bridge", "send_token", vec![ - CallArg::Object(*get_root_bridge_object_arg()), - CallArg::Pure(bcs::to_bytes(&(BridgeChainId::EthLocalTest as u8)).unwrap()), + CallArg::Object(bridge_object_arg), + CallArg::Pure(bcs::to_bytes(&(BridgeChainId::EthCustom as u8)).unwrap()), CallArg::Pure(bcs::to_bytes(&recv_address.as_bytes()).unwrap()), CallArg::Object(ObjectArg::ImmOrOwnedObject(token_ref)), ], ) - .with_type_args(vec![get_sui_token_type_tag(token_id)]) + .with_type_args(vec![token_type]) .build(); let signed_tn = context.sign_transaction(&tx); let resp = context.execute_transaction_must_succeed(signed_tn).await; let events = resp.events.unwrap(); - let mut bridge_events = events + let bridge_events = events .data .iter() .filter_map(|event| SuiBridgeEvent::try_from_sui_event(event).unwrap()) .collect::>(); - assert_eq!(bridge_events.len(), 1); - match bridge_events.remove(0) { - SuiBridgeEvent::SuiToEthTokenBridgeV1(event) => event, + bridge_events + .iter() + .find_map(|e| match e { + SuiBridgeEvent::SuiToEthTokenBridgeV1(event) => Some(event.clone()), + _ => None, + }) + .unwrap() +} + +/// Returns a VerifiedCertifiedBridgeAction with signatures from the given +/// BridgeAction and BridgeAuthorityKeyPair +pub fn get_certified_action_with_validator_secrets( + action: BridgeAction, + secrets: &Vec, +) -> VerifiedCertifiedBridgeAction { + let mut sigs = BTreeMap::new(); + for secret in secrets { + let signed_action = sign_action_with_key(&action, secret); + sigs.insert(secret.public().into(), signed_action.into_sig().signature); + } + let certified_action = CertifiedBridgeAction::new_from_data_and_sig( + action, + BridgeCommitteeValiditySignInfo { signatures: sigs }, + ); + VerifiedCertifiedBridgeAction::new_from_verified(certified_action) +} + +/// Approve a bridge action with the given validator secrets. Return the +/// newly created token object reference if `expected_token_receiver` is Some +/// (only relevant when the action is eth -> Sui transfer), +/// Otherwise return None. +/// Note: for sui -> eth transfers, the actual deposit needs to be recorded. +/// Use `bridge_token` to do it. +// TODO(bridge): It appears this function is very slow (particularly, `execute_transaction_must_succeed`). +// Investigate why. +pub async fn approve_action_with_validator_secrets( + wallet_context: &mut WalletContext, + bridge_obj_org: ObjectArg, + // TODO: add `token_recipient()` for `BridgeAction` so we don't need `expected_token_receiver` + action: BridgeAction, + validator_secrets: &Vec, + // Only relevant for eth -> sui transfers when token will be dropped to the recipient + expected_token_receiver: Option, + id_token_map: &HashMap, +) -> Option { + let action_certificate = get_certified_action_with_validator_secrets(action, validator_secrets); + let sui_address = wallet_context.active_address().unwrap(); + let gas_obj_ref = wallet_context + .get_one_gas_object() + .await + .unwrap() + .unwrap() + .1; + let tx_data = build_sui_transaction( + sui_address, + &gas_obj_ref, + action_certificate, + bridge_obj_org, + id_token_map, + ) + .unwrap(); + let signed_tx = wallet_context.sign_transaction(&tx_data); + let resp = wallet_context + .execute_transaction_must_succeed(signed_tx) + .await; + + // If `expected_token_receiver` is None, return + expected_token_receiver?; + + let expected_token_receiver = expected_token_receiver.unwrap(); + for created in resp.effects.unwrap().created() { + if created.owner == Owner::AddressOwner(expected_token_receiver) { + return Some(created.reference.to_object_ref()); + } } + panic!( + "Didn't find the creted object owned by {}", + expected_token_receiver + ); } diff --git a/crates/sui-bridge/src/tools/cli.rs b/crates/sui-bridge/src/tools/cli.rs index 1fcc3c4cd9bc1..2421d90b49f61 100644 --- a/crates/sui-bridge/src/tools/cli.rs +++ b/crates/sui-bridge/src/tools/cli.rs @@ -1,50 +1,35 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -use anyhow::anyhow; use clap::*; -use fastcrypto::ed25519::Ed25519KeyPair; -use fastcrypto::secp256k1::Secp256k1KeyPair; -use fastcrypto::traits::EncodeDecodeBase64; -use std::path::PathBuf; -use sui_bridge::config::BridgeNodeConfig; -use sui_bridge::crypto::BridgeAuthorityKeyPair; -use sui_bridge::crypto::BridgeAuthorityPublicKeyBytes; +use shared_crypto::intent::Intent; +use shared_crypto::intent::IntentMessage; +use std::sync::Arc; +use sui_bridge::client::bridge_authority_aggregator::BridgeAuthorityAggregator; +use sui_bridge::eth_transaction_builder::build_eth_transaction; +use sui_bridge::sui_client::SuiClient; +use sui_bridge::sui_transaction_builder::build_sui_transaction; +use sui_bridge::tools::{ + make_action, select_contract_address, Args, BridgeCliConfig, BridgeValidatorCommand, +}; +use sui_bridge::utils::{ + generate_bridge_authority_key_and_write_to_file, generate_bridge_client_key_and_write_to_file, + generate_bridge_node_config_and_write_to_file, +}; use sui_config::Config; -use sui_types::base_types::ObjectID; -use sui_types::base_types::SuiAddress; -use sui_types::crypto::get_key_pair; -use sui_types::crypto::SuiKeyPair; - -#[derive(Parser)] -#[clap(rename_all = "kebab-case")] -struct Args { - #[clap(subcommand)] - command: BridgeValidatorCommand, -} - -#[derive(Parser)] -#[clap(rename_all = "kebab-case")] -pub enum BridgeValidatorCommand { - #[clap(name = "create-bridge-validator-key")] - CreateBridgeValidatorKey { path: PathBuf }, - #[clap(name = "create-bridge-client-key")] - CreateBridgeClientKey { - path: PathBuf, - #[clap(name = "use-ecdsa", long)] - use_ecdsa: bool, - }, - #[clap(name = "create-bridge-node-config-template")] - CreateBridgeNodeConfigTemplate { - path: PathBuf, - #[clap(name = "run-client", long)] - run_client: bool, - }, -} +use sui_sdk::SuiClient as SuiSdkClient; +use sui_types::bridge::BridgeChainId; +use sui_types::crypto::Signature; +use sui_types::transaction::Transaction; #[tokio::main] async fn main() -> anyhow::Result<()> { + // Init logging + let (_guard, _filter_handle) = telemetry_subscribers::TelemetryConfig::new() + .with_env() + .init(); let args = Args::parse(); + match args.command { BridgeValidatorCommand::CreateBridgeValidatorKey { path } => { generate_bridge_authority_key_and_write_to_file(&path)?; @@ -61,80 +46,115 @@ async fn main() -> anyhow::Result<()> { path.display() ); } - } - Ok(()) -} + BridgeValidatorCommand::GovernanceClient { + config_path, + chain_id, + cmd, + } => { + let chain_id = BridgeChainId::try_from(chain_id).expect("Invalid chain id"); + println!("Chain ID: {:?}", chain_id); + let config = BridgeCliConfig::load(config_path).expect("Couldn't load BridgeCliConfig"); + let sui_client = SuiClient::::new(&config.sui_rpc_url).await?; -/// Generate Bridge Authority key (Secp256k1KeyPair) and write to a file as base64 encoded `privkey`. -fn generate_bridge_authority_key_and_write_to_file(path: &PathBuf) -> Result<(), anyhow::Error> { - let (_, kp): (_, BridgeAuthorityKeyPair) = get_key_pair(); - let eth_address = BridgeAuthorityPublicKeyBytes::from(&kp.public).to_eth_address(); - println!( - "Corresponding Ethereum address by this ecdsa key: {:?}", - eth_address - ); - let sui_address = SuiAddress::from(&kp.public); - println!( - "Corresponding Sui address by this ecdsa key: {:?}", - sui_address - ); - let base64_encoded = kp.encode_base64(); - std::fs::write(path, base64_encoded) - .map_err(|err| anyhow!("Failed to write encoded key to path: {:?}", err)) -} + let (sui_key, sui_address, gas_object_ref) = config + .get_sui_account_info() + .await + .expect("Failed to get sui account info"); + let bridge_summary = sui_client + .get_bridge_summary() + .await + .expect("Failed to get bridge summary"); + let bridge_committee = Arc::new( + sui_client + .get_bridge_committee() + .await + .expect("Failed to get bridge committee"), + ); + let agg = BridgeAuthorityAggregator::new(bridge_committee); -/// Generate Bridge Client key (Secp256k1KeyPair or Ed25519KeyPair) and write to a file as base64 encoded `flag || privkey`. -fn generate_bridge_client_key_and_write_to_file( - path: &PathBuf, - use_ecdsa: bool, -) -> Result<(), anyhow::Error> { - let kp = if use_ecdsa { - let (_, kp): (_, Secp256k1KeyPair) = get_key_pair(); - let eth_address = BridgeAuthorityPublicKeyBytes::from(&kp.public).to_eth_address(); - println!( - "Corresponding Ethereum address by this ecdsa key: {:?}", - eth_address - ); - SuiKeyPair::from(kp) - } else { - let (_, kp): (_, Ed25519KeyPair) = get_key_pair(); - SuiKeyPair::from(kp) - }; - let sui_address = SuiAddress::from(&kp.public()); - println!("Corresponding Sui address by this key: {:?}", sui_address); + // Handle Sui Side + if chain_id.is_sui_chain() { + let sui_chain_id = BridgeChainId::try_from(bridge_summary.chain_id).unwrap(); + assert_eq!( + sui_chain_id, chain_id, + "Chain ID mismatch, expected: {:?}, got from url: {:?}", + chain_id, sui_chain_id + ); + // Create BridgeAction + let sui_action = make_action(sui_chain_id, &cmd); + println!("Action to execute on Sui: {:?}", sui_action); + let threshold = sui_action.approval_threshold(); + let certified_action = agg + .request_committee_signatures(sui_action, threshold) + .await + .expect("Failed to request committee signatures"); + let bridge_arg = sui_client + .get_mutable_bridge_object_arg_must_succeed() + .await; + let id_token_map = sui_client.get_token_id_map().await.unwrap(); + let tx = build_sui_transaction( + sui_address, + &gas_object_ref, + certified_action, + bridge_arg, + &id_token_map, + ) + .expect("Failed to build sui transaction"); + let sui_sig = Signature::new_secure( + &IntentMessage::new(Intent::sui_transaction(), tx.clone()), + &sui_key, + ); + let tx = Transaction::from_data(tx, vec![sui_sig]); + let resp = sui_client + .execute_transaction_block_with_effects(tx) + .await + .expect("Failed to execute transaction block with effects"); + if resp.status_ok().unwrap() { + println!("Sui Transaction succeeded: {:?}", resp.digest); + } else { + println!( + "Sui Transaction failed: {:?}. Effects: {:?}", + resp.digest, resp.effects + ); + } + return Ok(()); + } - let contents = kp.encode_base64(); - std::fs::write(path, contents) - .map_err(|err| anyhow!("Failed to write encoded key to path: {:?}", err)) -} + // Handle eth side + // TODO assert chain id returned from rpc matches chain_id + let eth_signer_client = config + .get_eth_signer_client() + .await + .expect("Failed to get eth signer client"); + println!("Using Eth address: {:?}", eth_signer_client.address()); + // Create BridgeAction + let eth_action = make_action(chain_id, &cmd); + println!("Action to execute on Eth: {:?}", eth_action); + // Create Eth Signer Client + let threshold = eth_action.approval_threshold(); + let certified_action = agg + .request_committee_signatures(eth_action, threshold) + .await + .expect("Failed to request committee signatures"); + let contract_address = select_contract_address(&config, &cmd); + let tx = build_eth_transaction(contract_address, eth_signer_client, certified_action) + .await + .expect("Failed to build eth transaction"); + println!("sending Eth tx: {:?}", tx); + match tx.send().await { + Ok(tx_hash) => { + println!("Transaction sent with hash: {:?}", tx_hash); + } + Err(err) => { + let revert = err.as_revert(); + println!("Transaction reverted: {:?}", revert); + } + }; -/// Generate Bridge Node Config template and write to a file. -fn generate_bridge_node_config_and_write_to_file( - path: &PathBuf, - run_client: bool, -) -> Result<(), anyhow::Error> { - let mut config = BridgeNodeConfig { - server_listen_port: 9191, - metrics_port: 9184, - bridge_authority_key_path_base64_raw: PathBuf::from("/path/to/your/bridge_authority_key"), - sui_rpc_url: "your_sui_rpc_url".to_string(), - eth_rpc_url: "your_eth_rpc_url".to_string(), - eth_addresses: vec!["bridge_eth_proxy_address".into()], - approved_governance_actions: vec![], - run_client, - bridge_client_key_path_base64_sui_key: None, - bridge_client_gas_object: None, - sui_bridge_modules: Some(vec!["modules_to_watch".into()]), - db_path: None, - eth_bridge_contracts_start_block_override: None, - sui_bridge_modules_last_processed_event_id_override: None, - }; - if run_client { - config.bridge_client_key_path_base64_sui_key = - Some(PathBuf::from("/path/to/your/bridge_client_key")); - config.bridge_client_gas_object = Some(ObjectID::ZERO); - config.db_path = Some(PathBuf::from("/path/to/your/client_db")); + return Ok(()); + } } - config.save(path) + + Ok(()) } diff --git a/crates/sui-bridge/src/tools/mod.rs b/crates/sui-bridge/src/tools/mod.rs new file mode 100644 index 0000000000000..82b1144845769 --- /dev/null +++ b/crates/sui-bridge/src/tools/mod.rs @@ -0,0 +1,327 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 +use crate::config::read_bridge_client_key; +use crate::crypto::BridgeAuthorityPublicKeyBytes; +use crate::types::{ + AssetPriceUpdateAction, BlocklistCommitteeAction, BlocklistType, EmergencyAction, + EmergencyActionType, EvmContractUpgradeAction, LimitUpdateAction, +}; +use crate::utils::{get_eth_signer_client, EthSigner}; +use anyhow::anyhow; +use clap::*; +use ethers::types::Address as EthAddress; +use fastcrypto::encoding::Encoding; +use fastcrypto::encoding::Hex; +use fastcrypto::hash::{HashFunction, Keccak256}; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; +use std::path::PathBuf; +use sui_config::Config; +use sui_sdk::SuiClientBuilder; +use sui_types::base_types::ObjectRef; +use sui_types::base_types::SuiAddress; +use sui_types::bridge::BridgeChainId; +use sui_types::crypto::SuiKeyPair; + +use crate::types::BridgeAction; + +#[derive(Parser)] +#[clap(rename_all = "kebab-case")] +pub struct Args { + #[clap(subcommand)] + pub command: BridgeValidatorCommand, +} + +#[derive(Parser)] +#[clap(rename_all = "kebab-case")] +pub enum BridgeValidatorCommand { + #[clap(name = "create-bridge-validator-key")] + CreateBridgeValidatorKey { path: PathBuf }, + #[clap(name = "create-bridge-client-key")] + CreateBridgeClientKey { + path: PathBuf, + #[clap(name = "use-ecdsa", long)] + use_ecdsa: bool, + }, + #[clap(name = "create-bridge-node-config-template")] + CreateBridgeNodeConfigTemplate { + path: PathBuf, + #[clap(name = "run-client", long)] + run_client: bool, + }, + /// Client to facilitate and execute Bridge governance actions + #[clap(name = "client")] + GovernanceClient { + /// Path of BridgeCliConfig + #[clap(long = "config-path")] + config_path: PathBuf, + #[clap(long = "chain-id")] + chain_id: u8, + #[clap(subcommand)] + cmd: GovernanceClientCommands, + }, +} + +#[derive(Parser)] +#[clap(rename_all = "kebab-case")] +pub enum GovernanceClientCommands { + #[clap(name = "emergency-button")] + EmergencyButton { + #[clap(name = "nonce", long)] + nonce: u64, + #[clap(name = "action-type", long)] + action_type: EmergencyActionType, + }, + #[clap(name = "update-committee-blocklist")] + UpdateCommitteeBlocklist { + #[clap(name = "nonce", long)] + nonce: u64, + #[clap(name = "blocklist-type", long)] + blocklist_type: BlocklistType, + #[clap(name = "pubkey-hex", long)] + pubkeys_hex: Vec, + }, + #[clap(name = "update-limit")] + UpdateLimit { + #[clap(name = "nonce", long)] + nonce: u64, + #[clap(name = "sending-chain", long)] + sending_chain: u8, + #[clap(name = "new-usd-limit", long)] + new_usd_limit: u64, + }, + #[clap(name = "update-asset-price")] + UpdateAssetPrice { + #[clap(name = "nonce", long)] + nonce: u64, + #[clap(name = "token-id", long)] + token_id: u8, + #[clap(name = "new-usd-price", long)] + new_usd_price: u64, + }, + #[clap(name = "upgrade-evm-contract")] + UpgradeEVMContract { + #[clap(name = "nonce", long)] + nonce: u64, + #[clap(name = "proxy-address", long)] + proxy_address: EthAddress, + /// The address of the new implementation contract + #[clap(name = "implementation-address", long)] + implementation_address: EthAddress, + /// Function selector with params types, e.g. `foo(uint256,bool,string)` + #[clap(name = "function-selector", long)] + function_selector: String, + /// Params to be passed to the function, e.g. `420,false,hello` + #[clap(name = "params", long)] + params: Vec, + }, +} + +pub fn make_action(chain_id: BridgeChainId, cmd: &GovernanceClientCommands) -> BridgeAction { + match cmd { + GovernanceClientCommands::EmergencyButton { nonce, action_type } => { + BridgeAction::EmergencyAction(EmergencyAction { + nonce: *nonce, + chain_id, + action_type: *action_type, + }) + } + GovernanceClientCommands::UpdateCommitteeBlocklist { + nonce, + blocklist_type, + pubkeys_hex, + } => BridgeAction::BlocklistCommitteeAction(BlocklistCommitteeAction { + nonce: *nonce, + chain_id, + blocklist_type: *blocklist_type, + blocklisted_members: pubkeys_hex.clone(), + }), + GovernanceClientCommands::UpdateLimit { + nonce, + sending_chain, + new_usd_limit, + } => { + let sending_chain_id = + BridgeChainId::try_from(*sending_chain).expect("Invalid sending chain id"); + BridgeAction::LimitUpdateAction(LimitUpdateAction { + nonce: *nonce, + chain_id, + sending_chain_id, + new_usd_limit: *new_usd_limit, + }) + } + GovernanceClientCommands::UpdateAssetPrice { + nonce, + token_id, + new_usd_price, + } => BridgeAction::AssetPriceUpdateAction(AssetPriceUpdateAction { + nonce: *nonce, + chain_id, + token_id: *token_id, + new_usd_price: *new_usd_price, + }), + GovernanceClientCommands::UpgradeEVMContract { + nonce, + proxy_address, + implementation_address, + function_selector, + params, + } => BridgeAction::EvmContractUpgradeAction(EvmContractUpgradeAction { + nonce: *nonce, + chain_id, + proxy_address: *proxy_address, + new_impl_address: *implementation_address, + call_data: encode_call_data(function_selector, params), + }), + } +} + +fn encode_call_data(function_selector: &str, params: &Vec) -> Vec { + let left = function_selector + .find('(') + .expect("Invalid function selector, no left parentheses"); + let right = function_selector + .find(')') + .expect("Invalid function selector, no right parentheses"); + let param_types = function_selector[left + 1..right] + .split(',') + .map(|x| x.trim()) + .collect::>(); + + assert_eq!(param_types.len(), params.len(), "Invalid number of params"); + + let mut call_data = Keccak256::digest(function_selector).digest[0..4].to_vec(); + let mut tokens = vec![]; + for (param, param_type) in params.iter().zip(param_types.iter()) { + match param_type.to_lowercase().as_str() { + "uint256" => { + tokens.push(ethers::abi::Token::Uint( + ethers::types::U256::from_dec_str(param).expect("Invalid U256"), + )); + } + "bool" => { + tokens.push(ethers::abi::Token::Bool(match param.as_str() { + "true" => true, + "false" => false, + _ => panic!("Invalid bool in params"), + })); + } + "string" => { + tokens.push(ethers::abi::Token::String(param.clone())); + } + // TODO: need to support more types if needed + _ => panic!("Invalid param type"), + } + } + if !tokens.is_empty() { + call_data.extend(ethers::abi::encode(&tokens)); + } + call_data +} + +pub fn select_contract_address( + config: &BridgeCliConfig, + cmd: &GovernanceClientCommands, +) -> EthAddress { + match cmd { + GovernanceClientCommands::EmergencyButton { .. } => config.eth_sui_bridge_proxy_address, + GovernanceClientCommands::UpdateCommitteeBlocklist { .. } => { + config.eth_bridge_committee_proxy_address + } + GovernanceClientCommands::UpdateLimit { .. } => config.eth_bridge_limiter_proxy_address, + GovernanceClientCommands::UpdateAssetPrice { .. } => { + config.eth_bridge_limiter_proxy_address + } + GovernanceClientCommands::UpgradeEVMContract { proxy_address, .. } => *proxy_address, + } +} + +#[serde_as] +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct BridgeCliConfig { + /// Rpc url for Sui fullnode, used for query stuff and submit transactions. + pub sui_rpc_url: String, + /// Rpc url for Eth fullnode, used for query stuff. + pub eth_rpc_url: String, + /// Proxy address for SuiBridge deployed on Eth + pub eth_sui_bridge_proxy_address: EthAddress, + /// Proxy address for BridgeCommittee deployed on Eth + pub eth_bridge_committee_proxy_address: EthAddress, + /// Proxy address for BridgeLimiter deployed on Eth + pub eth_bridge_limiter_proxy_address: EthAddress, + /// Path of the file where bridge client key (any SuiKeyPair) is stored as Base64 encoded `flag || privkey`. + /// The derived accounts + pub bridge_client_key_path_base64_sui_key: PathBuf, +} + +impl Config for BridgeCliConfig {} + +impl BridgeCliConfig { + pub async fn get_eth_signer_client(self: &BridgeCliConfig) -> anyhow::Result { + let client_key = read_bridge_client_key(&self.bridge_client_key_path_base64_sui_key)?; + let private_key = Hex::encode(client_key.to_bytes_no_flag()); + let url = self.eth_rpc_url.clone(); + get_eth_signer_client(&url, &private_key).await + } + + pub async fn get_sui_account_info( + self: &BridgeCliConfig, + ) -> anyhow::Result<(SuiKeyPair, SuiAddress, ObjectRef)> { + let client_key = read_bridge_client_key(&self.bridge_client_key_path_base64_sui_key)?; + let pubkey = client_key.public(); + let sui_client_address = SuiAddress::from(&pubkey); + println!("Using Sui address: {:?}", sui_client_address); + let sui_sdk_client = SuiClientBuilder::default() + .build(self.sui_rpc_url.clone()) + .await?; + let gases = sui_sdk_client + .coin_read_api() + .get_coins(sui_client_address, None, None, None) + .await? + .data; + // TODO: is 5 Sui a good number? + let gas = gases + .into_iter() + .find(|coin| coin.balance >= 5_000_000_000) + .ok_or(anyhow!("Did not find gas object with enough balance"))?; + println!("Using Gas object: {}", gas.coin_object_id); + Ok((client_key, sui_client_address, gas.object_ref())) + } +} + +#[cfg(test)] +mod tests { + use ethers::abi::FunctionExt; + + use super::*; + + #[tokio::test] + async fn test_encode_call_data() { + let abi_json = std::fs::read_to_string("abi/tests/mock_sui_bridge_v2.json").unwrap(); + let abi: ethers::abi::Abi = serde_json::from_str(&abi_json).unwrap(); + + let function_selector = "initializeV2Params(uint256,bool,string)"; + let params = vec!["420".to_string(), "false".to_string(), "hello".to_string()]; + let call_data = encode_call_data(function_selector, ¶ms); + + let function = abi + .functions() + .find(|f| { + let selector = f.selector(); + call_data.starts_with(selector.as_ref()) + }) + .expect("Function not found"); + + // Decode the data excluding the selector + let tokens = function.decode_input(&call_data[4..]).unwrap(); + assert_eq!( + tokens, + vec![ + ethers::abi::Token::Uint(ethers::types::U256::from_dec_str("420").unwrap()), + ethers::abi::Token::Bool(false), + ethers::abi::Token::String("hello".to_string()) + ] + ) + } +} diff --git a/crates/sui-bridge/src/types.rs b/crates/sui-bridge/src/types.rs index fcb37e3f7c3a0..485383f44d9a6 100644 --- a/crates/sui-bridge/src/types.rs +++ b/crates/sui-bridge/src/types.rs @@ -6,8 +6,10 @@ use crate::crypto::BridgeAuthorityPublicKeyBytes; use crate::crypto::{ BridgeAuthorityPublicKey, BridgeAuthorityRecoverableSignature, BridgeAuthoritySignInfo, }; +use crate::encoding::BridgeMessageEncoding; use crate::error::{BridgeError, BridgeResult}; use crate::events::EmittedSuiToEthTokenBridgeV1; +use enum_dispatch::enum_dispatch; use ethers::types::Address as EthAddress; use ethers::types::Log; use ethers::types::H256; @@ -19,25 +21,26 @@ use rand::Rng; use serde::{Deserialize, Serialize}; use shared_crypto::intent::IntentScope; use std::collections::{BTreeMap, BTreeSet}; -use sui_types::base_types::SuiAddress; -use sui_types::base_types::SUI_ADDRESS_LENGTH; -use sui_types::collection_types::{Bag, LinkedTable, LinkedTableNode, VecMap}; +use sui_types::bridge::{ + BridgeChainId, APPROVAL_THRESHOLD_ADD_TOKENS_ON_EVM, APPROVAL_THRESHOLD_ADD_TOKENS_ON_SUI, + BRIDGE_COMMITTEE_MAXIMAL_VOTING_POWER, BRIDGE_COMMITTEE_MINIMAL_VOTING_POWER, +}; +use sui_types::bridge::{ + APPROVAL_THRESHOLD_ASSET_PRICE_UPDATE, APPROVAL_THRESHOLD_COMMITTEE_BLOCKLIST, + APPROVAL_THRESHOLD_EMERGENCY_PAUSE, APPROVAL_THRESHOLD_EMERGENCY_UNPAUSE, + APPROVAL_THRESHOLD_EVM_CONTRACT_UPGRADE, APPROVAL_THRESHOLD_LIMIT_UPDATE, + APPROVAL_THRESHOLD_TOKEN_TRANSFER, +}; use sui_types::committee::CommitteeTrait; use sui_types::committee::StakeUnit; use sui_types::digests::{Digest, TransactionDigest}; -use sui_types::dynamic_field::Field; use sui_types::message_envelope::{Envelope, Message, VerifiedEnvelope}; +use sui_types::TypeTag; pub const BRIDGE_AUTHORITY_TOTAL_VOTING_POWER: u64 = 10000; pub const USD_MULTIPLIER: u64 = 10000; // decimal places = 4 -pub type BridgeInnerDynamicField = Field; -pub type BridgeRecordDynamicField = Field< - MoveTypeBridgeMessageKey, - LinkedTableNode, ->; - #[derive(Debug, Eq, PartialEq, Clone)] pub struct BridgeAuthority { pub pubkey: BridgeAuthorityPublicKey, @@ -52,7 +55,6 @@ impl BridgeAuthority { } } -// A static Bridge committee implementation #[derive(Debug, Clone)] pub struct BridgeCommittee { members: BTreeMap, @@ -62,8 +64,8 @@ pub struct BridgeCommittee { impl BridgeCommittee { pub fn new(members: Vec) -> BridgeResult { let mut members_map = BTreeMap::new(); - let mut total_stake = 0; let mut total_blocklisted_stake = 0; + let mut total_stake = 0; for member in members { let public_key = BridgeAuthorityPublicKeyBytes::from(&member.pubkey); if members_map.contains_key(&public_key) { @@ -72,16 +74,21 @@ impl BridgeCommittee { )); } // TODO: should we disallow identical network addresses? - total_stake += member.voting_power; if member.is_blocklisted { total_blocklisted_stake += member.voting_power; } + total_stake += member.voting_power; members_map.insert(public_key, member); } - if total_stake != BRIDGE_AUTHORITY_TOTAL_VOTING_POWER { - return Err(BridgeError::InvalidBridgeCommittee( - "Total voting power does not equal to 10000".into(), - )); + if total_stake < BRIDGE_COMMITTEE_MINIMAL_VOTING_POWER { + return Err(BridgeError::InvalidBridgeCommittee(format!( + "Total voting power is below minimal {BRIDGE_COMMITTEE_MINIMAL_VOTING_POWER}" + ))); + } + if total_stake > BRIDGE_COMMITTEE_MAXIMAL_VOTING_POWER { + return Err(BridgeError::InvalidBridgeCommittee(format!( + "Total voting power is above maximal {BRIDGE_COMMITTEE_MAXIMAL_VOTING_POWER}" + ))); } Ok(Self { members: members_map, @@ -164,54 +171,17 @@ pub enum BridgeActionType { LimitUpdate = 3, AssetPriceUpdate = 4, EvmContractUpgrade = 5, + AddTokensOnSui = 6, + AddTokensOnEvm = 7, } -pub const SUI_TX_DIGEST_LENGTH: usize = 32; -pub const ETH_TX_HASH_LENGTH: usize = 32; - -pub const BRIDGE_MESSAGE_PREFIX: &[u8] = b"SUI_BRIDGE_MESSAGE"; - -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, TryFromPrimitive, Hash)] -#[repr(u8)] -pub enum BridgeChainId { - SuiMainnet = 0, - SuiTestnet = 1, - SuiDevnet = 2, - SuiLocalTest = 3, - - EthMainnet = 10, - EthSepolia = 11, - EthLocalTest = 12, -} - -#[derive( - Copy, - Clone, - Debug, - PartialEq, - Eq, - Serialize, - Deserialize, - TryFromPrimitive, - Hash, - Ord, - PartialOrd, -)] +#[derive(Debug, PartialEq, Eq, Clone, TryFromPrimitive)] #[repr(u8)] -pub enum TokenId { - Sui = 0, - BTC = 1, - ETH = 2, - USDC = 3, - USDT = 4, -} - -#[derive(Debug, PartialEq, Eq, Clone)] pub enum BridgeActionStatus { - RecordNotFound, - Pending, - Approved, - Claimed, + Pending = 0, + Approved = 1, + Claimed = 2, + NotFound = 3, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] @@ -223,40 +193,6 @@ pub struct SuiToEthBridgeAction { pub sui_bridge_event: EmittedSuiToEthTokenBridgeV1, } -impl SuiToEthBridgeAction { - pub fn to_bytes(&self) -> Vec { - let mut bytes = Vec::new(); - let e = &self.sui_bridge_event; - // Add message type - bytes.push(BridgeActionType::TokenTransfer as u8); - // Add message version - bytes.push(TOKEN_TRANSFER_MESSAGE_VERSION); - // Add nonce - bytes.extend_from_slice(&e.nonce.to_be_bytes()); - // Add source chain id - bytes.push(e.sui_chain_id as u8); - - // Add source address length - bytes.push(SUI_ADDRESS_LENGTH as u8); - // Add source address - bytes.extend_from_slice(&e.sui_address.to_vec()); - // Add dest chain id - bytes.push(e.eth_chain_id as u8); - // Add dest address length - bytes.push(EthAddress::len_bytes() as u8); - // Add dest address - bytes.extend_from_slice(e.eth_address.as_bytes()); - - // Add token id - bytes.push(e.token_id as u8); - - // Add token amount - bytes.extend_from_slice(&e.amount.to_be_bytes()); - - bytes - } -} - #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] pub struct EthToSuiBridgeAction { // Digest of the transaction where the event was emitted @@ -266,41 +202,18 @@ pub struct EthToSuiBridgeAction { pub eth_bridge_event: EthToSuiTokenBridgeV1, } -impl EthToSuiBridgeAction { - pub fn to_bytes(&self) -> Vec { - let mut bytes = Vec::new(); - let e = &self.eth_bridge_event; - // Add message type - bytes.push(BridgeActionType::TokenTransfer as u8); - // Add message version - bytes.push(TOKEN_TRANSFER_MESSAGE_VERSION); - // Add nonce - bytes.extend_from_slice(&e.nonce.to_be_bytes()); - // Add source chain id - bytes.push(e.eth_chain_id as u8); - - // Add source address length - bytes.push(EthAddress::len_bytes() as u8); - // Add source address - bytes.extend_from_slice(e.eth_address.as_bytes()); - // Add dest chain id - bytes.push(e.sui_chain_id as u8); - // Add dest address length - bytes.push(SUI_ADDRESS_LENGTH as u8); - // Add dest address - bytes.extend_from_slice(&e.sui_address.to_vec()); - - // Add token id - bytes.push(e.token_id as u8); - - // Add token amount - bytes.extend_from_slice(&e.amount.to_be_bytes()); - - bytes - } -} - -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, TryFromPrimitive, Hash)] +#[derive( + Debug, + Serialize, + Deserialize, + PartialEq, + Eq, + Clone, + Copy, + TryFromPrimitive, + Hash, + clap::ValueEnum, +)] #[repr(u8)] pub enum BlocklistType { Blocklist = 0, @@ -312,41 +225,22 @@ pub struct BlocklistCommitteeAction { pub nonce: u64, pub chain_id: BridgeChainId, pub blocklist_type: BlocklistType, + // TODO: rename this to `members_to_update` pub blocklisted_members: Vec, } -impl BlocklistCommitteeAction { - pub fn to_bytes(&self) -> Vec { - let mut bytes = Vec::new(); - // Add message type - bytes.push(BridgeActionType::UpdateCommitteeBlocklist as u8); - // Add message version - bytes.push(COMMITTEE_BLOCKLIST_MESSAGE_VERSION); - // Add nonce - bytes.extend_from_slice(&self.nonce.to_be_bytes()); - // Add chain id - bytes.push(self.chain_id as u8); - // Add blocklist type - bytes.push(self.blocklist_type as u8); - // Add length of updated members. - // Unwrap: It should not overflow given what we have today. - bytes.push(u8::try_from(self.blocklisted_members.len()).unwrap()); - - // Add list of updated members - // Members are represented as pubkey derived evm addresses (20 bytes) - let members_bytes = self - .blocklisted_members - .iter() - .map(|m| m.to_eth_address().to_fixed_bytes().to_vec()) - .collect::>(); - for members_bytes in members_bytes { - bytes.extend_from_slice(&members_bytes); - } - bytes - } -} - -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, TryFromPrimitive, Hash)] +#[derive( + Debug, + Serialize, + Deserialize, + PartialEq, + Eq, + Clone, + Copy, + TryFromPrimitive, + Hash, + clap::ValueEnum, +)] #[repr(u8)] pub enum EmergencyActionType { Pause = 0, @@ -360,23 +254,6 @@ pub struct EmergencyAction { pub action_type: EmergencyActionType, } -impl EmergencyAction { - pub fn to_bytes(&self) -> Vec { - let mut bytes = Vec::new(); - // Add message type - bytes.push(BridgeActionType::EmergencyButton as u8); - // Add message version - bytes.push(EMERGENCY_BUTTON_MESSAGE_VERSION); - // Add nonce - bytes.extend_from_slice(&self.nonce.to_be_bytes()); - // Add chain id - bytes.push(self.chain_id as u8); - // Add action type - bytes.push(self.action_type as u8); - bytes - } -} - #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] pub struct LimitUpdateAction { pub nonce: u64, @@ -386,55 +263,18 @@ pub struct LimitUpdateAction { pub chain_id: BridgeChainId, // The sending chain id for the limit update. pub sending_chain_id: BridgeChainId, + // 4 decimal places, namely 1 USD = 10000 pub new_usd_limit: u64, } -impl LimitUpdateAction { - pub fn to_bytes(&self) -> Vec { - let mut bytes = Vec::new(); - // Add message type - bytes.push(BridgeActionType::LimitUpdate as u8); - // Add message version - bytes.push(LIMIT_UPDATE_MESSAGE_VERSION); - // Add nonce - bytes.extend_from_slice(&self.nonce.to_be_bytes()); - // Add chain id - bytes.push(self.chain_id as u8); - // Add sending chain id - bytes.push(self.sending_chain_id as u8); - // Add new usd limit - bytes.extend_from_slice(&self.new_usd_limit.to_be_bytes()); - bytes - } -} - #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] pub struct AssetPriceUpdateAction { pub nonce: u64, pub chain_id: BridgeChainId, - pub token_id: TokenId, + pub token_id: u8, pub new_usd_price: u64, } -impl AssetPriceUpdateAction { - pub fn to_bytes(&self) -> Vec { - let mut bytes = Vec::new(); - // Add message type - bytes.push(BridgeActionType::AssetPriceUpdate as u8); - // Add message version - bytes.push(EMERGENCY_BUTTON_MESSAGE_VERSION); - // Add nonce - bytes.extend_from_slice(&self.nonce.to_be_bytes()); - // Add chain id - bytes.push(self.chain_id as u8); - // Add token id - bytes.push(self.token_id as u8); - // Add new usd limit - bytes.extend_from_slice(&self.new_usd_price.to_be_bytes()); - bytes - } -} - #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] pub struct EvmContractUpgradeAction { pub nonce: u64, @@ -444,31 +284,31 @@ pub struct EvmContractUpgradeAction { pub call_data: Vec, } -impl EvmContractUpgradeAction { - pub fn to_bytes(&self) -> Vec { - let mut bytes = Vec::new(); - // Add message type - bytes.push(BridgeActionType::EvmContractUpgrade as u8); - // Add message version - bytes.push(EVM_CONTRACT_UPGRADE_MESSAGE_VERSION); - // Add nonce - bytes.extend_from_slice(&self.nonce.to_be_bytes()); - // Add chain id - bytes.push(self.chain_id as u8); - // Add payload - let encoded = ethers::abi::encode(&[ - ethers::abi::Token::Address(self.proxy_address), - ethers::abi::Token::Address(self.new_impl_address), - ethers::abi::Token::Bytes(self.call_data.clone()), - ]); - bytes.extend_from_slice(&encoded); - bytes - } +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub struct AddTokensOnSuiAction { + pub nonce: u64, + pub chain_id: BridgeChainId, + pub native: bool, + pub token_ids: Vec, + pub token_type_names: Vec, + pub token_prices: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub struct AddTokensOnEvmAction { + pub nonce: u64, + pub chain_id: BridgeChainId, + pub native: bool, + pub token_ids: Vec, + pub token_addresses: Vec, + pub token_sui_decimals: Vec, + pub token_prices: Vec, } /// The type of actions Bridge Committee verify and sign off to execution. /// Its relationship with BridgeEvent is similar to the relationship between #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] +#[enum_dispatch(BridgeMessageEncoding)] pub enum BridgeAction { /// Sui to Eth bridge action SuiToEthBridgeAction(SuiToEthBridgeAction), @@ -479,48 +319,11 @@ pub enum BridgeAction { LimitUpdateAction(LimitUpdateAction), AssetPriceUpdateAction(AssetPriceUpdateAction), EvmContractUpgradeAction(EvmContractUpgradeAction), - // TODO: add other bridge actions such as blocklist & emergency button + AddTokensOnSuiAction(AddTokensOnSuiAction), + AddTokensOnEvmAction(AddTokensOnEvmAction), } -pub const TOKEN_TRANSFER_MESSAGE_VERSION: u8 = 1; -pub const COMMITTEE_BLOCKLIST_MESSAGE_VERSION: u8 = 1; -pub const EMERGENCY_BUTTON_MESSAGE_VERSION: u8 = 1; -pub const LIMIT_UPDATE_MESSAGE_VERSION: u8 = 1; -pub const ASSET_PRICE_UPDATE_MESSAGE_VERSION: u8 = 1; -pub const EVM_CONTRACT_UPGRADE_MESSAGE_VERSION: u8 = 1; - impl BridgeAction { - /// Convert to message bytes that are verified in Move and Solidity - pub fn to_bytes(&self) -> Vec { - let mut bytes = Vec::new(); - // Add prefix - bytes.extend_from_slice(BRIDGE_MESSAGE_PREFIX); - match self { - BridgeAction::SuiToEthBridgeAction(a) => { - bytes.extend_from_slice(&a.to_bytes()); - } - BridgeAction::EthToSuiBridgeAction(a) => { - bytes.extend_from_slice(&a.to_bytes()); - } - BridgeAction::BlocklistCommitteeAction(a) => { - bytes.extend_from_slice(&a.to_bytes()); - } - BridgeAction::EmergencyAction(a) => { - bytes.extend_from_slice(&a.to_bytes()); - } - BridgeAction::LimitUpdateAction(a) => { - bytes.extend_from_slice(&a.to_bytes()); - } - BridgeAction::AssetPriceUpdateAction(a) => { - bytes.extend_from_slice(&a.to_bytes()); - } - BridgeAction::EvmContractUpgradeAction(a) => { - bytes.extend_from_slice(&a.to_bytes()); - } // TODO add formats for other events - } - bytes - } - // Digest of BridgeAction (with Keccak256 hasher) pub fn digest(&self) -> BridgeActionDigest { let mut hasher = Keccak256::default(); @@ -537,6 +340,8 @@ impl BridgeAction { BridgeAction::LimitUpdateAction(a) => a.chain_id, BridgeAction::AssetPriceUpdateAction(a) => a.chain_id, BridgeAction::EvmContractUpgradeAction(a) => a.chain_id, + BridgeAction::AddTokensOnSuiAction(a) => a.chain_id, + BridgeAction::AddTokensOnEvmAction(a) => a.chain_id, } } @@ -548,6 +353,8 @@ impl BridgeAction { BridgeActionType::LimitUpdate => true, BridgeActionType::AssetPriceUpdate => true, BridgeActionType::EvmContractUpgrade => true, + BridgeActionType::AddTokensOnSui => true, + BridgeActionType::AddTokensOnEvm => true, } } @@ -561,6 +368,8 @@ impl BridgeAction { BridgeAction::LimitUpdateAction(_) => BridgeActionType::LimitUpdate, BridgeAction::AssetPriceUpdateAction(_) => BridgeActionType::AssetPriceUpdate, BridgeAction::EvmContractUpgradeAction(_) => BridgeActionType::EvmContractUpgrade, + BridgeAction::AddTokensOnSuiAction(_) => BridgeActionType::AddTokensOnSui, + BridgeAction::AddTokensOnEvmAction(_) => BridgeActionType::AddTokensOnEvm, } } @@ -574,6 +383,25 @@ impl BridgeAction { BridgeAction::LimitUpdateAction(a) => a.nonce, BridgeAction::AssetPriceUpdateAction(a) => a.nonce, BridgeAction::EvmContractUpgradeAction(a) => a.nonce, + BridgeAction::AddTokensOnSuiAction(a) => a.nonce, + BridgeAction::AddTokensOnEvmAction(a) => a.nonce, + } + } + + pub fn approval_threshold(&self) -> u64 { + match self { + BridgeAction::SuiToEthBridgeAction(_) => APPROVAL_THRESHOLD_TOKEN_TRANSFER, + BridgeAction::EthToSuiBridgeAction(_) => APPROVAL_THRESHOLD_TOKEN_TRANSFER, + BridgeAction::BlocklistCommitteeAction(_) => APPROVAL_THRESHOLD_COMMITTEE_BLOCKLIST, + BridgeAction::EmergencyAction(a) => match a.action_type { + EmergencyActionType::Pause => APPROVAL_THRESHOLD_EMERGENCY_PAUSE, + EmergencyActionType::Unpause => APPROVAL_THRESHOLD_EMERGENCY_UNPAUSE, + }, + BridgeAction::LimitUpdateAction(_) => APPROVAL_THRESHOLD_LIMIT_UPDATE, + BridgeAction::AssetPriceUpdateAction(_) => APPROVAL_THRESHOLD_ASSET_PRICE_UPDATE, + BridgeAction::EvmContractUpgradeAction(_) => APPROVAL_THRESHOLD_EVM_CONTRACT_UPGRADE, + BridgeAction::AddTokensOnSuiAction(_) => APPROVAL_THRESHOLD_ADD_TOKENS_ON_SUI, + BridgeAction::AddTokensOnEvmAction(_) => APPROVAL_THRESHOLD_ADD_TOKENS_ON_EVM, } } } @@ -628,598 +456,55 @@ pub struct EthLog { pub log: Log, } -/////////////////////////// Move Types Start ////////////////////////// - -/// Rust version of the Move bridge::BridgeInner type. -#[derive(Debug, Serialize, Deserialize)] -pub struct MoveTypeBridgeInner { - pub bridge_version: u64, - pub chain_id: u8, - pub sequence_nums: VecMap, - pub committee: MoveTypeBridgeCommittee, - pub treasury: MoveTypeBridgeTreasury, - pub bridge_records: LinkedTable, - pub frozen: bool, -} - -/// Rust version of the Move treasury::BridgeTreasury type. -#[derive(Debug, Serialize, Deserialize)] -pub struct MoveTypeBridgeTreasury { - pub treasuries: Bag, -} - -/// Rust version of the Move committee::BridgeCommittee type. -#[derive(Debug, Serialize, Deserialize)] -pub struct MoveTypeBridgeCommittee { - pub members: VecMap, MoveTypeCommitteeMember>, - pub thresholds: VecMap, -} - -/// Rust version of the Move committee::CommitteeMember type. -#[derive(Debug, Serialize, Deserialize)] -pub struct MoveTypeCommitteeMember { - pub sui_address: SuiAddress, - pub bridge_pubkey_bytes: Vec, - pub voting_power: u64, - pub http_rest_url: Vec, - pub blocklisted: bool, -} - -/// Rust version of the Move message::BridgeMessageKey type. -#[derive(Debug, Serialize, Deserialize)] -pub struct MoveTypeBridgeMessageKey { - pub source_chain: u8, - pub message_type: u8, - pub bridge_seq_num: u64, -} - -/// Rust version of the Move message::BridgeMessage type. -#[derive(Debug, Serialize, Deserialize)] -pub struct MoveTypeBridgeMessage { - pub message_type: u8, - pub message_version: u8, - pub seq_num: u64, - pub source_chain: u8, - pub payload: Vec, -} - -/// Rust version of the Move message::BridgeMessage type. -#[derive(Debug, Serialize, Deserialize)] -pub struct MoveTypeBridgeRecord { - pub message: MoveTypeBridgeMessage, - pub verified_signatures: Option>>, - pub claimed: bool, -} - -/////////////////////////// Move Types End ////////////////////////// - -#[cfg(test)] -mod tests { - use crate::{test_utils::get_test_authority_and_key, types::TokenId}; - use ethers::abi::ParamType; - use ethers::types::{Address as EthAddress, TxHash}; - use fastcrypto::encoding::Hex; - use fastcrypto::hash::HashFunction; - use fastcrypto::traits::ToFromBytes; - use fastcrypto::{encoding::Encoding, traits::KeyPair}; - use prometheus::Registry; - use std::{collections::HashSet, str::FromStr}; - use sui_types::{ - base_types::{SuiAddress, TransactionDigest}, - crypto::get_key_pair, - }; - - use super::*; - - #[test] - fn test_bridge_message_encoding() -> anyhow::Result<()> { - telemetry_subscribers::init_for_testing(); - let registry = Registry::new(); - mysten_metrics::init_metrics(®istry); - let nonce = 54321u64; - let sui_tx_digest = TransactionDigest::random(); - let sui_chain_id = BridgeChainId::SuiTestnet; - let sui_tx_event_index = 1u16; - let eth_chain_id = BridgeChainId::EthSepolia; - let sui_address = SuiAddress::random_for_testing_only(); - let eth_address = EthAddress::random(); - let token_id = TokenId::USDC; - let amount = 1_000_000; - - let sui_bridge_event = EmittedSuiToEthTokenBridgeV1 { - nonce, - sui_chain_id, - eth_chain_id, - sui_address, - eth_address, - token_id, - amount, - }; - - let encoded_bytes = BridgeAction::SuiToEthBridgeAction(SuiToEthBridgeAction { - sui_tx_digest, - sui_tx_event_index, - sui_bridge_event, - }) - .to_bytes(); - - // Construct the expected bytes - let prefix_bytes = BRIDGE_MESSAGE_PREFIX.to_vec(); // len: 18 - let message_type = vec![BridgeActionType::TokenTransfer as u8]; // len: 1 - let message_version = vec![TOKEN_TRANSFER_MESSAGE_VERSION]; // len: 1 - let nonce_bytes = nonce.to_be_bytes().to_vec(); // len: 8 - let source_chain_id_bytes = vec![sui_chain_id as u8]; // len: 1 - - let sui_address_length_bytes = vec![SUI_ADDRESS_LENGTH as u8]; // len: 1 - let sui_address_bytes = sui_address.to_vec(); // len: 32 - let dest_chain_id_bytes = vec![eth_chain_id as u8]; // len: 1 - let eth_address_length_bytes = vec![EthAddress::len_bytes() as u8]; // len: 1 - let eth_address_bytes = eth_address.as_bytes().to_vec(); // len: 20 - - let token_id_bytes = vec![token_id as u8]; // len: 1 - let token_amount_bytes = amount.to_be_bytes().to_vec(); // len: 8 - - let mut combined_bytes = Vec::new(); - combined_bytes.extend_from_slice(&prefix_bytes); - combined_bytes.extend_from_slice(&message_type); - combined_bytes.extend_from_slice(&message_version); - combined_bytes.extend_from_slice(&nonce_bytes); - combined_bytes.extend_from_slice(&source_chain_id_bytes); - combined_bytes.extend_from_slice(&sui_address_length_bytes); - combined_bytes.extend_from_slice(&sui_address_bytes); - combined_bytes.extend_from_slice(&dest_chain_id_bytes); - combined_bytes.extend_from_slice(ð_address_length_bytes); - combined_bytes.extend_from_slice(ð_address_bytes); - combined_bytes.extend_from_slice(&token_id_bytes); - combined_bytes.extend_from_slice(&token_amount_bytes); - - assert_eq!(combined_bytes, encoded_bytes); - - // Assert fixed length - // TODO: for each action type add a test to assert the length - assert_eq!( - combined_bytes.len(), - 18 + 1 + 1 + 8 + 1 + 1 + 32 + 1 + 20 + 1 + 1 + 8 - ); - Ok(()) - } - - #[test] - fn test_bridge_message_encoding_regression_emitted_sui_to_eth_token_bridge_v1( - ) -> anyhow::Result<()> { - telemetry_subscribers::init_for_testing(); - let registry = Registry::new(); - mysten_metrics::init_metrics(®istry); - let sui_tx_digest = TransactionDigest::random(); - let sui_tx_event_index = 1u16; - - let nonce = 10u64; - let sui_chain_id = BridgeChainId::SuiTestnet; - let eth_chain_id = BridgeChainId::EthSepolia; - let sui_address = SuiAddress::from_str( - "0x0000000000000000000000000000000000000000000000000000000000000064", - ) - .unwrap(); - let eth_address = - EthAddress::from_str("0x00000000000000000000000000000000000000c8").unwrap(); - let token_id = TokenId::USDC; - let amount = 12345; - - let sui_bridge_event = EmittedSuiToEthTokenBridgeV1 { - nonce, - sui_chain_id, - eth_chain_id, - sui_address, - eth_address, - token_id, - amount, - }; - let encoded_bytes = BridgeAction::SuiToEthBridgeAction(SuiToEthBridgeAction { - sui_tx_digest, - sui_tx_event_index, - sui_bridge_event, - }) - .to_bytes(); - assert_eq!( - encoded_bytes, - Hex::decode("5355495f4252494447455f4d4553534147450001000000000000000a012000000000000000000000000000000000000000000000000000000000000000640b1400000000000000000000000000000000000000c8030000000000003039").unwrap(), - ); - - let hash = Keccak256::digest(encoded_bytes).digest; - assert_eq!( - hash.to_vec(), - Hex::decode("6ab34c52b6264cbc12fe8c3874f9b08f8481d2e81530d136386646dbe2f8baf4") - .unwrap(), - ); - Ok(()) +/// Check if the bridge route is valid +/// Only mainnet can bridge to mainnet, other than that we do not care. +pub fn is_route_valid(one: BridgeChainId, other: BridgeChainId) -> bool { + if one.is_sui_chain() && other.is_sui_chain() { + return false; } - - #[test] - fn test_bridge_message_encoding_blocklist_update_v1() { - telemetry_subscribers::init_for_testing(); - let registry = Registry::new(); - mysten_metrics::init_metrics(®istry); - - let pub_key_bytes = BridgeAuthorityPublicKeyBytes::from_bytes( - &Hex::decode("02321ede33d2c2d7a8a152f275a1484edef2098f034121a602cb7d767d38680aa4") - .unwrap(), - ) - .unwrap(); - let blocklist_action = BridgeAction::BlocklistCommitteeAction(BlocklistCommitteeAction { - nonce: 129, - chain_id: BridgeChainId::SuiLocalTest, - blocklist_type: BlocklistType::Blocklist, - blocklisted_members: vec![pub_key_bytes.clone()], - }); - let bytes = blocklist_action.to_bytes(); - /* - 5355495f4252494447455f4d455353414745: prefix - 01: msg type - 01: msg version - 0000000000000081: nonce - 03: chain id - 00: blocklist type - 01: length of updated members - [ - 68b43fd906c0b8f024a18c56e06744f7c6157c65 - ]: blocklisted members abi-encoded - */ - assert_eq!(bytes, Hex::decode("5355495f4252494447455f4d4553534147450101000000000000008103000168b43fd906c0b8f024a18c56e06744f7c6157c65").unwrap()); - - let pub_key_bytes_2 = BridgeAuthorityPublicKeyBytes::from_bytes( - &Hex::decode("027f1178ff417fc9f5b8290bd8876f0a157a505a6c52db100a8492203ddd1d4279") - .unwrap(), - ) - .unwrap(); - // its evm address: 0xacaef39832cb995c4e049437a3e2ec6a7bad1ab5 - let blocklist_action = BridgeAction::BlocklistCommitteeAction(BlocklistCommitteeAction { - nonce: 68, - chain_id: BridgeChainId::SuiDevnet, - blocklist_type: BlocklistType::Unblocklist, - blocklisted_members: vec![pub_key_bytes.clone(), pub_key_bytes_2.clone()], - }); - let bytes = blocklist_action.to_bytes(); - /* - 5355495f4252494447455f4d455353414745: prefix - 01: msg type - 01: msg version - 0000000000000044: nonce - 02: chain id - 01: blocklist type - 02: length of updated members - [ - 68b43fd906c0b8f024a18c56e06744f7c6157c65 - acaef39832cb995c4e049437a3e2ec6a7bad1ab5 - ]: blocklisted members abi-encoded - */ - assert_eq!(bytes, Hex::decode("5355495f4252494447455f4d4553534147450101000000000000004402010268b43fd906c0b8f024a18c56e06744f7c6157c65acaef39832cb995c4e049437a3e2ec6a7bad1ab5").unwrap()); - - let blocklist_action = BridgeAction::BlocklistCommitteeAction(BlocklistCommitteeAction { - nonce: 49, - chain_id: BridgeChainId::EthLocalTest, - blocklist_type: BlocklistType::Blocklist, - blocklisted_members: vec![pub_key_bytes.clone()], - }); - let bytes = blocklist_action.to_bytes(); - /* - 5355495f4252494447455f4d455353414745: prefix - 01: msg type - 01: msg version - 0000000000000031: nonce - 0c: chain id - 00: blocklist type - 01: length of updated members - [ - 68b43fd906c0b8f024a18c56e06744f7c6157c65 - ]: blocklisted members abi-encoded - */ - assert_eq!(bytes, Hex::decode("5355495f4252494447455f4d455353414745010100000000000000310c000168b43fd906c0b8f024a18c56e06744f7c6157c65").unwrap()); - - let blocklist_action = BridgeAction::BlocklistCommitteeAction(BlocklistCommitteeAction { - nonce: 94, - chain_id: BridgeChainId::EthSepolia, - blocklist_type: BlocklistType::Unblocklist, - blocklisted_members: vec![pub_key_bytes.clone(), pub_key_bytes_2.clone()], - }); - let bytes = blocklist_action.to_bytes(); - /* - 5355495f4252494447455f4d455353414745: prefix - 01: msg type - 01: msg version - 000000000000005e: nonce - 0b: chain id - 01: blocklist type - 02: length of updated members - [ - 00000000000000000000000068b43fd906c0b8f024a18c56e06744f7c6157c65 - 000000000000000000000000acaef39832cb995c4e049437a3e2ec6a7bad1ab5 - ]: blocklisted members abi-encoded - */ - assert_eq!(bytes, Hex::decode("5355495f4252494447455f4d4553534147450101000000000000005e0b010268b43fd906c0b8f024a18c56e06744f7c6157c65acaef39832cb995c4e049437a3e2ec6a7bad1ab5").unwrap()); + if !one.is_sui_chain() && !other.is_sui_chain() { + return false; } - - #[test] - fn test_emergency_action_encoding() { - let action = BridgeAction::EmergencyAction(EmergencyAction { - nonce: 55, - chain_id: BridgeChainId::SuiLocalTest, - action_type: EmergencyActionType::Pause, - }); - let bytes = action.to_bytes(); - /* - 5355495f4252494447455f4d455353414745: prefix - 02: msg type - 01: msg version - 0000000000000037: nonce - 03: chain id - 00: action type - */ - assert_eq!( - bytes, - Hex::decode("5355495f4252494447455f4d455353414745020100000000000000370300").unwrap() - ); - - let action = BridgeAction::EmergencyAction(EmergencyAction { - nonce: 56, - chain_id: BridgeChainId::EthSepolia, - action_type: EmergencyActionType::Unpause, - }); - let bytes = action.to_bytes(); - /* - 5355495f4252494447455f4d455353414745: prefix - 02: msg type - 01: msg version - 0000000000000038: nonce - 0b: chain id - 01: action type - */ - assert_eq!( - bytes, - Hex::decode("5355495f4252494447455f4d455353414745020100000000000000380b01").unwrap() - ); + if one == BridgeChainId::EthMainnet { + return other == BridgeChainId::SuiMainnet; } - - #[test] - fn test_limit_update_action_encoding() { - let action = BridgeAction::LimitUpdateAction(LimitUpdateAction { - nonce: 15, - chain_id: BridgeChainId::SuiLocalTest, - sending_chain_id: BridgeChainId::EthLocalTest, - new_usd_limit: 1_000_000 * USD_MULTIPLIER, // $1M USD - }); - let bytes = action.to_bytes(); - /* - 5355495f4252494447455f4d455353414745: prefix - 03: msg type - 01: msg version - 000000000000000f: nonce - 03: chain id - 0c: sending chain id - 00000002540be400: new usd limit - */ - assert_eq!( - bytes, - Hex::decode( - "5355495f4252494447455f4d4553534147450301000000000000000f030c00000002540be400" - ) - .unwrap() - ); + if one == BridgeChainId::SuiMainnet { + return other == BridgeChainId::EthMainnet; } - - #[test] - fn test_asset_price_update_action_encoding() { - let action = BridgeAction::AssetPriceUpdateAction(AssetPriceUpdateAction { - nonce: 266, - chain_id: BridgeChainId::SuiLocalTest, - token_id: TokenId::BTC, - new_usd_price: 100_000 * USD_MULTIPLIER, // $100k USD - }); - let bytes = action.to_bytes(); - /* - 5355495f4252494447455f4d455353414745: prefix - 04: msg type - 01: msg version - 000000000000010a: nonce - 03: chain id - 01: token id - 000000003b9aca00: new usd price - */ - assert_eq!( - bytes, - Hex::decode( - "5355495f4252494447455f4d4553534147450401000000000000010a0301000000003b9aca00" - ) - .unwrap() - ); + if other == BridgeChainId::EthMainnet { + return one == BridgeChainId::SuiMainnet; } - - #[test] - fn test_evm_contract_upgrade_action() { - // Calldata with only the function selector and no parameters: `function initializeV2()` - let function_signature = "initializeV2()"; - let selector = &Keccak256::digest(function_signature).digest[0..4]; - let call_data = selector.to_vec(); - assert_eq!(Hex::encode(call_data.clone()), "5cd8a76b"); - - let action = BridgeAction::EvmContractUpgradeAction(EvmContractUpgradeAction { - nonce: 123, - chain_id: BridgeChainId::EthLocalTest, - proxy_address: EthAddress::repeat_byte(6), - new_impl_address: EthAddress::repeat_byte(9), - call_data, - }); - /* - 5355495f4252494447455f4d455353414745: prefix - 05: msg type - 01: msg version - 000000000000007b: nonce - 0c: chain id - 0000000000000000000000000606060606060606060606060606060606060606: proxy address - 0000000000000000000000000909090909090909090909090909090909090909: new impl address - - 0000000000000000000000000000000000000000000000000000000000000060 - 0000000000000000000000000000000000000000000000000000000000000004 - 5cd8a76b00000000000000000000000000000000000000000000000000000000: call data - */ - assert_eq!(Hex::encode(action.to_bytes().clone()), "5355495f4252494447455f4d4553534147450501000000000000007b0c00000000000000000000000006060606060606060606060606060606060606060000000000000000000000000909090909090909090909090909090909090909000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000045cd8a76b00000000000000000000000000000000000000000000000000000000"); - - // Calldata with one parameter: `function newMockFunction(bool)` - let function_signature = "newMockFunction(bool)"; - let selector = &Keccak256::digest(function_signature).digest[0..4]; - let mut call_data = selector.to_vec(); - call_data.extend(ethers::abi::encode(&[ethers::abi::Token::Bool(true)])); - assert_eq!( - Hex::encode(call_data.clone()), - "417795ef0000000000000000000000000000000000000000000000000000000000000001" - ); - let action = BridgeAction::EvmContractUpgradeAction(EvmContractUpgradeAction { - nonce: 123, - chain_id: BridgeChainId::EthLocalTest, - proxy_address: EthAddress::repeat_byte(6), - new_impl_address: EthAddress::repeat_byte(9), - call_data, - }); - /* - 5355495f4252494447455f4d455353414745: prefix - 05: msg type - 01: msg version - 000000000000007b: nonce - 0c: chain id - 0000000000000000000000000606060606060606060606060606060606060606: proxy address - 0000000000000000000000000909090909090909090909090909090909090909: new impl address - - 0000000000000000000000000000000000000000000000000000000000000060 - 0000000000000000000000000000000000000000000000000000000000000024 - 417795ef00000000000000000000000000000000000000000000000000000000 - 0000000100000000000000000000000000000000000000000000000000000000: call data - */ - assert_eq!(Hex::encode(action.to_bytes().clone()), "5355495f4252494447455f4d4553534147450501000000000000007b0c0000000000000000000000000606060606060606060606060606060606060606000000000000000000000000090909090909090909090909090909090909090900000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000024417795ef000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000"); - - // Calldata with two parameters: `function newerMockFunction(bool, uint8)` - let function_signature = "newMockFunction(bool,uint8)"; - let selector = &Keccak256::digest(function_signature).digest[0..4]; - let mut call_data = selector.to_vec(); - call_data.extend(ethers::abi::encode(&[ - ethers::abi::Token::Bool(true), - ethers::abi::Token::Uint(42u8.into()), - ])); - assert_eq!( - Hex::encode(call_data.clone()), - "be8fc25d0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002a" - ); - let action = BridgeAction::EvmContractUpgradeAction(EvmContractUpgradeAction { - nonce: 123, - chain_id: BridgeChainId::EthLocalTest, - proxy_address: EthAddress::repeat_byte(6), - new_impl_address: EthAddress::repeat_byte(9), - call_data, - }); - /* - 5355495f4252494447455f4d455353414745: prefix - 05: msg type - 01: msg version - 000000000000007b: nonce - 0c: chain id - 0000000000000000000000000606060606060606060606060606060606060606: proxy address - 0000000000000000000000000909090909090909090909090909090909090909: new impl address - - 0000000000000000000000000000000000000000000000000000000000000060 - 0000000000000000000000000000000000000000000000000000000000000044 - be8fc25d00000000000000000000000000000000000000000000000000000000 - 0000000100000000000000000000000000000000000000000000000000000000 - 0000002a00000000000000000000000000000000000000000000000000000000: call data - */ - assert_eq!(Hex::encode(action.to_bytes().clone()), "5355495f4252494447455f4d4553534147450501000000000000007b0c0000000000000000000000000606060606060606060606060606060606060606000000000000000000000000090909090909090909090909090909090909090900000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000044be8fc25d0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002a00000000000000000000000000000000000000000000000000000000"); - - // Empty calldate - let action = BridgeAction::EvmContractUpgradeAction(EvmContractUpgradeAction { - nonce: 123, - chain_id: BridgeChainId::EthLocalTest, - proxy_address: EthAddress::repeat_byte(6), - new_impl_address: EthAddress::repeat_byte(9), - call_data: vec![], - }); - /* - 5355495f4252494447455f4d455353414745: prefix - 05: msg type - 01: msg version - 000000000000007b: nonce - 0c: chain id - 0000000000000000000000000606060606060606060606060606060606060606: proxy address - 0000000000000000000000000909090909090909090909090909090909090909: new impl address - - 0000000000000000000000000000000000000000000000000000000000000060 - 0000000000000000000000000000000000000000000000000000000000000000: call data - */ - let data = action.to_bytes(); - assert_eq!(Hex::encode(data.clone()), "5355495f4252494447455f4d4553534147450501000000000000007b0c0000000000000000000000000606060606060606060606060606060606060606000000000000000000000000090909090909090909090909090909090909090900000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000"); - let types = vec![ParamType::Address, ParamType::Address, ParamType::Bytes]; - // Ensure that the call data (start from bytes 29) can be decoded - ethers::abi::decode(&types, &data[29..]).unwrap(); + if other == BridgeChainId::SuiMainnet { + return one == BridgeChainId::EthMainnet; } + true +} - #[test] - fn test_bridge_message_encoding_regression_eth_to_sui_token_bridge_v1() -> anyhow::Result<()> { - telemetry_subscribers::init_for_testing(); - let registry = Registry::new(); - mysten_metrics::init_metrics(®istry); - let eth_tx_hash = TxHash::random(); - let eth_event_index = 1u16; - - let nonce = 10u64; - let sui_chain_id = BridgeChainId::SuiTestnet; - let eth_chain_id = BridgeChainId::EthSepolia; - let sui_address = SuiAddress::from_str( - "0x0000000000000000000000000000000000000000000000000000000000000064", - ) - .unwrap(); - let eth_address = - EthAddress::from_str("0x00000000000000000000000000000000000000c8").unwrap(); - let token_id = TokenId::USDC; - let amount = 12345; - - let eth_bridge_event = EthToSuiTokenBridgeV1 { - nonce, - sui_chain_id, - eth_chain_id, - sui_address, - eth_address, - token_id, - amount, - }; - let encoded_bytes = BridgeAction::EthToSuiBridgeAction(EthToSuiBridgeAction { - eth_tx_hash, - eth_event_index, - eth_bridge_event, - }) - .to_bytes(); - - assert_eq!( - encoded_bytes, - Hex::decode("5355495f4252494447455f4d4553534147450001000000000000000a0b1400000000000000000000000000000000000000c801200000000000000000000000000000000000000000000000000000000000000064030000000000003039").unwrap(), - ); +#[cfg(test)] +mod tests { + use crate::test_utils::get_test_authority_and_key; + use crate::test_utils::get_test_eth_to_sui_bridge_action; + use crate::test_utils::get_test_sui_to_eth_bridge_action; + use ethers::types::Address as EthAddress; + use fastcrypto::traits::KeyPair; + use std::collections::HashSet; + use sui_types::bridge::TOKEN_ID_BTC; + use sui_types::crypto::get_key_pair; - let hash = Keccak256::digest(encoded_bytes).digest; - assert_eq!( - hash.to_vec(), - Hex::decode("b352508c301a37bb1b68a75dd0fc42b6f692b2650818631c8f8a4d4d3e5bef46") - .unwrap(), - ); - Ok(()) - } + use super::*; #[test] fn test_bridge_committee_construction() -> anyhow::Result<()> { - let (mut authority, _, _) = get_test_authority_and_key(10000, 9999); + let (mut authority, _, _) = get_test_authority_and_key(8000, 9999); // This is ok let _ = BridgeCommittee::new(vec![authority.clone()]).unwrap(); - // This is not ok - total voting power != 10000 - authority.voting_power = 9999; + // This is not ok - total voting power < BRIDGE_COMMITTEE_MINIMAL_VOTING_POWER + authority.voting_power = BRIDGE_COMMITTEE_MINIMAL_VOTING_POWER - 1; let _ = BridgeCommittee::new(vec![authority.clone()]).unwrap_err(); - // This is not ok - total voting power != 10000 - authority.voting_power = 10001; + // This is not ok - total voting power > BRIDGE_COMMITTEE_MAXIMAL_VOTING_POWER + authority.voting_power = BRIDGE_COMMITTEE_MAXIMAL_VOTING_POWER + 1; let _ = BridgeCommittee::new(vec![authority.clone()]).unwrap_err(); // This is ok @@ -1270,6 +555,64 @@ mod tests { Ok(()) } + // Regression test to avoid accidentally change to approval threshold + #[test] + fn test_bridge_action_approval_threshold_regression_test() -> anyhow::Result<()> { + let action = get_test_sui_to_eth_bridge_action(None, None, None, None, None, None, None); + assert_eq!(action.approval_threshold(), 3334); + + let action = get_test_eth_to_sui_bridge_action(None, None, None); + assert_eq!(action.approval_threshold(), 3334); + + let action = BridgeAction::BlocklistCommitteeAction(BlocklistCommitteeAction { + nonce: 94, + chain_id: BridgeChainId::EthSepolia, + blocklist_type: BlocklistType::Unblocklist, + blocklisted_members: vec![], + }); + assert_eq!(action.approval_threshold(), 5001); + + let action = BridgeAction::EmergencyAction(EmergencyAction { + nonce: 56, + chain_id: BridgeChainId::EthSepolia, + action_type: EmergencyActionType::Pause, + }); + assert_eq!(action.approval_threshold(), 450); + + let action = BridgeAction::EmergencyAction(EmergencyAction { + nonce: 56, + chain_id: BridgeChainId::EthSepolia, + action_type: EmergencyActionType::Unpause, + }); + assert_eq!(action.approval_threshold(), 5001); + + let action = BridgeAction::LimitUpdateAction(LimitUpdateAction { + nonce: 15, + chain_id: BridgeChainId::SuiCustom, + sending_chain_id: BridgeChainId::EthCustom, + new_usd_limit: 1_000_000 * USD_MULTIPLIER, + }); + assert_eq!(action.approval_threshold(), 5001); + + let action = BridgeAction::AssetPriceUpdateAction(AssetPriceUpdateAction { + nonce: 266, + chain_id: BridgeChainId::SuiCustom, + token_id: TOKEN_ID_BTC, + new_usd_price: 100_000 * USD_MULTIPLIER, + }); + assert_eq!(action.approval_threshold(), 5001); + + let action = BridgeAction::EvmContractUpgradeAction(EvmContractUpgradeAction { + nonce: 123, + chain_id: BridgeChainId::EthCustom, + proxy_address: EthAddress::repeat_byte(6), + new_impl_address: EthAddress::repeat_byte(9), + call_data: vec![], + }); + assert_eq!(action.approval_threshold(), 5001); + Ok(()) + } + #[test] fn test_bridge_committee_filter_blocklisted_authorities() -> anyhow::Result<()> { // Note: today BridgeCommittee does not shuffle authorities diff --git a/crates/sui-bridge/src/utils.rs b/crates/sui-bridge/src/utils.rs new file mode 100644 index 0000000000000..5d8c529f61bb9 --- /dev/null +++ b/crates/sui-bridge/src/utils.rs @@ -0,0 +1,229 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::config::BridgeNodeConfig; +use crate::config::EthConfig; +use crate::config::SuiConfig; +use crate::crypto::BridgeAuthorityKeyPair; +use crate::crypto::BridgeAuthorityPublicKeyBytes; +use anyhow::anyhow; +use ethers::core::k256::ecdsa::SigningKey; +use ethers::middleware::SignerMiddleware; +use ethers::prelude::*; +use ethers::providers::{Http, Provider}; +use ethers::signers::Wallet; +use fastcrypto::ed25519::Ed25519KeyPair; +use fastcrypto::secp256k1::Secp256k1KeyPair; +use fastcrypto::traits::EncodeDecodeBase64; +use std::path::PathBuf; +use std::str::FromStr; +use sui_config::Config; +use sui_types::base_types::SuiAddress; +use sui_types::bridge::BridgeChainId; +use sui_types::crypto::get_key_pair; +use sui_types::crypto::SuiKeyPair; + +use crate::server::APPLICATION_JSON; +use crate::types::{AddTokensOnSuiAction, BridgeAction}; + +use sui_json_rpc_types::SuiTransactionBlockResponse; +use sui_sdk::wallet_context::WalletContext; +use sui_types::bridge::{BRIDGE_MODULE_NAME, BRIDGE_REGISTER_FOREIGN_TOKEN_FUNCTION_NAME}; +use sui_types::programmable_transaction_builder::ProgrammableTransactionBuilder; +use sui_types::transaction::{ObjectArg, TransactionData}; +use sui_types::BRIDGE_PACKAGE_ID; + +pub type EthSigner = SignerMiddleware, Wallet>; + +/// Generate Bridge Authority key (Secp256k1KeyPair) and write to a file as base64 encoded `privkey`. +pub fn generate_bridge_authority_key_and_write_to_file( + path: &PathBuf, +) -> Result<(), anyhow::Error> { + let (_, kp): (_, BridgeAuthorityKeyPair) = get_key_pair(); + let eth_address = BridgeAuthorityPublicKeyBytes::from(&kp.public).to_eth_address(); + println!( + "Corresponding Ethereum address by this ecdsa key: {:?}", + eth_address + ); + let sui_address = SuiAddress::from(&kp.public); + println!( + "Corresponding Sui address by this ecdsa key: {:?}", + sui_address + ); + let base64_encoded = kp.encode_base64(); + std::fs::write(path, base64_encoded) + .map_err(|err| anyhow!("Failed to write encoded key to path: {:?}", err)) +} + +/// Generate Bridge Client key (Secp256k1KeyPair or Ed25519KeyPair) and write to a file as base64 encoded `flag || privkey`. +pub fn generate_bridge_client_key_and_write_to_file( + path: &PathBuf, + use_ecdsa: bool, +) -> Result<(), anyhow::Error> { + let kp = if use_ecdsa { + let (_, kp): (_, Secp256k1KeyPair) = get_key_pair(); + let eth_address = BridgeAuthorityPublicKeyBytes::from(&kp.public).to_eth_address(); + println!( + "Corresponding Ethereum address by this ecdsa key: {:?}", + eth_address + ); + SuiKeyPair::from(kp) + } else { + let (_, kp): (_, Ed25519KeyPair) = get_key_pair(); + SuiKeyPair::from(kp) + }; + let sui_address = SuiAddress::from(&kp.public()); + println!("Corresponding Sui address by this key: {:?}", sui_address); + + let contents = kp.encode_base64(); + std::fs::write(path, contents) + .map_err(|err| anyhow!("Failed to write encoded key to path: {:?}", err)) +} + +/// Generate Bridge Node Config template and write to a file. +pub fn generate_bridge_node_config_and_write_to_file( + path: &PathBuf, + run_client: bool, +) -> Result<(), anyhow::Error> { + let mut config = BridgeNodeConfig { + server_listen_port: 9191, + metrics_port: 9184, + bridge_authority_key_path_base64_raw: PathBuf::from("/path/to/your/bridge_authority_key"), + sui: SuiConfig { + sui_rpc_url: "your_sui_rpc_url".to_string(), + sui_bridge_chain_id: BridgeChainId::SuiTestnet as u8, + bridge_client_key_path_base64_sui_key: None, + bridge_client_gas_object: None, + sui_bridge_module_last_processed_event_id_override: None, + }, + eth: EthConfig { + eth_rpc_url: "your_eth_rpc_url".to_string(), + eth_bridge_proxy_address: "0x0000000000000000000000000000000000000000".to_string(), + eth_bridge_chain_id: BridgeChainId::EthSepolia as u8, + eth_contracts_start_block_fallback: Some(0), + eth_contracts_start_block_override: None, + }, + approved_governance_actions: vec![], + run_client, + db_path: None, + }; + if run_client { + config.sui.bridge_client_key_path_base64_sui_key = + Some(PathBuf::from("/path/to/your/bridge_client_key")); + config.db_path = Some(PathBuf::from("/path/to/your/client_db")); + } + config.save(path) +} + +pub async fn get_eth_signer_client(url: &str, private_key_hex: &str) -> anyhow::Result { + let provider = Provider::::try_from(url) + .unwrap() + .interval(std::time::Duration::from_millis(2000)); + let chain_id = provider.get_chainid().await?; + let wallet = Wallet::from_str(private_key_hex) + .unwrap() + .with_chain_id(chain_id.as_u64()); + Ok(SignerMiddleware::new(provider, wallet)) +} + +pub async fn publish_coins_return_add_coins_on_sui_action( + wallet_context: &mut WalletContext, + bridge_arg: ObjectArg, + publish_coin_responses: Vec, + token_ids: Vec, + token_prices: Vec, + nonce: u64, +) -> BridgeAction { + assert!(token_ids.len() == publish_coin_responses.len()); + assert!(token_prices.len() == publish_coin_responses.len()); + let sender = wallet_context.active_address().unwrap(); + let rgp = wallet_context.get_reference_gas_price().await.unwrap(); + let mut token_type_names = vec![]; + for response in publish_coin_responses { + let object_changes = response.object_changes.unwrap(); + let mut tc = None; + let mut type_ = None; + let mut uc = None; + let mut metadata = None; + for object_change in &object_changes { + if let o @ sui_json_rpc_types::ObjectChange::Created { object_type, .. } = object_change + { + if object_type.name.as_str().starts_with("TreasuryCap") { + assert!(tc.is_none() && type_.is_none()); + tc = Some(o.clone()); + type_ = Some(object_type.type_params.first().unwrap().clone()); + } else if object_type.name.as_str().starts_with("UpgradeCap") { + assert!(uc.is_none()); + uc = Some(o.clone()); + } else if object_type.name.as_str().starts_with("CoinMetadata") { + assert!(metadata.is_none()); + metadata = Some(o.clone()); + } + } + } + let (tc, type_, uc, metadata) = + (tc.unwrap(), type_.unwrap(), uc.unwrap(), metadata.unwrap()); + + // register with the bridge + let mut builder = ProgrammableTransactionBuilder::new(); + let bridge_arg = builder.obj(bridge_arg).unwrap(); + let uc_arg = builder + .obj(ObjectArg::ImmOrOwnedObject(uc.object_ref())) + .unwrap(); + let tc_arg = builder + .obj(ObjectArg::ImmOrOwnedObject(tc.object_ref())) + .unwrap(); + let metadata_arg = builder + .obj(ObjectArg::ImmOrOwnedObject(metadata.object_ref())) + .unwrap(); + builder.programmable_move_call( + BRIDGE_PACKAGE_ID, + BRIDGE_MODULE_NAME.into(), + BRIDGE_REGISTER_FOREIGN_TOKEN_FUNCTION_NAME.into(), + vec![type_.clone()], + vec![bridge_arg, tc_arg, uc_arg, metadata_arg], + ); + let pt = builder.finish(); + let gas = wallet_context + .get_one_gas_object_owned_by_address(sender) + .await + .unwrap() + .unwrap(); + let tx = TransactionData::new_programmable(sender, vec![gas], pt, 1_000_000_000, rgp); + let signed_tx = wallet_context.sign_transaction(&tx); + let _ = wallet_context + .execute_transaction_must_succeed(signed_tx) + .await; + + token_type_names.push(type_); + } + + BridgeAction::AddTokensOnSuiAction(AddTokensOnSuiAction { + nonce, + chain_id: BridgeChainId::SuiCustom, + native: false, + token_ids, + token_type_names, + token_prices, + }) +} + +pub async fn wait_for_server_to_be_up(server_url: String, timeout_sec: u64) -> anyhow::Result<()> { + let now = std::time::Instant::now(); + loop { + if let Ok(true) = reqwest::Client::new() + .get(server_url.clone()) + .header(reqwest::header::ACCEPT, APPLICATION_JSON) + .send() + .await + .map(|res| res.status().is_success()) + { + break; + } + if now.elapsed().as_secs() > timeout_sec { + anyhow::bail!("Server is not up and running after {} seconds", timeout_sec); + } + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + } + Ok(()) +} diff --git a/crates/sui-config/src/genesis.rs b/crates/sui-config/src/genesis.rs index 33040dbfde121..4a2eafa51dcc0 100644 --- a/crates/sui-config/src/genesis.rs +++ b/crates/sui-config/src/genesis.rs @@ -23,12 +23,12 @@ use sui_types::sui_system_state::{ SuiSystemStateWrapper, SuiValidatorGenesis, }; use sui_types::transaction::Transaction; -use sui_types::SUI_RANDOMNESS_STATE_OBJECT_ID; use sui_types::{ committee::{Committee, EpochId, ProtocolVersion}, error::SuiResult, object::Object, }; +use sui_types::{SUI_BRIDGE_OBJECT_ID, SUI_RANDOMNESS_STATE_OBJECT_ID}; use tracing::trace; #[derive(Clone, Debug)] @@ -333,6 +333,13 @@ impl UnsignedGenesis { .is_some() } + pub fn has_bridge_object(&self) -> bool { + self.objects() + .get_object(&SUI_BRIDGE_OBJECT_ID) + .expect("read from genesis cannot fail") + .is_some() + } + pub fn coin_deny_list_state(&self) -> Option { get_coin_deny_list(&self.objects()) } diff --git a/crates/sui-core/src/authority.rs b/crates/sui-core/src/authority.rs index bc281fe7abb38..285540fa22844 100644 --- a/crates/sui-core/src/authority.rs +++ b/crates/sui-core/src/authority.rs @@ -4410,6 +4410,50 @@ impl AuthorityState { Some(tx) } + #[instrument(level = "debug", skip_all)] + fn create_bridge_tx( + &self, + epoch_store: &Arc, + ) -> Option { + if !epoch_store.protocol_config().enable_bridge() { + info!("bridge not enabled"); + return None; + } + if epoch_store.bridge_exists() { + return None; + } + let tx = EndOfEpochTransactionKind::new_bridge_create(epoch_store.get_chain_identifier()); + info!("Creating Bridge Create tx"); + Some(tx) + } + + #[instrument(level = "debug", skip_all)] + fn init_bridge_committee_tx( + &self, + epoch_store: &Arc, + ) -> Option { + if !epoch_store.protocol_config().enable_bridge() { + info!("bridge not enabled"); + return None; + } + // Only create this transaction if bridge exists + if !epoch_store.bridge_exists() { + return None; + } + + if epoch_store.bridge_committee_initiated() { + return None; + } + + let bridge_initial_shared_version = epoch_store + .epoch_start_config() + .bridge_obj_initial_shared_version() + .expect("initial version must exist"); + let tx = EndOfEpochTransactionKind::init_bridge_committee(bridge_initial_shared_version); + info!("Init Bridge committee tx"); + Some(tx) + } + #[instrument(level = "debug", skip_all)] fn create_deny_list_state_tx( &self, @@ -4454,6 +4498,12 @@ impl AuthorityState { if let Some(tx) = self.create_randomness_state_tx(epoch_store) { txns.push(tx); } + if let Some(tx) = self.create_bridge_tx(epoch_store) { + txns.push(tx); + } + if let Some(tx) = self.init_bridge_committee_tx(epoch_store) { + txns.push(tx); + } if let Some(tx) = self.create_deny_list_state_tx(epoch_store) { txns.push(tx); } diff --git a/crates/sui-core/src/authority/authority_per_epoch_store.rs b/crates/sui-core/src/authority/authority_per_epoch_store.rs index 1ac4913444fe8..5e0db018a0a4a 100644 --- a/crates/sui-core/src/authority/authority_per_epoch_store.rs +++ b/crates/sui-core/src/authority/authority_per_epoch_store.rs @@ -881,6 +881,16 @@ impl AuthorityPerEpochStore { self.protocol_config().enable_coin_deny_list() && self.coin_deny_list_state_exists() } + pub fn bridge_exists(&self) -> bool { + self.epoch_start_configuration + .bridge_obj_initial_shared_version() + .is_some() + } + + pub fn bridge_committee_initiated(&self) -> bool { + self.epoch_start_configuration.bridge_committee_initiated() + } + pub fn get_parent_path(&self) -> PathBuf { self.parent_path.clone() } diff --git a/crates/sui-core/src/authority/epoch_start_configuration.rs b/crates/sui-core/src/authority/epoch_start_configuration.rs index 88d2b7c5de0e0..1debf59ace03c 100644 --- a/crates/sui-core/src/authority/epoch_start_configuration.rs +++ b/crates/sui-core/src/authority/epoch_start_configuration.rs @@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize}; use std::fmt; use sui_types::authenticator_state::get_authenticator_state_obj_initial_shared_version; use sui_types::base_types::SequenceNumber; +use sui_types::bridge::{get_bridge_obj_initial_shared_version, is_bridge_committee_initiated}; use sui_types::deny_list::get_deny_list_obj_initial_shared_version; use sui_types::epoch_data::EpochData; use sui_types::error::SuiResult; @@ -25,6 +26,8 @@ pub trait EpochStartConfigTrait { fn authenticator_obj_initial_shared_version(&self) -> Option; fn randomness_obj_initial_shared_version(&self) -> Option; fn coin_deny_list_obj_initial_shared_version(&self) -> Option; + fn bridge_obj_initial_shared_version(&self) -> Option; + fn bridge_committee_initiated(&self) -> bool; } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] @@ -43,6 +46,7 @@ pub enum EpochStartConfiguration { V3(EpochStartConfigurationV3), V4(EpochStartConfigurationV4), V5(EpochStartConfigurationV5), + V6(EpochStartConfigurationV6), } impl EpochStartConfiguration { @@ -58,13 +62,18 @@ impl EpochStartConfiguration { get_randomness_state_obj_initial_shared_version(object_store)?; let coin_deny_list_obj_initial_shared_version = get_deny_list_obj_initial_shared_version(object_store); - Ok(Self::V5(EpochStartConfigurationV5 { + let bridge_obj_initial_shared_version = + get_bridge_obj_initial_shared_version(object_store)?; + let bridge_committee_initiated = is_bridge_committee_initiated(object_store)?; + Ok(Self::V6(EpochStartConfigurationV6 { system_state, epoch_digest, flags: initial_epoch_flags.unwrap_or_else(EpochFlag::default_flags_for_new_epoch), authenticator_obj_initial_shared_version, randomness_obj_initial_shared_version, coin_deny_list_obj_initial_shared_version, + bridge_obj_initial_shared_version, + bridge_committee_initiated, })) } @@ -132,6 +141,19 @@ pub struct EpochStartConfigurationV5 { coin_deny_list_obj_initial_shared_version: Option, } +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct EpochStartConfigurationV6 { + system_state: EpochStartSystemState, + epoch_digest: CheckpointDigest, + flags: Vec, + /// Do the state objects exist at the beginning of the epoch? + authenticator_obj_initial_shared_version: Option, + randomness_obj_initial_shared_version: Option, + coin_deny_list_obj_initial_shared_version: Option, + bridge_obj_initial_shared_version: Option, + bridge_committee_initiated: bool, +} + impl EpochStartConfigurationV1 { pub fn new(system_state: EpochStartSystemState, epoch_digest: CheckpointDigest) -> Self { Self { @@ -165,6 +187,14 @@ impl EpochStartConfigTrait for EpochStartConfigurationV1 { fn coin_deny_list_obj_initial_shared_version(&self) -> Option { None } + + fn bridge_obj_initial_shared_version(&self) -> Option { + None + } + + fn bridge_committee_initiated(&self) -> bool { + false + } } impl EpochStartConfigTrait for EpochStartConfigurationV2 { @@ -191,6 +221,14 @@ impl EpochStartConfigTrait for EpochStartConfigurationV2 { fn coin_deny_list_obj_initial_shared_version(&self) -> Option { None } + + fn bridge_obj_initial_shared_version(&self) -> Option { + None + } + + fn bridge_committee_initiated(&self) -> bool { + false + } } impl EpochStartConfigTrait for EpochStartConfigurationV3 { @@ -217,6 +255,13 @@ impl EpochStartConfigTrait for EpochStartConfigurationV3 { fn coin_deny_list_obj_initial_shared_version(&self) -> Option { None } + + fn bridge_obj_initial_shared_version(&self) -> Option { + None + } + fn bridge_committee_initiated(&self) -> bool { + false + } } impl EpochStartConfigTrait for EpochStartConfigurationV4 { @@ -243,6 +288,14 @@ impl EpochStartConfigTrait for EpochStartConfigurationV4 { fn coin_deny_list_obj_initial_shared_version(&self) -> Option { None } + + fn bridge_obj_initial_shared_version(&self) -> Option { + None + } + + fn bridge_committee_initiated(&self) -> bool { + false + } } impl EpochStartConfigTrait for EpochStartConfigurationV5 { @@ -269,6 +322,47 @@ impl EpochStartConfigTrait for EpochStartConfigurationV5 { fn coin_deny_list_obj_initial_shared_version(&self) -> Option { self.coin_deny_list_obj_initial_shared_version } + + fn bridge_obj_initial_shared_version(&self) -> Option { + None + } + fn bridge_committee_initiated(&self) -> bool { + false + } +} + +impl EpochStartConfigTrait for EpochStartConfigurationV6 { + fn epoch_digest(&self) -> CheckpointDigest { + self.epoch_digest + } + + fn epoch_start_state(&self) -> &EpochStartSystemState { + &self.system_state + } + + fn flags(&self) -> &[EpochFlag] { + &self.flags + } + + fn authenticator_obj_initial_shared_version(&self) -> Option { + self.authenticator_obj_initial_shared_version + } + + fn randomness_obj_initial_shared_version(&self) -> Option { + self.randomness_obj_initial_shared_version + } + + fn coin_deny_list_obj_initial_shared_version(&self) -> Option { + self.coin_deny_list_obj_initial_shared_version + } + + fn bridge_obj_initial_shared_version(&self) -> Option { + self.bridge_obj_initial_shared_version + } + + fn bridge_committee_initiated(&self) -> bool { + self.bridge_committee_initiated + } } impl EpochFlag { diff --git a/crates/sui-core/src/execution_cache.rs b/crates/sui-core/src/execution_cache.rs index 3c66f93961b0b..e300be7af004d 100644 --- a/crates/sui-core/src/execution_cache.rs +++ b/crates/sui-core/src/execution_cache.rs @@ -9,6 +9,7 @@ use crate::authority::{ }; use crate::transaction_outputs::TransactionOutputs; use async_trait::async_trait; +use sui_types::bridge::Bridge; use futures::{future::BoxFuture, FutureExt}; use prometheus::{register_int_gauge_with_registry, IntGauge, Registry}; @@ -423,6 +424,8 @@ pub trait ExecutionCacheRead: Send + Sync { fn get_sui_system_state_object_unsafe(&self) -> SuiResult; + fn get_bridge_object_unsafe(&self) -> SuiResult; + // Marker methods /// Get the marker at a specific version diff --git a/crates/sui-core/src/execution_cache/passthrough_cache.rs b/crates/sui-core/src/execution_cache/passthrough_cache.rs index 1e951bfe49123..c5a7f55344d96 100644 --- a/crates/sui-core/src/execution_cache/passthrough_cache.rs +++ b/crates/sui-core/src/execution_cache/passthrough_cache.rs @@ -27,6 +27,7 @@ use sui_storage::package_object_cache::PackageObjectCache; use sui_types::accumulator::Accumulator; use sui_types::base_types::VerifiedExecutionData; use sui_types::base_types::{EpochId, ObjectID, ObjectRef, SequenceNumber}; +use sui_types::bridge::{get_bridge, Bridge}; use sui_types::digests::{TransactionDigest, TransactionEffectsDigest, TransactionEventsDigest}; use sui_types::effects::{TransactionEffects, TransactionEvents}; use sui_types::error::{SuiError, SuiResult}; @@ -249,6 +250,10 @@ impl ExecutionCacheRead for PassthroughCache { get_sui_system_state(self) } + fn get_bridge_object_unsafe(&self) -> SuiResult { + get_bridge(self) + } + fn get_marker_value( &self, object_id: &ObjectID, diff --git a/crates/sui-core/src/execution_cache/writeback_cache.rs b/crates/sui-core/src/execution_cache/writeback_cache.rs index c964bb4abfab9..5467f8b7fd731 100644 --- a/crates/sui-core/src/execution_cache/writeback_cache.rs +++ b/crates/sui-core/src/execution_cache/writeback_cache.rs @@ -70,6 +70,7 @@ use sui_macros::fail_point_async; use sui_protocol_config::ProtocolVersion; use sui_types::accumulator::Accumulator; use sui_types::base_types::{EpochId, ObjectID, ObjectRef, SequenceNumber, VerifiedExecutionData}; +use sui_types::bridge::Bridge; use sui_types::digests::{ ObjectDigest, TransactionDigest, TransactionEffectsDigest, TransactionEventsDigest, }; @@ -1369,6 +1370,10 @@ impl ExecutionCacheRead for WritebackCache { )?; Ok(()) } + + fn get_bridge_object_unsafe(&self) -> SuiResult { + todo!() + } } impl ExecutionCacheWrite for WritebackCache { diff --git a/crates/sui-core/tests/staged/sui.yaml b/crates/sui-core/tests/staged/sui.yaml index 2e5268772b311..87c3ce7f089ba 100644 --- a/crates/sui-core/tests/staged/sui.yaml +++ b/crates/sui-core/tests/staged/sui.yaml @@ -52,6 +52,9 @@ CallArg: Object: NEWTYPE: TYPENAME: ObjectArg +ChainIdentifier: + NEWTYPESTRUCT: + TYPENAME: CheckpointDigest ChangeEpoch: STRUCT: - epoch: U64 @@ -306,6 +309,14 @@ EndOfEpochTransactionKind: RandomnessStateCreate: UNIT 4: DenyListStateCreate: UNIT + 5: + BridgeStateCreate: + NEWTYPE: + TYPENAME: ChainIdentifier + 6: + BridgeCommitteeInit: + NEWTYPE: + TYPENAME: SequenceNumber Envelope: STRUCT: - data: diff --git a/crates/sui-e2e-tests/Cargo.toml b/crates/sui-e2e-tests/Cargo.toml index da7d29bca5d17..cd01272caa4df 100644 --- a/crates/sui-e2e-tests/Cargo.toml +++ b/crates/sui-e2e-tests/Cargo.toml @@ -36,6 +36,7 @@ fastcrypto.workspace = true fastcrypto-zkp.workspace = true move-core-types.workspace = true +sui-bridge.workspace = true sui-core.workspace = true sui-framework.workspace = true sui-json-rpc.workspace = true diff --git a/crates/sui-e2e-tests/tests/bridge_tests.rs b/crates/sui-e2e-tests/tests/bridge_tests.rs new file mode 100644 index 0000000000000..981d5a922908c --- /dev/null +++ b/crates/sui-e2e-tests/tests/bridge_tests.rs @@ -0,0 +1,105 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use sui_bridge::crypto::BridgeAuthorityKeyPair; +use sui_bridge::BRIDGE_ENABLE_PROTOCOL_VERSION; +use sui_json_rpc_api::BridgeReadApiClient; +use sui_macros::sim_test; +use sui_types::bridge::get_bridge; +use sui_types::bridge::BridgeTrait; +use sui_types::crypto::get_key_pair; +use sui_types::SUI_BRIDGE_OBJECT_ID; +use test_cluster::TestClusterBuilder; + +#[sim_test] +async fn test_create_bridge_state_object() { + let test_cluster = TestClusterBuilder::new() + .with_protocol_version((BRIDGE_ENABLE_PROTOCOL_VERSION - 1).into()) + .with_epoch_duration_ms(20000) + .build() + .await; + + let handles = test_cluster.all_node_handles(); + + // no node has the bridge state object yet + for h in &handles { + h.with(|node| { + assert!(node + .state() + .get_cache_reader() + .get_latest_object_ref_or_tombstone(SUI_BRIDGE_OBJECT_ID) + .unwrap() + .is_none()); + }); + } + + // wait until feature is enabled + test_cluster + .wait_for_protocol_version(BRIDGE_ENABLE_PROTOCOL_VERSION.into()) + .await; + // wait until next epoch - authenticator state object is created at the end of the first epoch + // in which it is supported. + test_cluster.wait_for_epoch_all_nodes(2).await; // protocol upgrade completes in epoch 1 + + for h in &handles { + h.with(|node| { + node.state() + .get_cache_reader() + .get_latest_object_ref_or_tombstone(SUI_BRIDGE_OBJECT_ID) + .unwrap() + .expect("auth state object should exist"); + }); + } +} + +#[tokio::test] +async fn test_committee_registration() { + telemetry_subscribers::init_for_testing(); + let mut bridge_keys = vec![]; + for _ in 0..=3 { + let (_, kp): (_, BridgeAuthorityKeyPair) = get_key_pair(); + bridge_keys.push(kp); + } + let test_cluster: test_cluster::TestCluster = TestClusterBuilder::new() + .with_protocol_version((BRIDGE_ENABLE_PROTOCOL_VERSION).into()) + .build_with_bridge(bridge_keys, false) + .await; + + let bridge = get_bridge( + test_cluster + .fullnode_handle + .sui_node + .state() + .get_object_store(), + ) + .unwrap(); + + // Member should be empty before end of epoch + assert!(bridge.committee().members.contents.is_empty()); + assert_eq!( + test_cluster.swarm.active_validators().count(), + bridge.committee().member_registrations.contents.len() + ); + + test_cluster + .trigger_reconfiguration_if_not_yet_and_assert_bridge_committee_initialized() + .await; +} + +#[tokio::test] +async fn test_bridge_api_compatibility() { + let test_cluster: test_cluster::TestCluster = TestClusterBuilder::new() + .with_protocol_version(BRIDGE_ENABLE_PROTOCOL_VERSION.into()) + .build() + .await; + + test_cluster.trigger_reconfiguration().await; + let client = test_cluster.rpc_client(); + client.get_latest_bridge().await.unwrap(); + // TODO: assert fields in summary + + client + .get_bridge_object_initial_shared_version() + .await + .unwrap(); +} diff --git a/crates/sui-e2e-tests/tests/framework_upgrades/mock_sui_systems/base/sources/sui_system.move b/crates/sui-e2e-tests/tests/framework_upgrades/mock_sui_systems/base/sources/sui_system.move index 100d9df9ca5a6..335667589ca8e 100644 --- a/crates/sui-e2e-tests/tests/framework_upgrades/mock_sui_systems/base/sources/sui_system.move +++ b/crates/sui-e2e-tests/tests/framework_upgrades/mock_sui_systems/base/sources/sui_system.move @@ -2,6 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 module sui_system::sui_system { + use std::vector; + use sui::balance::Balance; use sui::object::UID; use sui::sui::SUI; @@ -73,6 +75,10 @@ module sui_system::sui_system { storage_rebate } + public fun active_validator_addresses(wrapper: &mut SuiSystemState): vector

{ + vector::empty() + } + fun load_system_state_mut(self: &mut SuiSystemState): &mut SuiSystemStateInner { load_inner_maybe_upgrade(self) } diff --git a/crates/sui-e2e-tests/tests/framework_upgrades/mock_sui_systems/deep_upgrade/sources/sui_system.move b/crates/sui-e2e-tests/tests/framework_upgrades/mock_sui_systems/deep_upgrade/sources/sui_system.move index 4e4d53d17c751..c2a0c43a8cd33 100644 --- a/crates/sui-e2e-tests/tests/framework_upgrades/mock_sui_systems/deep_upgrade/sources/sui_system.move +++ b/crates/sui-e2e-tests/tests/framework_upgrades/mock_sui_systems/deep_upgrade/sources/sui_system.move @@ -2,6 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 module sui_system::sui_system { + use std::vector; + use sui::balance::Balance; use sui::object::UID; use sui::sui::SUI; @@ -72,6 +74,10 @@ module sui_system::sui_system { storage_rebate } + public fun active_validator_addresses(wrapper: &mut SuiSystemState): vector
{ + vector::empty() + } + fun load_system_state_mut(self: &mut SuiSystemState): &mut SuiSystemStateInnerV2 { load_inner_maybe_upgrade(self) } diff --git a/crates/sui-e2e-tests/tests/framework_upgrades/mock_sui_systems/safe_mode/sources/sui_system.move b/crates/sui-e2e-tests/tests/framework_upgrades/mock_sui_systems/safe_mode/sources/sui_system.move index 35eedd49ddd54..b0053e6a72d4a 100644 --- a/crates/sui-e2e-tests/tests/framework_upgrades/mock_sui_systems/safe_mode/sources/sui_system.move +++ b/crates/sui-e2e-tests/tests/framework_upgrades/mock_sui_systems/safe_mode/sources/sui_system.move @@ -2,6 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 module sui_system::sui_system { + use std::vector; + use sui::balance::Balance; use sui::object::UID; use sui::sui::SUI; @@ -67,6 +69,10 @@ module sui_system::sui_system { ) } + public fun active_validator_addresses(wrapper: &mut SuiSystemState): vector
{ + vector::empty() + } + fun load_system_state_mut(self: &mut SuiSystemState): &mut SuiSystemStateInner { let version = self.version; dynamic_field::borrow_mut(&mut self.id, version) diff --git a/crates/sui-e2e-tests/tests/framework_upgrades/mock_sui_systems/shallow_upgrade/sources/sui_system.move b/crates/sui-e2e-tests/tests/framework_upgrades/mock_sui_systems/shallow_upgrade/sources/sui_system.move index 6c8c8253b3aa4..0f3eb8825da03 100644 --- a/crates/sui-e2e-tests/tests/framework_upgrades/mock_sui_systems/shallow_upgrade/sources/sui_system.move +++ b/crates/sui-e2e-tests/tests/framework_upgrades/mock_sui_systems/shallow_upgrade/sources/sui_system.move @@ -2,6 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 module sui_system::sui_system { + use std::vector; + use sui::balance::Balance; use sui::object::UID; use sui::sui::SUI; @@ -72,6 +74,10 @@ module sui_system::sui_system { storage_rebate } + public fun active_validator_addresses(wrapper: &mut SuiSystemState): vector
{ + vector::empty() + } + fun load_system_state_mut(self: &mut SuiSystemState): &mut SuiSystemStateInnerV2 { load_inner_maybe_upgrade(self) } diff --git a/crates/sui-e2e-tests/tests/protocol_version_tests.rs b/crates/sui-e2e-tests/tests/protocol_version_tests.rs index 2ed51d985d0a6..fc15bd366ce90 100644 --- a/crates/sui-e2e-tests/tests/protocol_version_tests.rs +++ b/crates/sui-e2e-tests/tests/protocol_version_tests.rs @@ -67,6 +67,8 @@ mod sim_only_tests { use sui_macros::*; use sui_move_build::{BuildConfig, CompiledPackage}; use sui_protocol_config::SupportedProtocolVersions; + use sui_swarm_config::genesis_config::GenesisConfig; + use sui_swarm_config::network_config::NetworkConfig; use sui_types::base_types::ConciseableName; use sui_types::base_types::{ObjectID, ObjectRef}; use sui_types::effects::{TransactionEffects, TransactionEffectsAPI}; @@ -87,7 +89,8 @@ mod sim_only_tests { object::Object, programmable_transaction_builder::ProgrammableTransactionBuilder, transaction::TransactionKind, - MOVE_STDLIB_PACKAGE_ID, SUI_FRAMEWORK_PACKAGE_ID, SUI_SYSTEM_PACKAGE_ID, + MOVE_STDLIB_PACKAGE_ID, SUI_BRIDGE_OBJECT_ID, SUI_FRAMEWORK_PACKAGE_ID, + SUI_SYSTEM_PACKAGE_ID, }; use sui_types::{ SUI_AUTHENTICATOR_STATE_OBJECT_ID, SUI_CLOCK_OBJECT_ID, SUI_RANDOMNESS_STATE_OBJECT_ID, @@ -436,6 +439,7 @@ mod sim_only_tests { SUI_CLOCK_OBJECT_ID, SUI_AUTHENTICATOR_STATE_OBJECT_ID, SUI_RANDOMNESS_STATE_OBJECT_ID, + SUI_BRIDGE_OBJECT_ID, ] .contains(&obj.0); (!is_framework_obj).then_some(obj.0) @@ -787,6 +791,11 @@ mod sim_only_tests { #[sim_test] async fn test_safe_mode_recovery() { + let _guard = ProtocolConfig::apply_overrides_for_testing(|_, mut config| { + config.set_disable_bridge_for_testing(); + config + }); + override_sui_system_modules("mock_sui_systems/base"); let test_cluster = TestClusterBuilder::new() .with_epoch_duration_ms(20000) @@ -835,6 +844,11 @@ mod sim_only_tests { #[sim_test] async fn sui_system_mock_smoke_test() { + let _guard = ProtocolConfig::apply_overrides_for_testing(|_, mut config| { + config.set_disable_bridge_for_testing(); + config + }); + let test_cluster = TestClusterBuilder::new() .with_epoch_duration_ms(20000) .with_supported_protocol_versions(SupportedProtocolVersions::new_for_testing( @@ -849,6 +863,11 @@ mod sim_only_tests { #[sim_test] async fn sui_system_state_shallow_upgrade_test() { + let _guard = ProtocolConfig::apply_overrides_for_testing(|_, mut config| { + config.set_disable_bridge_for_testing(); + config + }); + override_sui_system_modules("mock_sui_systems/shallow_upgrade"); let test_cluster = TestClusterBuilder::new() @@ -881,6 +900,11 @@ mod sim_only_tests { #[sim_test] async fn sui_system_state_deep_upgrade_test() { + let _guard = ProtocolConfig::apply_overrides_for_testing(|_, mut config| { + config.set_disable_bridge_for_testing(); + config + }); + override_sui_system_modules("mock_sui_systems/deep_upgrade"); let test_cluster = TestClusterBuilder::new() diff --git a/crates/sui-framework-snapshot/src/lib.rs b/crates/sui-framework-snapshot/src/lib.rs index 808073492e356..cc25543a165bc 100644 --- a/crates/sui-framework-snapshot/src/lib.rs +++ b/crates/sui-framework-snapshot/src/lib.rs @@ -7,7 +7,8 @@ use std::{fs, io::Read, path::PathBuf}; use sui_framework::SystemPackage; use sui_types::base_types::ObjectID; use sui_types::{ - DEEPBOOK_PACKAGE_ID, MOVE_STDLIB_PACKAGE_ID, SUI_FRAMEWORK_PACKAGE_ID, SUI_SYSTEM_PACKAGE_ID, + BRIDGE_PACKAGE_ID, DEEPBOOK_PACKAGE_ID, MOVE_STDLIB_PACKAGE_ID, SUI_FRAMEWORK_PACKAGE_ID, + SUI_SYSTEM_PACKAGE_ID, }; pub type SnapshotManifest = BTreeMap; @@ -34,6 +35,7 @@ const SYSTEM_PACKAGE_PUBLISH_ORDER: &[ObjectID] = &[ SUI_FRAMEWORK_PACKAGE_ID, SUI_SYSTEM_PACKAGE_ID, DEEPBOOK_PACKAGE_ID, + BRIDGE_PACKAGE_ID, ]; pub fn load_bytecode_snapshot_manifest() -> SnapshotManifest { diff --git a/crates/sui-framework-tests/src/unit_tests.rs b/crates/sui-framework-tests/src/unit_tests.rs index d2616b5842bdd..905dc34ccccb9 100644 --- a/crates/sui-framework-tests/src/unit_tests.rs +++ b/crates/sui-framework-tests/src/unit_tests.rs @@ -39,6 +39,15 @@ fn run_deepbook_tests() { buf }); } +#[test] +#[cfg_attr(msim, ignore)] +fn run_bridge_tests() { + check_move_unit_tests({ + let mut buf = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + buf.extend(["..", "sui-framework", "packages", "bridge"]); + buf + }); +} #[test] #[cfg_attr(msim, ignore)] diff --git a/crates/sui-framework/build.rs b/crates/sui-framework/build.rs index 8059b4feb3d82..46642f80c6276 100644 --- a/crates/sui-framework/build.rs +++ b/crates/sui-framework/build.rs @@ -22,15 +22,18 @@ fn main() { let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); let packages_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("packages"); + let bridge_path = packages_path.join("bridge"); let deepbook_path = packages_path.join("deepbook"); let sui_system_path = packages_path.join("sui-system"); let sui_framework_path = packages_path.join("sui-framework"); + let bridge_path_clone = bridge_path.clone(); let deepbook_path_clone = deepbook_path.clone(); let sui_system_path_clone = sui_system_path.clone(); let sui_framework_path_clone = sui_framework_path.clone(); let move_stdlib_path = packages_path.join("move-stdlib"); build_packages( + bridge_path_clone, deepbook_path_clone, sui_system_path_clone, sui_framework_path_clone, @@ -46,6 +49,14 @@ fn main() { "cargo:rerun-if-changed={}", deepbook_path.join("sources").display() ); + println!( + "cargo:rerun-if-changed={}", + bridge_path.join("Move.toml").display() + ); + println!( + "cargo:rerun-if-changed={}", + bridge_path.join("sources").display() + ); println!( "cargo:rerun-if-changed={}", sui_system_path.join("Move.toml").display() @@ -73,6 +84,7 @@ fn main() { } fn build_packages( + bridge_path: PathBuf, deepbook_path: PathBuf, sui_system_path: PathBuf, sui_framework_path: PathBuf, @@ -88,10 +100,12 @@ fn build_packages( }; debug_assert!(!config.test_mode); build_packages_with_move_config( + bridge_path.clone(), deepbook_path.clone(), sui_system_path.clone(), sui_framework_path.clone(), out_dir.clone(), + "bridge", "deepbook", "sui-system", "sui-framework", @@ -109,10 +123,12 @@ fn build_packages( ..Default::default() }; build_packages_with_move_config( + bridge_path, deepbook_path, sui_system_path, sui_framework_path, out_dir, + "bridge-test", "deepbook-test", "sui-system-test", "sui-framework-test", @@ -123,10 +139,12 @@ fn build_packages( } fn build_packages_with_move_config( + bridge_path: PathBuf, deepbook_path: PathBuf, sui_system_path: PathBuf, sui_framework_path: PathBuf, out_dir: PathBuf, + bridge_dir: &str, deepbook_dir: &str, system_dir: &str, framework_dir: &str, @@ -149,16 +167,24 @@ fn build_packages_with_move_config( .build(sui_system_path) .unwrap(); let deepbook_pkg = BuildConfig { - config, + config: config.clone(), run_bytecode_verifier: true, print_diags_to_stderr: false, } .build(deepbook_path) .unwrap(); + let bridge_pkg = BuildConfig { + config, + run_bytecode_verifier: true, + print_diags_to_stderr: false, + } + .build(bridge_path) + .unwrap(); let sui_system = system_pkg.get_sui_system_modules(); let sui_framework = framework_pkg.get_sui_framework_modules(); let deepbook = deepbook_pkg.get_deepbook_modules(); + let bridge = bridge_pkg.get_bridge_modules(); let move_stdlib = framework_pkg.get_stdlib_modules(); let sui_system_members = @@ -167,6 +193,7 @@ fn build_packages_with_move_config( serialize_modules_to_file(sui_framework, &out_dir.join(framework_dir)).unwrap(); let deepbook_members = serialize_modules_to_file(deepbook, &out_dir.join(deepbook_dir)).unwrap(); + let bridge_members = serialize_modules_to_file(bridge, &out_dir.join(bridge_dir)).unwrap(); let stdlib_members = serialize_modules_to_file(move_stdlib, &out_dir.join(stdlib_dir)).unwrap(); // write out generated docs @@ -192,6 +219,11 @@ fn build_packages_with_move_config( &framework_pkg.package.compiled_docs.unwrap(), &mut files_to_write, ); + relocate_docs( + bridge_dir, + &bridge_pkg.package.compiled_docs.unwrap(), + &mut files_to_write, + ); for (fname, doc) in files_to_write { let mut dst_path = PathBuf::from(DOCS_DIR); dst_path.push(fname); @@ -203,6 +235,7 @@ fn build_packages_with_move_config( sui_system_members.join("\n"), sui_framework_members.join("\n"), deepbook_members.join("\n"), + bridge_members.join("\n"), stdlib_members.join("\n"), ] .join("\n"); diff --git a/crates/sui-framework/docs/bridge/bridge.md b/crates/sui-framework/docs/bridge/bridge.md new file mode 100644 index 0000000000000..2e889352e7eb4 --- /dev/null +++ b/crates/sui-framework/docs/bridge/bridge.md @@ -0,0 +1,1490 @@ +--- +title: Module `0xb::bridge` +--- + + + +- [Resource `Bridge`](#0xb_bridge_Bridge) +- [Struct `BridgeInner`](#0xb_bridge_BridgeInner) +- [Struct `TokenDepositedEvent`](#0xb_bridge_TokenDepositedEvent) +- [Struct `EmergencyOpEvent`](#0xb_bridge_EmergencyOpEvent) +- [Struct `BridgeRecord`](#0xb_bridge_BridgeRecord) +- [Struct `TokenTransferApproved`](#0xb_bridge_TokenTransferApproved) +- [Struct `TokenTransferClaimed`](#0xb_bridge_TokenTransferClaimed) +- [Struct `TokenTransferAlreadyApproved`](#0xb_bridge_TokenTransferAlreadyApproved) +- [Struct `TokenTransferAlreadyClaimed`](#0xb_bridge_TokenTransferAlreadyClaimed) +- [Struct `TokenTransferLimitExceed`](#0xb_bridge_TokenTransferLimitExceed) +- [Constants](#@Constants_0) +- [Function `create`](#0xb_bridge_create) +- [Function `init_bridge_committee`](#0xb_bridge_init_bridge_committee) +- [Function `committee_registration`](#0xb_bridge_committee_registration) +- [Function `register_foreign_token`](#0xb_bridge_register_foreign_token) +- [Function `send_token`](#0xb_bridge_send_token) +- [Function `approve_token_transfer`](#0xb_bridge_approve_token_transfer) +- [Function `claim_token`](#0xb_bridge_claim_token) +- [Function `claim_and_transfer_token`](#0xb_bridge_claim_and_transfer_token) +- [Function `execute_system_message`](#0xb_bridge_execute_system_message) +- [Function `get_token_transfer_action_status`](#0xb_bridge_get_token_transfer_action_status) +- [Function `load_inner`](#0xb_bridge_load_inner) +- [Function `load_inner_mut`](#0xb_bridge_load_inner_mut) +- [Function `claim_token_internal`](#0xb_bridge_claim_token_internal) +- [Function `execute_emergency_op`](#0xb_bridge_execute_emergency_op) +- [Function `execute_update_bridge_limit`](#0xb_bridge_execute_update_bridge_limit) +- [Function `execute_update_asset_price`](#0xb_bridge_execute_update_asset_price) +- [Function `execute_add_tokens_on_sui`](#0xb_bridge_execute_add_tokens_on_sui) +- [Function `get_current_seq_num_and_increment`](#0xb_bridge_get_current_seq_num_and_increment) +- [Function `get_token_transfer_action_signatures`](#0xb_bridge_get_token_transfer_action_signatures) + + +
use 0x1::ascii;
+use 0x1::option;
+use 0x2::address;
+use 0x2::balance;
+use 0x2::clock;
+use 0x2::coin;
+use 0x2::event;
+use 0x2::linked_table;
+use 0x2::object;
+use 0x2::package;
+use 0x2::transfer;
+use 0x2::tx_context;
+use 0x2::vec_map;
+use 0x2::versioned;
+use 0x3::sui_system;
+use 0xb::chain_ids;
+use 0xb::committee;
+use 0xb::limiter;
+use 0xb::message;
+use 0xb::message_types;
+use 0xb::treasury;
+
+ + + + + +## Resource `Bridge` + + + +
struct Bridge has key
+
+ + + +
+Fields + + +
+
+id: object::UID +
+
+ +
+
+inner: versioned::Versioned +
+
+ +
+
+ + +
+ + + +## Struct `BridgeInner` + + + +
struct BridgeInner has store
+
+ + + +
+Fields + + +
+
+bridge_version: u64 +
+
+ +
+
+message_version: u8 +
+
+ +
+
+chain_id: u8 +
+
+ +
+
+sequence_nums: vec_map::VecMap<u8, u64> +
+
+ +
+
+committee: committee::BridgeCommittee +
+
+ +
+
+treasury: treasury::BridgeTreasury +
+
+ +
+
+token_transfer_records: linked_table::LinkedTable<message::BridgeMessageKey, bridge::BridgeRecord> +
+
+ +
+
+limiter: limiter::TransferLimiter +
+
+ +
+
+paused: bool +
+
+ +
+
+ + +
+ + + +## Struct `TokenDepositedEvent` + + + +
struct TokenDepositedEvent has copy, drop
+
+ + + +
+Fields + + +
+
+seq_num: u64 +
+
+ +
+
+source_chain: u8 +
+
+ +
+
+sender_address: vector<u8> +
+
+ +
+
+target_chain: u8 +
+
+ +
+
+target_address: vector<u8> +
+
+ +
+
+token_type: u8 +
+
+ +
+
+amount: u64 +
+
+ +
+
+ + +
+ + + +## Struct `EmergencyOpEvent` + + + +
struct EmergencyOpEvent has copy, drop
+
+ + + +
+Fields + + +
+
+frozen: bool +
+
+ +
+
+ + +
+ + + +## Struct `BridgeRecord` + + + +
struct BridgeRecord has drop, store
+
+ + + +
+Fields + + +
+
+message: message::BridgeMessage +
+
+ +
+
+verified_signatures: option::Option<vector<vector<u8>>> +
+
+ +
+
+claimed: bool +
+
+ +
+
+ + +
+ + + +## Struct `TokenTransferApproved` + + + +
struct TokenTransferApproved has copy, drop
+
+ + + +
+Fields + + +
+
+message_key: message::BridgeMessageKey +
+
+ +
+
+ + +
+ + + +## Struct `TokenTransferClaimed` + + + +
struct TokenTransferClaimed has copy, drop
+
+ + + +
+Fields + + +
+
+message_key: message::BridgeMessageKey +
+
+ +
+
+ + +
+ + + +## Struct `TokenTransferAlreadyApproved` + + + +
struct TokenTransferAlreadyApproved has copy, drop
+
+ + + +
+Fields + + +
+
+message_key: message::BridgeMessageKey +
+
+ +
+
+ + +
+ + + +## Struct `TokenTransferAlreadyClaimed` + + + +
struct TokenTransferAlreadyClaimed has copy, drop
+
+ + + +
+Fields + + +
+
+message_key: message::BridgeMessageKey +
+
+ +
+
+ + +
+ + + +## Struct `TokenTransferLimitExceed` + + + +
struct TokenTransferLimitExceed has copy, drop
+
+ + + +
+Fields + + +
+
+message_key: message::BridgeMessageKey +
+
+ +
+
+ + +
+ + + +## Constants + + + + + + +
const ENotSystemAddress: u64 = 5;
+
+ + + + + + + +
const EWrongInnerVersion: u64 = 7;
+
+ + + + + + + +
const CURRENT_VERSION: u64 = 1;
+
+ + + + + + + +
const EInvalidBridgeRoute: u64 = 16;
+
+ + + + + + + +
const EBridgeAlreadyPaused: u64 = 13;
+
+ + + + + + + +
const EBridgeNotPaused: u64 = 14;
+
+ + + + + + + +
const EBridgeUnavailable: u64 = 8;
+
+ + + + + + + +
const EInvariantSuiInitializedTokenTransferShouldNotBeClaimed: u64 = 10;
+
+ + + + + + + +
const EMalformedMessageError: u64 = 2;
+
+ + + + + + + +
const EMessageNotFoundInRecords: u64 = 11;
+
+ + + + + + + +
const EMustBeTokenMessage: u64 = 17;
+
+ + + + + + + +
const ETokenAlreadyClaimed: u64 = 15;
+
+ + + + + + + +
const EUnauthorisedClaim: u64 = 1;
+
+ + + + + + + +
const EUnexpectedChainID: u64 = 4;
+
+ + + + + + + +
const EUnexpectedMessageType: u64 = 0;
+
+ + + + + + + +
const EUnexpectedMessageVersion: u64 = 12;
+
+ + + + + + + +
const EUnexpectedOperation: u64 = 9;
+
+ + + + + + + +
const EUnexpectedSeqNum: u64 = 6;
+
+ + + + + + + +
const EUnexpectedTokenType: u64 = 3;
+
+ + + + + + + +
const MESSAGE_VERSION: u8 = 1;
+
+ + + + + + + +
const TRANSFER_STATUS_APPROVED: u8 = 1;
+
+ + + + + + + +
const TRANSFER_STATUS_CLAIMED: u8 = 2;
+
+ + + + + + + +
const TRANSFER_STATUS_NOT_FOUND: u8 = 3;
+
+ + + + + + + +
const TRANSFER_STATUS_PENDING: u8 = 0;
+
+ + + + + +## Function `create` + + + +
fun create(id: object::UID, chain_id: u8, ctx: &mut tx_context::TxContext)
+
+ + + +
+Implementation + + +
fun create(id: UID, chain_id: u8, ctx: &mut TxContext) {
+    assert!(ctx.sender() == @0x0, ENotSystemAddress);
+    let bridge_inner = BridgeInner {
+        bridge_version: CURRENT_VERSION,
+        message_version: MESSAGE_VERSION,
+        chain_id,
+        sequence_nums: vec_map::empty(),
+        committee: committee::create(ctx),
+        treasury: treasury::create(ctx),
+        token_transfer_records: linked_table::new(ctx),
+        limiter: limiter::new(),
+        paused: false,
+    };
+    let bridge = Bridge {
+        id,
+        inner: versioned::create(CURRENT_VERSION, bridge_inner, ctx),
+    };
+    transfer::share_object(bridge);
+}
+
+ + + +
+ + + +## Function `init_bridge_committee` + + + +
fun init_bridge_committee(bridge: &mut bridge::Bridge, active_validator_voting_power: vec_map::VecMap<address, u64>, min_stake_participation_percentage: u64, ctx: &tx_context::TxContext)
+
+ + + +
+Implementation + + +
fun init_bridge_committee(
+    bridge: &mut Bridge,
+    active_validator_voting_power: VecMap<address, u64>,
+    min_stake_participation_percentage: u64,
+    ctx: &TxContext
+) {
+    assert!(ctx.sender() == @0x0, ENotSystemAddress);
+    let inner = load_inner_mut(bridge);
+    if (inner.committee.committee_members().is_empty()) {
+        inner.committee.try_create_next_committee(
+            active_validator_voting_power,
+            min_stake_participation_percentage,
+            ctx,
+        )
+    }
+}
+
+ + + +
+ + + +## Function `committee_registration` + + + +
public fun committee_registration(bridge: &mut bridge::Bridge, system_state: &mut sui_system::SuiSystemState, bridge_pubkey_bytes: vector<u8>, http_rest_url: vector<u8>, ctx: &tx_context::TxContext)
+
+ + + +
+Implementation + + +
public fun committee_registration(
+    bridge: &mut Bridge,
+    system_state: &mut SuiSystemState,
+    bridge_pubkey_bytes: vector<u8>,
+    http_rest_url: vector<u8>,
+    ctx: &TxContext
+) {
+    load_inner_mut(bridge)
+        .committee
+        .register(system_state, bridge_pubkey_bytes, http_rest_url, ctx);
+}
+
+ + + +
+ + + +## Function `register_foreign_token` + + + +
public fun register_foreign_token<T>(bridge: &mut bridge::Bridge, tc: coin::TreasuryCap<T>, uc: package::UpgradeCap, metadata: &coin::CoinMetadata<T>)
+
+ + + +
+Implementation + + +
public fun register_foreign_token<T>(
+    bridge: &mut Bridge,
+    tc: TreasuryCap<T>,
+    uc: UpgradeCap,
+    metadata: &CoinMetadata<T>,
+) {
+    load_inner_mut(bridge)
+        .treasury
+        .register_foreign_token<T>(tc, uc, metadata)
+}
+
+ + + +
+ + + +## Function `send_token` + + + +
public fun send_token<T>(bridge: &mut bridge::Bridge, target_chain: u8, target_address: vector<u8>, token: coin::Coin<T>, ctx: &mut tx_context::TxContext)
+
+ + + +
+Implementation + + +
public fun send_token<T>(
+    bridge: &mut Bridge,
+    target_chain: u8,
+    target_address: vector<u8>,
+    token: Coin<T>,
+    ctx: &mut TxContext
+) {
+    let inner = load_inner_mut(bridge);
+    assert!(!inner.paused, EBridgeUnavailable);
+    assert!(chain_ids::is_valid_route(inner.chain_id, target_chain), EInvalidBridgeRoute);
+    let amount = token.balance().value();
+
+    let bridge_seq_num = inner.get_current_seq_num_and_increment(message_types::token());
+    let token_id = inner.treasury.token_id<T>();
+    let token_amount = token.balance().value();
+
+    // create bridge message
+    let message = message::create_token_bridge_message(
+        inner.chain_id,
+        bridge_seq_num,
+        address::to_bytes(ctx.sender()),
+        target_chain,
+        target_address,
+        token_id,
+        amount,
+    );
+
+    // burn / escrow token, unsupported coins will fail in this step
+    inner.treasury.burn(token);
+
+    // Store pending bridge request
+    inner.token_transfer_records.push_back(
+        message.key(),
+        BridgeRecord {
+            message,
+            verified_signatures: option::none(),
+            claimed: false,
+        },
+    );
+
+    // emit event
+    emit(
+        TokenDepositedEvent {
+            seq_num: bridge_seq_num,
+            source_chain: inner.chain_id,
+            sender_address: address::to_bytes(ctx.sender()),
+            target_chain,
+            target_address,
+            token_type: token_id,
+            amount: token_amount,
+        },
+    );
+}
+
+ + + +
+ + + +## Function `approve_token_transfer` + + + +
public fun approve_token_transfer(bridge: &mut bridge::Bridge, message: message::BridgeMessage, signatures: vector<vector<u8>>)
+
+ + + +
+Implementation + + +
public fun approve_token_transfer(
+    bridge: &mut Bridge,
+    message: BridgeMessage,
+    signatures: vector<vector<u8>>,
+) {
+    let inner = load_inner_mut(bridge);
+    assert!(!inner.paused, EBridgeUnavailable);
+    // verify signatures
+    inner.committee.verify_signatures(message, signatures);
+
+    assert!(message.message_type() == message_types::token(), EMustBeTokenMessage);
+    assert!(message.message_version() == MESSAGE_VERSION, EUnexpectedMessageVersion);
+    let token_payload = message.extract_token_bridge_payload();
+    let target_chain = token_payload.token_target_chain();
+    assert!(
+        message.source_chain() == inner.chain_id || target_chain == inner.chain_id,
+        EUnexpectedChainID,
+    );
+
+    let message_key = message.key();
+    // retrieve pending message if source chain is Sui, the initial message
+    // must exist on chain
+    if (message.source_chain() == inner.chain_id) {
+        let record = &mut inner.token_transfer_records[message_key];
+
+        assert!(record.message == message, EMalformedMessageError);
+        assert!(!record.claimed, EInvariantSuiInitializedTokenTransferShouldNotBeClaimed);
+
+        // If record already has verified signatures, it means the message has been approved
+        // Then we exit early.
+        if (record.verified_signatures.is_some()) {
+            emit(TokenTransferAlreadyApproved { message_key });
+            return
+        };
+        // Store approval
+        record.verified_signatures = option::some(signatures)
+    } else {
+        // At this point, if this message is in token_transfer_records, we know
+        // it's already approved because we only add a message to token_transfer_records
+        // after verifying the signatures
+        if (inner.token_transfer_records.contains(message_key)) {
+            emit(TokenTransferAlreadyApproved { message_key });
+            return
+        };
+        // Store message and approval
+        inner.token_transfer_records.push_back(
+            message_key,
+            BridgeRecord {
+                message,
+                verified_signatures: option::some(signatures),
+                claimed: false
+            },
+        );
+    };
+
+    emit(TokenTransferApproved { message_key });
+}
+
+ + + +
+ + + +## Function `claim_token` + + + +
public fun claim_token<T>(bridge: &mut bridge::Bridge, clock: &clock::Clock, source_chain: u8, bridge_seq_num: u64, ctx: &mut tx_context::TxContext): coin::Coin<T>
+
+ + + +
+Implementation + + +
public fun claim_token<T>(
+    bridge: &mut Bridge,
+    clock: &Clock,
+    source_chain: u8,
+    bridge_seq_num: u64,
+    ctx: &mut TxContext,
+): Coin<T> {
+    let (maybe_token, owner) = bridge.claim_token_internal<T>(
+        clock,
+        source_chain,
+        bridge_seq_num,
+        ctx,
+    );
+    // Only token owner can claim the token
+    assert!(ctx.sender() == owner, EUnauthorisedClaim);
+    assert!(maybe_token.is_some(), ETokenAlreadyClaimed);
+    maybe_token.destroy_some()
+}
+
+ + + +
+ + + +## Function `claim_and_transfer_token` + + + +
public fun claim_and_transfer_token<T>(bridge: &mut bridge::Bridge, clock: &clock::Clock, source_chain: u8, bridge_seq_num: u64, ctx: &mut tx_context::TxContext)
+
+ + + +
+Implementation + + +
public fun claim_and_transfer_token<T>(
+    bridge: &mut Bridge,
+    clock: &Clock,
+    source_chain: u8,
+    bridge_seq_num: u64,
+    ctx: &mut TxContext,
+) {
+    let (token, owner) = bridge.claim_token_internal<T>(clock, source_chain, bridge_seq_num, ctx);
+    if (token.is_some()) {
+        transfer::public_transfer(token.destroy_some(), owner)
+    } else {
+        token.destroy_none();
+    };
+}
+
+ + + +
+ + + +## Function `execute_system_message` + + + +
public fun execute_system_message(bridge: &mut bridge::Bridge, message: message::BridgeMessage, signatures: vector<vector<u8>>)
+
+ + + +
+Implementation + + +
public fun execute_system_message(
+    bridge: &mut Bridge,
+    message: BridgeMessage,
+    signatures: vector<vector<u8>>,
+) {
+    let message_type = message.message_type();
+
+    // TODO: test version mismatch
+    assert!(message.message_version() == MESSAGE_VERSION, EUnexpectedMessageVersion);
+    let inner = load_inner_mut(bridge);
+
+    assert!(message.source_chain() == inner.chain_id, EUnexpectedChainID);
+
+    // check system ops seq number and increment it
+    let expected_seq_num = inner.get_current_seq_num_and_increment(message_type);
+    assert!(message.seq_num() == expected_seq_num, EUnexpectedSeqNum);
+
+    inner.committee.verify_signatures(message, signatures);
+
+    if (message_type == message_types::emergency_op()) {
+        let payload = message.extract_emergency_op_payload();
+        inner.execute_emergency_op(payload);
+    } else if (message_type == message_types::committee_blocklist()) {
+        let payload = message.extract_blocklist_payload();
+        inner.committee.execute_blocklist(payload);
+    } else if (message_type == message_types::update_bridge_limit()) {
+        let payload = message.extract_update_bridge_limit();
+        inner.execute_update_bridge_limit(payload);
+    } else if (message_type == message_types::update_asset_price()) {
+        let payload = message.extract_update_asset_price();
+        inner.execute_update_asset_price(payload);
+    } else if (message_type == message_types::add_tokens_on_sui()) {
+        let payload = message.extract_add_tokens_on_sui();
+        inner.execute_add_tokens_on_sui(payload);
+    } else {
+        abort EUnexpectedMessageType
+    };
+}
+
+ + + +
+ + + +## Function `get_token_transfer_action_status` + + + +
public fun get_token_transfer_action_status(bridge: &bridge::Bridge, source_chain: u8, bridge_seq_num: u64): u8
+
+ + + +
+Implementation + + +
public fun get_token_transfer_action_status(
+    bridge: &Bridge,
+    source_chain: u8,
+    bridge_seq_num: u64,
+): u8 {
+    let inner = load_inner(bridge);
+    let key = message::create_key(
+        source_chain,
+        message_types::token(),
+        bridge_seq_num
+    );
+
+    if (!inner.token_transfer_records.contains(key)) {
+        return TRANSFER_STATUS_NOT_FOUND
+    };
+
+    let record = &inner.token_transfer_records[key];
+    if (record.claimed) {
+        return TRANSFER_STATUS_CLAIMED
+    };
+
+    if (record.verified_signatures.is_some()) {
+        return TRANSFER_STATUS_APPROVED
+    };
+
+    TRANSFER_STATUS_PENDING
+}
+
+ + + +
+ + + +## Function `load_inner` + + + +
fun load_inner(bridge: &bridge::Bridge): &bridge::BridgeInner
+
+ + + +
+Implementation + + +
fun load_inner(
+    bridge: &Bridge,
+): &BridgeInner {
+    let version = bridge.inner.version();
+
+    // TODO: Replace this with a lazy update function when we add a new version of the inner object.
+    assert!(version == CURRENT_VERSION, EWrongInnerVersion);
+    let inner: &BridgeInner = bridge.inner.load_value();
+    assert!(inner.bridge_version == version, EWrongInnerVersion);
+    inner
+}
+
+ + + +
+ + + +## Function `load_inner_mut` + + + +
fun load_inner_mut(bridge: &mut bridge::Bridge): &mut bridge::BridgeInner
+
+ + + +
+Implementation + + +
fun load_inner_mut(bridge: &mut Bridge): &mut BridgeInner {
+    let version = bridge.inner.version();
+    // TODO: Replace this with a lazy update function when we add a new version of the inner object.
+    assert!(version == CURRENT_VERSION, EWrongInnerVersion);
+    let inner: &mut BridgeInner = bridge.inner.load_value_mut();
+    assert!(inner.bridge_version == version, EWrongInnerVersion);
+    inner
+}
+
+ + + +
+ + + +## Function `claim_token_internal` + + + +
fun claim_token_internal<T>(bridge: &mut bridge::Bridge, clock: &clock::Clock, source_chain: u8, bridge_seq_num: u64, ctx: &mut tx_context::TxContext): (option::Option<coin::Coin<T>>, address)
+
+ + + +
+Implementation + + +
fun claim_token_internal<T>(
+    bridge: &mut Bridge,
+    clock: &Clock,
+    source_chain: u8,
+    bridge_seq_num: u64,
+    ctx: &mut TxContext,
+): (Option<Coin<T>>, address) {
+    let inner = load_inner_mut(bridge);
+    assert!(!inner.paused, EBridgeUnavailable);
+
+    let key = message::create_key(source_chain, message_types::token(), bridge_seq_num);
+    assert!(inner.token_transfer_records.contains(key), EMessageNotFoundInRecords);
+
+    // retrieve approved bridge message
+    let record = &mut inner.token_transfer_records[key];
+    // ensure this is a token bridge message
+    assert!(
+        &record.message.message_type() == message_types::token(),
+        EUnexpectedMessageType,
+    );
+    // Ensure it's signed
+    assert!(record.verified_signatures.is_some(), EUnauthorisedClaim);
+
+    // extract token message
+    let token_payload = record.message.extract_token_bridge_payload();
+    // get owner address
+    let owner = address::from_bytes(token_payload.token_target_address());
+
+    // If already claimed, exit early
+    if (record.claimed) {
+        emit(TokenTransferAlreadyClaimed { message_key: key });
+        return (option::none(), owner)
+    };
+
+    let target_chain = token_payload.token_target_chain();
+    // ensure target chain matches bridge.chain_id
+    assert!(target_chain == inner.chain_id, EUnexpectedChainID);
+
+    // TODO: why do we check validity of the route here? what if inconsistency?
+    // Ensure route is valid
+    // TODO: add unit tests
+    // `get_route` abort if route is invalid
+    let route = chain_ids::get_route(source_chain, target_chain);
+    // check token type
+    assert!(
+        treasury::token_id<T>(&inner.treasury) == token_payload.token_type(),
+        EUnexpectedTokenType,
+    );
+
+    let amount = token_payload.token_amount();
+    // Make sure transfer is within limit.
+    if (!inner
+            .limiter
+            .check_and_record_sending_transfer<T>(
+                &inner.treasury,
+                clock,
+                route,
+                amount,
+            )
+    ) {
+        emit(TokenTransferLimitExceed { message_key: key });
+        return (option::none(), owner)
+    };
+
+    // claim from treasury
+    let token = inner.treasury.mint<T>(amount, ctx);
+
+    // Record changes
+    record.claimed = true;
+    emit(TokenTransferClaimed { message_key: key });
+
+    (option::some(token), owner)
+}
+
+ + + +
+ + + +## Function `execute_emergency_op` + + + +
fun execute_emergency_op(inner: &mut bridge::BridgeInner, payload: message::EmergencyOp)
+
+ + + +
+Implementation + + +
fun execute_emergency_op(inner: &mut BridgeInner, payload: EmergencyOp) {
+    let op = payload.emergency_op_type();
+    if (op == message::emergency_op_pause()) {
+        assert!(!inner.paused, EBridgeAlreadyPaused);
+        inner.paused = true;
+        emit(EmergencyOpEvent { frozen: true });
+    } else if (op == message::emergency_op_unpause()) {
+        assert!(inner.paused, EBridgeNotPaused);
+        inner.paused = false;
+        emit(EmergencyOpEvent { frozen: false });
+    } else {
+        abort EUnexpectedOperation
+    };
+}
+
+ + + +
+ + + +## Function `execute_update_bridge_limit` + + + +
fun execute_update_bridge_limit(inner: &mut bridge::BridgeInner, payload: message::UpdateBridgeLimit)
+
+ + + +
+Implementation + + +
fun execute_update_bridge_limit(inner: &mut BridgeInner, payload: UpdateBridgeLimit) {
+    let receiving_chain = payload.update_bridge_limit_payload_receiving_chain();
+    assert!(receiving_chain == inner.chain_id, EUnexpectedChainID);
+    let route = chain_ids::get_route(
+        payload.update_bridge_limit_payload_sending_chain(),
+        receiving_chain
+    );
+
+    inner.limiter.update_route_limit(
+        &route,
+        payload.update_bridge_limit_payload_limit()
+    )
+}
+
+ + + +
+ + + +## Function `execute_update_asset_price` + + + +
fun execute_update_asset_price(inner: &mut bridge::BridgeInner, payload: message::UpdateAssetPrice)
+
+ + + +
+Implementation + + +
fun execute_update_asset_price(inner: &mut BridgeInner, payload: UpdateAssetPrice) {
+    inner.treasury.update_asset_notional_price(
+        payload.update_asset_price_payload_token_id(),
+        payload.update_asset_price_payload_new_price()
+    )
+}
+
+ + + +
+ + + +## Function `execute_add_tokens_on_sui` + + + +
fun execute_add_tokens_on_sui(inner: &mut bridge::BridgeInner, payload: message::AddTokenOnSui)
+
+ + + +
+Implementation + + +
fun execute_add_tokens_on_sui(inner: &mut BridgeInner, payload: AddTokenOnSui) {
+    // FIXME: assert native_token to be false and add test
+    let native_token = payload.is_native();
+    let mut token_ids = payload.token_ids();
+    let mut token_type_names = payload.token_type_names();
+    let mut token_prices = payload.token_prices();
+
+    // Make sure token data is consistent
+    assert!(token_ids.length() == token_type_names.length(), EMalformedMessageError);
+    assert!(token_ids.length() == token_prices.length(), EMalformedMessageError);
+
+    while (vector::length(&token_ids) > 0) {
+        let token_id = token_ids.pop_back();
+        let token_type_name = token_type_names.pop_back();
+        let token_price = token_prices.pop_back();
+        inner.treasury.add_new_token(token_type_name, token_id, native_token, token_price)
+    }
+}
+
+ + + +
+ + + +## Function `get_current_seq_num_and_increment` + + + +
fun get_current_seq_num_and_increment(bridge: &mut bridge::BridgeInner, msg_type: u8): u64
+
+ + + +
+Implementation + + +
fun get_current_seq_num_and_increment(bridge: &mut BridgeInner, msg_type: u8): u64 {
+    if (!bridge.sequence_nums.contains(&msg_type)) {
+        bridge.sequence_nums.insert(msg_type, 1);
+        return 0
+    };
+
+    let entry = &mut bridge.sequence_nums[&msg_type];
+    let seq_num = *entry;
+    *entry = seq_num + 1;
+    seq_num
+}
+
+ + + +
+ + + +## Function `get_token_transfer_action_signatures` + + + +
fun get_token_transfer_action_signatures(bridge: &bridge::Bridge, source_chain: u8, bridge_seq_num: u64): option::Option<vector<vector<u8>>>
+
+ + + +
+Implementation + + +
fun get_token_transfer_action_signatures(
+    bridge: &Bridge,
+    source_chain: u8,
+    bridge_seq_num: u64,
+): Option<vector<vector<u8>>> {
+    let inner = load_inner(bridge);
+    let key = message::create_key(
+        source_chain,
+        message_types::token(),
+        bridge_seq_num
+    );
+
+    if (!inner.token_transfer_records.contains(key)) {
+        return option::none()
+    };
+
+    let record = &inner.token_transfer_records[key];
+    record.verified_signatures
+}
+
+ + + +
diff --git a/crates/sui-framework/docs/bridge/chain_ids.md b/crates/sui-framework/docs/bridge/chain_ids.md new file mode 100644 index 0000000000000..d7a34ffcce0c8 --- /dev/null +++ b/crates/sui-framework/docs/bridge/chain_ids.md @@ -0,0 +1,426 @@ +--- +title: Module `0xb::chain_ids` +--- + + + +- [Struct `BridgeRoute`](#0xb_chain_ids_BridgeRoute) +- [Constants](#@Constants_0) +- [Function `sui_mainnet`](#0xb_chain_ids_sui_mainnet) +- [Function `sui_testnet`](#0xb_chain_ids_sui_testnet) +- [Function `sui_custom`](#0xb_chain_ids_sui_custom) +- [Function `eth_mainnet`](#0xb_chain_ids_eth_mainnet) +- [Function `eth_sepolia`](#0xb_chain_ids_eth_sepolia) +- [Function `eth_custom`](#0xb_chain_ids_eth_custom) +- [Function `route_source`](#0xb_chain_ids_route_source) +- [Function `route_destination`](#0xb_chain_ids_route_destination) +- [Function `assert_valid_chain_id`](#0xb_chain_ids_assert_valid_chain_id) +- [Function `valid_routes`](#0xb_chain_ids_valid_routes) +- [Function `is_valid_route`](#0xb_chain_ids_is_valid_route) +- [Function `get_route`](#0xb_chain_ids_get_route) + + +
use 0x1::vector;
+
+ + + + + +## Struct `BridgeRoute` + + + +
struct BridgeRoute has copy, drop, store
+
+ + + +
+Fields + + +
+
+source: u8 +
+
+ +
+
+destination: u8 +
+
+ +
+
+ + +
+ + + +## Constants + + + + + + +
const EInvalidBridgeRoute: u64 = 0;
+
+ + + + + + + +
const EthCustom: u8 = 12;
+
+ + + + + + + +
const EthMainnet: u8 = 10;
+
+ + + + + + + +
const EthSepolia: u8 = 11;
+
+ + + + + + + +
const SuiCustom: u8 = 2;
+
+ + + + + + + +
const SuiMainnet: u8 = 0;
+
+ + + + + + + +
const SuiTestnet: u8 = 1;
+
+ + + + + +## Function `sui_mainnet` + + + +
public fun sui_mainnet(): u8
+
+ + + +
+Implementation + + +
public fun sui_mainnet(): u8 { SuiMainnet }
+
+ + + +
+ + + +## Function `sui_testnet` + + + +
public fun sui_testnet(): u8
+
+ + + +
+Implementation + + +
public fun sui_testnet(): u8 { SuiTestnet }
+
+ + + +
+ + + +## Function `sui_custom` + + + +
public fun sui_custom(): u8
+
+ + + +
+Implementation + + +
public fun sui_custom(): u8 { SuiCustom }
+
+ + + +
+ + + +## Function `eth_mainnet` + + + +
public fun eth_mainnet(): u8
+
+ + + +
+Implementation + + +
public fun eth_mainnet(): u8 { EthMainnet }
+
+ + + +
+ + + +## Function `eth_sepolia` + + + +
public fun eth_sepolia(): u8
+
+ + + +
+Implementation + + +
public fun eth_sepolia(): u8 { EthSepolia }
+
+ + + +
+ + + +## Function `eth_custom` + + + +
public fun eth_custom(): u8
+
+ + + +
+Implementation + + +
public fun eth_custom(): u8 { EthCustom }
+
+ + + +
+ + + +## Function `route_source` + + + +
public fun route_source(route: &chain_ids::BridgeRoute): &u8
+
+ + + +
+Implementation + + +
public fun route_source(route: &BridgeRoute): &u8 {
+    &route.source
+}
+
+ + + +
+ + + +## Function `route_destination` + + + +
public fun route_destination(route: &chain_ids::BridgeRoute): &u8
+
+ + + +
+Implementation + + +
public fun route_destination(route: &BridgeRoute): &u8 {
+    &route.destination
+}
+
+ + + +
+ + + +## Function `assert_valid_chain_id` + + + +
public fun assert_valid_chain_id(id: u8)
+
+ + + +
+Implementation + + +
public fun assert_valid_chain_id(id: u8) {
+    assert!(
+        id == SuiMainnet ||
+        id == SuiTestnet ||
+        id == SuiCustom ||
+        id == EthMainnet ||
+        id == EthSepolia ||
+        id == EthCustom,
+        EInvalidBridgeRoute
+    )
+}
+
+ + + +
+ + + +## Function `valid_routes` + + + +
public fun valid_routes(): vector<chain_ids::BridgeRoute>
+
+ + + +
+Implementation + + +
public fun valid_routes(): vector<BridgeRoute> {
+    vector[
+        BridgeRoute { source: SuiMainnet, destination: EthMainnet },
+        BridgeRoute { source: EthMainnet, destination: SuiMainnet },
+
+        BridgeRoute { source: SuiTestnet, destination: EthSepolia },
+        BridgeRoute { source: SuiTestnet, destination: EthCustom },
+        BridgeRoute { source: SuiCustom, destination: EthCustom },
+        BridgeRoute { source: SuiCustom, destination: EthSepolia },
+        BridgeRoute { source: EthSepolia, destination: SuiTestnet },
+        BridgeRoute { source: EthSepolia, destination: SuiCustom },
+        BridgeRoute { source: EthCustom, destination: SuiTestnet },
+        BridgeRoute { source: EthCustom, destination: SuiCustom }
+    ]
+}
+
+ + + +
+ + + +## Function `is_valid_route` + + + +
public fun is_valid_route(source: u8, destination: u8): bool
+
+ + + +
+Implementation + + +
public fun is_valid_route(source: u8, destination: u8): bool {
+    let route = BridgeRoute { source, destination };
+    valid_routes().contains(&route)
+}
+
+ + + +
+ + + +## Function `get_route` + + + +
public fun get_route(source: u8, destination: u8): chain_ids::BridgeRoute
+
+ + + +
+Implementation + + +
public fun get_route(source: u8, destination: u8): BridgeRoute {
+    let route = BridgeRoute { source, destination };
+    assert!(valid_routes().contains(&route), EInvalidBridgeRoute);
+    route
+}
+
+ + + +
diff --git a/crates/sui-framework/docs/bridge/committee.md b/crates/sui-framework/docs/bridge/committee.md new file mode 100644 index 0000000000000..0d0b1c9f58766 --- /dev/null +++ b/crates/sui-framework/docs/bridge/committee.md @@ -0,0 +1,667 @@ +--- +title: Module `0xb::committee` +--- + + + +- [Struct `BlocklistValidatorEvent`](#0xb_committee_BlocklistValidatorEvent) +- [Struct `BridgeCommittee`](#0xb_committee_BridgeCommittee) +- [Struct `CommitteeUpdateEvent`](#0xb_committee_CommitteeUpdateEvent) +- [Struct `CommitteeMember`](#0xb_committee_CommitteeMember) +- [Struct `CommitteeMemberRegistration`](#0xb_committee_CommitteeMemberRegistration) +- [Constants](#@Constants_0) +- [Function `verify_signatures`](#0xb_committee_verify_signatures) +- [Function `create`](#0xb_committee_create) +- [Function `register`](#0xb_committee_register) +- [Function `try_create_next_committee`](#0xb_committee_try_create_next_committee) +- [Function `execute_blocklist`](#0xb_committee_execute_blocklist) +- [Function `committee_members`](#0xb_committee_committee_members) +- [Function `check_uniqueness_bridge_keys`](#0xb_committee_check_uniqueness_bridge_keys) + + +
use 0x1::option;
+use 0x1::vector;
+use 0x2::ecdsa_k1;
+use 0x2::event;
+use 0x2::tx_context;
+use 0x2::vec_map;
+use 0x2::vec_set;
+use 0x3::sui_system;
+use 0xb::crypto;
+use 0xb::message;
+
+ + + + + +## Struct `BlocklistValidatorEvent` + + + +
struct BlocklistValidatorEvent has copy, drop
+
+ + + +
+Fields + + +
+
+blocklisted: bool +
+
+ +
+
+public_keys: vector<vector<u8>> +
+
+ +
+
+ + +
+ + + +## Struct `BridgeCommittee` + + + +
struct BridgeCommittee has store
+
+ + + +
+Fields + + +
+
+members: vec_map::VecMap<vector<u8>, committee::CommitteeMember> +
+
+ +
+
+member_registrations: vec_map::VecMap<address, committee::CommitteeMemberRegistration> +
+
+ +
+
+last_committee_update_epoch: u64 +
+
+ +
+
+ + +
+ + + +## Struct `CommitteeUpdateEvent` + + + +
struct CommitteeUpdateEvent has copy, drop
+
+ + + +
+Fields + + +
+
+members: vec_map::VecMap<vector<u8>, committee::CommitteeMember> +
+
+ +
+
+stake_participation_percentage: u64 +
+
+ +
+
+ + +
+ + + +## Struct `CommitteeMember` + + + +
struct CommitteeMember has copy, drop, store
+
+ + + +
+Fields + + +
+
+sui_address: address +
+
+ The Sui Address of the validator +
+
+bridge_pubkey_bytes: vector<u8> +
+
+ The public key bytes of the bridge key +
+
+voting_power: u64 +
+
+ Voting power, values are voting power in the scale of 10000. +
+
+http_rest_url: vector<u8> +
+
+ The HTTP REST URL the member's node listens to + it looks like b'https://127.0.0.1:9191' +
+
+blocklisted: bool +
+
+ If this member is blocklisted +
+
+ + +
+ + + +## Struct `CommitteeMemberRegistration` + + + +
struct CommitteeMemberRegistration has copy, drop, store
+
+ + + +
+Fields + + +
+
+sui_address: address +
+
+ The Sui Address of the validator +
+
+bridge_pubkey_bytes: vector<u8> +
+
+ The public key bytes of the bridge key +
+
+http_rest_url: vector<u8> +
+
+ The HTTP REST URL the member's node listens to + it looks like b'https://127.0.0.1:9191' +
+
+ + +
+ + + +## Constants + + + + + + +
const ENotSystemAddress: u64 = 3;
+
+ + + + + + + +
const EInvalidSignature: u64 = 2;
+
+ + + + + + + +
const ECDSA_COMPRESSED_PUBKEY_LENGTH: u64 = 33;
+
+ + + + + + + +
const ECommitteeAlreadyInitiated: u64 = 7;
+
+ + + + + + + +
const EDuplicatePubkey: u64 = 8;
+
+ + + + + + + +
const EDuplicatedSignature: u64 = 1;
+
+ + + + + + + +
const EInvalidPubkeyLength: u64 = 6;
+
+ + + + + + + +
const ESenderNotActiveValidator: u64 = 5;
+
+ + + + + + + +
const ESignatureBelowThreshold: u64 = 0;
+
+ + + + + + + +
const EValidatorBlocklistContainsUnknownKey: u64 = 4;
+
+ + + + + + + +
const SUI_MESSAGE_PREFIX: vector<u8> = [83, 85, 73, 95, 66, 82, 73, 68, 71, 69, 95, 77, 69, 83, 83, 65, 71, 69];
+
+ + + + + +## Function `verify_signatures` + + + +
public fun verify_signatures(self: &committee::BridgeCommittee, message: message::BridgeMessage, signatures: vector<vector<u8>>)
+
+ + + +
+Implementation + + +
public fun verify_signatures(
+    self: &BridgeCommittee,
+    message: BridgeMessage,
+    signatures: vector<vector<u8>>,
+) {
+    let (mut i, signature_counts) = (0, vector::length(&signatures));
+    let mut seen_pub_key = vec_set::empty<vector<u8>>();
+    let required_voting_power = message.required_voting_power();
+    // add prefix to the message bytes
+    let mut message_bytes = SUI_MESSAGE_PREFIX;
+    message_bytes.append(message.serialize_message());
+
+    let mut threshold = 0;
+    while (i < signature_counts) {
+        let pubkey = ecdsa_k1::secp256k1_ecrecover(&signatures[i], &message_bytes, 0);
+
+        // check duplicate
+        // and make sure pub key is part of the committee
+        assert!(!seen_pub_key.contains(&pubkey), EDuplicatedSignature);
+        assert!(self.members.contains(&pubkey), EInvalidSignature);
+
+        // get committee signature weight and check pubkey is part of the committee
+        let member = &self.members[&pubkey];
+        if (!member.blocklisted) {
+            threshold = threshold + member.voting_power;
+        };
+        seen_pub_key.insert(pubkey);
+        i = i + 1;
+    };
+
+    assert!(threshold >= required_voting_power, ESignatureBelowThreshold);
+}
+
+ + + +
+ + + +## Function `create` + + + +
public(friend) fun create(ctx: &tx_context::TxContext): committee::BridgeCommittee
+
+ + + +
+Implementation + + +
public(package) fun create(ctx: &TxContext): BridgeCommittee {
+    assert!(tx_context::sender(ctx) == @0x0, ENotSystemAddress);
+    BridgeCommittee {
+        members: vec_map::empty(),
+        member_registrations: vec_map::empty(),
+        last_committee_update_epoch: 0,
+    }
+}
+
+ + + +
+ + + +## Function `register` + + + +
public(friend) fun register(self: &mut committee::BridgeCommittee, system_state: &mut sui_system::SuiSystemState, bridge_pubkey_bytes: vector<u8>, http_rest_url: vector<u8>, ctx: &tx_context::TxContext)
+
+ + + +
+Implementation + + +
public(package) fun register(
+    self: &mut BridgeCommittee,
+    system_state: &mut SuiSystemState,
+    bridge_pubkey_bytes: vector<u8>,
+    http_rest_url: vector<u8>,
+    ctx: &TxContext
+) {
+    // We disallow registration after committee initiated in v1
+    assert!(self.members.is_empty(), ECommitteeAlreadyInitiated);
+    // Ensure pubkey is valid
+    assert!(bridge_pubkey_bytes.length() == ECDSA_COMPRESSED_PUBKEY_LENGTH, EInvalidPubkeyLength);
+    // sender must be the same sender that created the validator object, this is to prevent DDoS from non-validator actor.
+    let sender = ctx.sender();
+    let validators = system_state.active_validator_addresses();
+
+    assert!(validators.contains(&sender), ESenderNotActiveValidator);
+    // Sender is active validator, record the registration
+
+    // In case validator need to update the info
+    let registration = if (self.member_registrations.contains(&sender)) {
+        let registration = &mut self.member_registrations[&sender];
+        registration.http_rest_url = http_rest_url;
+        registration.bridge_pubkey_bytes = bridge_pubkey_bytes;
+        *registration
+    } else {
+        let registration = CommitteeMemberRegistration {
+            sui_address: sender,
+            bridge_pubkey_bytes,
+            http_rest_url,
+        };
+        self.member_registrations.insert(sender, registration);
+        registration
+    };
+
+    // check uniqueness of the bridge pubkey.
+    // `try_create_next_committee` will abort if bridge_pubkey_bytes are not unique and
+    // that will fail the end of epoch transaction (possibly "forever", well, we
+    // need to deploy proper validator changes to stop end of epoch from failing).
+    check_uniqueness_bridge_keys(self, bridge_pubkey_bytes);
+
+    emit(registration)
+}
+
+ + + +
+ + + +## Function `try_create_next_committee` + + + +
public(friend) fun try_create_next_committee(self: &mut committee::BridgeCommittee, active_validator_voting_power: vec_map::VecMap<address, u64>, min_stake_participation_percentage: u64, ctx: &tx_context::TxContext)
+
+ + + +
+Implementation + + +
public(package) fun try_create_next_committee(
+    self: &mut BridgeCommittee,
+    active_validator_voting_power: VecMap<address, u64>,
+    min_stake_participation_percentage: u64,
+    ctx: &TxContext
+) {
+    let mut i = 0;
+    let mut new_members = vec_map::empty();
+    let mut stake_participation_percentage = 0;
+
+    while (i < self.member_registrations.size()) {
+        // retrieve registration
+        let (_, registration) = self.member_registrations.get_entry_by_idx(i);
+        // Find validator stake amount from system state
+
+        // Process registration if it's active validator
+        let voting_power = active_validator_voting_power.try_get(®istration.sui_address);
+        if (voting_power.is_some()) {
+            let voting_power = voting_power.destroy_some();
+            stake_participation_percentage = stake_participation_percentage + voting_power;
+
+            let member = CommitteeMember {
+                sui_address: registration.sui_address,
+                bridge_pubkey_bytes: registration.bridge_pubkey_bytes,
+                voting_power: (voting_power as u64),
+                http_rest_url: registration.http_rest_url,
+                blocklisted: false,
+            };
+
+            new_members.insert(registration.bridge_pubkey_bytes, member)
+        };
+
+        i = i + 1;
+    };
+
+    // Make sure the new committee represent enough stakes, percentage are accurate to 2DP
+    if (stake_participation_percentage >= min_stake_participation_percentage) {
+        // Clear registrations
+        self.member_registrations = vec_map::empty();
+        // Store new committee info
+        self.members = new_members;
+        self.last_committee_update_epoch = ctx.epoch();
+
+        emit(CommitteeUpdateEvent {
+            members: new_members,
+            stake_participation_percentage
+        })
+    }
+}
+
+ + + +
+ + + +## Function `execute_blocklist` + + + +
public(friend) fun execute_blocklist(self: &mut committee::BridgeCommittee, blocklist: message::Blocklist)
+
+ + + +
+Implementation + + +
public(package) fun execute_blocklist(self: &mut BridgeCommittee, blocklist: Blocklist) {
+    let blocklisted = blocklist.blocklist_type() != 1;
+    let eth_addresses = blocklist.blocklist_validator_addresses();
+    let list_len = eth_addresses.length();
+    let mut list_idx = 0;
+    let mut member_idx = 0;
+    let mut pub_keys = vector[];
+
+    while (list_idx < list_len) {
+        let target_address = ð_addresses[list_idx];
+        let mut found = false;
+
+        while (member_idx < self.members.size()) {
+            let (pub_key, member) = self.members.get_entry_by_idx_mut(member_idx);
+            let eth_address = crypto::ecdsa_pub_key_to_eth_address(pub_key);
+
+            if (*target_address == eth_address) {
+                member.blocklisted = blocklisted;
+                pub_keys.push_back(*pub_key);
+                found = true;
+                member_idx = 0;
+                break
+            };
+
+            member_idx = member_idx + 1;
+        };
+
+        assert!(found, EValidatorBlocklistContainsUnknownKey);
+        list_idx = list_idx + 1;
+    };
+
+    emit(BlocklistValidatorEvent {
+        blocklisted,
+        public_keys: pub_keys,
+    })
+}
+
+ + + +
+ + + +## Function `committee_members` + + + +
public(friend) fun committee_members(self: &committee::BridgeCommittee): &vec_map::VecMap<vector<u8>, committee::CommitteeMember>
+
+ + + +
+Implementation + + +
public(package) fun committee_members(
+    self: &BridgeCommittee,
+): &VecMap<vector<u8>, CommitteeMember> {
+    &self.members
+}
+
+ + + +
+ + + +## Function `check_uniqueness_bridge_keys` + + + +
fun check_uniqueness_bridge_keys(self: &committee::BridgeCommittee, bridge_pubkey_bytes: vector<u8>)
+
+ + + +
+Implementation + + +
fun check_uniqueness_bridge_keys(self: &BridgeCommittee, bridge_pubkey_bytes: vector<u8>) {
+    let mut count = self.member_registrations.size();
+    // bridge_pubkey_bytes must be found once and once only
+    let mut bridge_key_found = false;
+    while (count > 0) {
+        count = count - 1;
+        let (_, registration) = self.member_registrations.get_entry_by_idx(count);
+        if (registration.bridge_pubkey_bytes == bridge_pubkey_bytes) {
+            assert!(!bridge_key_found, EDuplicatePubkey);
+            bridge_key_found = true; // bridge_pubkey_bytes found, we must not have another one
+        }
+    };
+}
+
+ + + +
diff --git a/crates/sui-framework/docs/bridge/crypto.md b/crates/sui-framework/docs/bridge/crypto.md new file mode 100644 index 0000000000000..13c63ffb27f2e --- /dev/null +++ b/crates/sui-framework/docs/bridge/crypto.md @@ -0,0 +1,58 @@ +--- +title: Module `0xb::crypto` +--- + + + +- [Function `ecdsa_pub_key_to_eth_address`](#0xb_crypto_ecdsa_pub_key_to_eth_address) + + +
use 0x2::ecdsa_k1;
+use 0x2::hash;
+
+ + + + + +## Function `ecdsa_pub_key_to_eth_address` + + + +
public(friend) fun ecdsa_pub_key_to_eth_address(compressed_pub_key: &vector<u8>): vector<u8>
+
+ + + +
+Implementation + + +
public(package) fun ecdsa_pub_key_to_eth_address(compressed_pub_key: &vector<u8>): vector<u8> {
+    // Decompress pub key
+    let decompressed = ecdsa_k1::decompress_pubkey(compressed_pub_key);
+
+    // Skip the first byte
+    let (mut i, mut decompressed_64) = (1, vector[]);
+    while (i < 65) {
+        decompressed_64.push_back(decompressed[i]);
+        i = i + 1;
+    };
+
+    // Hash
+    let hash = keccak256(&decompressed_64);
+
+    // Take last 20 bytes
+    let mut address = vector[];
+    let mut i = 12;
+    while (i < 32) {
+        address.push_back(hash[i]);
+        i = i + 1;
+    };
+    address
+}
+
+ + + +
diff --git a/crates/sui-framework/docs/bridge/limiter.md b/crates/sui-framework/docs/bridge/limiter.md new file mode 100644 index 0000000000000..d9593942c8459 --- /dev/null +++ b/crates/sui-framework/docs/bridge/limiter.md @@ -0,0 +1,473 @@ +--- +title: Module `0xb::limiter` +--- + + + +- [Struct `TransferLimiter`](#0xb_limiter_TransferLimiter) +- [Struct `TransferRecord`](#0xb_limiter_TransferRecord) +- [Struct `UpdateRouteLimitEvent`](#0xb_limiter_UpdateRouteLimitEvent) +- [Constants](#@Constants_0) +- [Function `get_route_limit`](#0xb_limiter_get_route_limit) +- [Function `new`](#0xb_limiter_new) +- [Function `check_and_record_sending_transfer`](#0xb_limiter_check_and_record_sending_transfer) +- [Function `update_route_limit`](#0xb_limiter_update_route_limit) +- [Function `current_hour_since_epoch`](#0xb_limiter_current_hour_since_epoch) +- [Function `adjust_transfer_records`](#0xb_limiter_adjust_transfer_records) +- [Function `initial_transfer_limits`](#0xb_limiter_initial_transfer_limits) + + +
use 0x1::option;
+use 0x1::vector;
+use 0x2::clock;
+use 0x2::event;
+use 0x2::vec_map;
+use 0xb::chain_ids;
+use 0xb::treasury;
+
+ + + + + +## Struct `TransferLimiter` + + + +
struct TransferLimiter has store
+
+ + + +
+Fields + + +
+
+transfer_limits: vec_map::VecMap<chain_ids::BridgeRoute, u64> +
+
+ +
+
+transfer_records: vec_map::VecMap<chain_ids::BridgeRoute, limiter::TransferRecord> +
+
+ +
+
+ + +
+ + + +## Struct `TransferRecord` + + + +
struct TransferRecord has store
+
+ + + +
+Fields + + +
+
+hour_head: u64 +
+
+ +
+
+hour_tail: u64 +
+
+ +
+
+per_hour_amounts: vector<u64> +
+
+ +
+
+total_amount: u64 +
+
+ +
+
+ + +
+ + + +## Struct `UpdateRouteLimitEvent` + + + +
struct UpdateRouteLimitEvent has copy, drop
+
+ + + +
+Fields + + +
+
+sending_chain: u8 +
+
+ +
+
+receiving_chain: u8 +
+
+ +
+
+new_limit: u64 +
+
+ +
+
+ + +
+ + + +## Constants + + + + + + +
const ELimitNotFoundForRoute: u64 = 0;
+
+ + + + + + + +
const MAX_TRANSFER_LIMIT: u64 = 18446744073709551615;
+
+ + + + + + + +
const USD_VALUE_MULTIPLIER: u64 = 100000000;
+
+ + + + + +## Function `get_route_limit` + + + +
public fun get_route_limit(self: &limiter::TransferLimiter, route: &chain_ids::BridgeRoute): u64
+
+ + + +
+Implementation + + +
public fun get_route_limit(self: &TransferLimiter, route: &BridgeRoute): u64 {
+    self.transfer_limits[route]
+}
+
+ + + +
+ + + +## Function `new` + + + +
public(friend) fun new(): limiter::TransferLimiter
+
+ + + +
+Implementation + + +
public(package) fun new(): TransferLimiter {
+    // hardcoded limit for bridge genesis
+    TransferLimiter {
+        transfer_limits: initial_transfer_limits(),
+        transfer_records: vec_map::empty()
+    }
+}
+
+ + + +
+ + + +## Function `check_and_record_sending_transfer` + + + +
public(friend) fun check_and_record_sending_transfer<T>(self: &mut limiter::TransferLimiter, treasury: &treasury::BridgeTreasury, clock: &clock::Clock, route: chain_ids::BridgeRoute, amount: u64): bool
+
+ + + +
+Implementation + + +
public(package) fun check_and_record_sending_transfer<T>(
+    self: &mut TransferLimiter,
+    treasury: &BridgeTreasury,
+    clock: &Clock,
+    route: BridgeRoute,
+    amount: u64
+): bool {
+    // Create record for route if not exists
+    if (!self.transfer_records.contains(&route)) {
+        self.transfer_records.insert(route, TransferRecord {
+            hour_head: 0,
+            hour_tail: 0,
+            per_hour_amounts: vector[],
+            total_amount: 0
+        })
+    };
+    let record = self.transfer_records.get_mut(&route);
+    let current_hour_since_epoch = current_hour_since_epoch(clock);
+
+    record.adjust_transfer_records(current_hour_since_epoch);
+
+    // Get limit for the route
+    let route_limit = self.transfer_limits.try_get(&route);
+    assert!(route_limit.is_some(), ELimitNotFoundForRoute);
+    let route_limit = route_limit.destroy_some();
+    let route_limit_adjusted =
+        (route_limit as u128) * (treasury.decimal_multiplier<T>() as u128);
+
+    // Compute notional amount
+    // Upcast to u128 to prevent overflow, to not miss out on small amounts.
+    let value = (treasury.notional_value<T>() as u128);
+    let notional_amount_with_token_multiplier = value * (amount as u128);
+
+    // Check if transfer amount exceed limit
+    // Upscale them to the token's decimal.
+    if ((record.total_amount as u128)
+        * (treasury.decimal_multiplier<T>() as u128)
+        + notional_amount_with_token_multiplier > route_limit_adjusted
+    ) {
+        return false
+    };
+
+    // Now scale down to notional value
+    let notional_amount = notional_amount_with_token_multiplier
+        / (treasury.decimal_multiplier<T>() as u128);
+    // Should be safe to downcast to u64 after dividing by the decimals
+    let notional_amount = (notional_amount as u64);
+
+    // Record transfer value
+    let new_amount = record.per_hour_amounts.pop_back() + notional_amount;
+    record.per_hour_amounts.push_back(new_amount);
+    record.total_amount = record.total_amount + notional_amount;
+    true
+}
+
+ + + +
+ + + +## Function `update_route_limit` + + + +
public(friend) fun update_route_limit(self: &mut limiter::TransferLimiter, route: &chain_ids::BridgeRoute, new_usd_limit: u64)
+
+ + + +
+Implementation + + +
public(package) fun update_route_limit(
+    self: &mut TransferLimiter,
+    route: &BridgeRoute,
+    new_usd_limit: u64
+) {
+    let receiving_chain = *route.destination();
+
+    if (!self.transfer_limits.contains(route)) {
+        self.transfer_limits.insert(*route, new_usd_limit);
+    } else {
+        *&mut self.transfer_limits[route] = new_usd_limit;
+    };
+
+    emit(UpdateRouteLimitEvent {
+        sending_chain: *route.source(),
+        receiving_chain,
+        new_limit: new_usd_limit,
+    })
+}
+
+ + + +
+ + + +## Function `current_hour_since_epoch` + + + +
fun current_hour_since_epoch(clock: &clock::Clock): u64
+
+ + + +
+Implementation + + +
fun current_hour_since_epoch(clock: &Clock): u64 {
+    clock::timestamp_ms(clock) / 3600000
+}
+
+ + + +
+ + + +## Function `adjust_transfer_records` + + + +
fun adjust_transfer_records(self: &mut limiter::TransferRecord, current_hour_since_epoch: u64)
+
+ + + +
+Implementation + + +
fun adjust_transfer_records(self: &mut TransferRecord, current_hour_since_epoch: u64) {
+    if (self.hour_head == current_hour_since_epoch) {
+        return // nothing to backfill
+    };
+
+    let target_tail = current_hour_since_epoch - 23;
+
+    // If `hour_head` is even older than 24 hours ago, it means all items in
+    // `per_hour_amounts` are to be evicted.
+    if (self.hour_head < target_tail) {
+        self.per_hour_amounts = vector[];
+        self.total_amount = 0;
+        self.hour_tail = target_tail;
+        self.hour_head = target_tail;
+        // Don't forget to insert this hour's record
+        self.per_hour_amounts.push_back(0);
+    } else {
+        // self.hour_head is within 24 hour range.
+        // some items in `per_hour_amounts` are still valid, we remove stale hours.
+        while (self.hour_tail < target_tail) {
+            self.total_amount = self.total_amount - self.per_hour_amounts.remove(0);
+            self.hour_tail = self.hour_tail + 1;
+        }
+    };
+
+    // Backfill from hour_head to current hour
+    while (self.hour_head < current_hour_since_epoch) {
+        self.per_hour_amounts.push_back(0);
+        self.hour_head = self.hour_head + 1;
+    }
+}
+
+ + + +
+ + + +## Function `initial_transfer_limits` + + + +
fun initial_transfer_limits(): vec_map::VecMap<chain_ids::BridgeRoute, u64>
+
+ + + +
+Implementation + + +
fun initial_transfer_limits(): VecMap<BridgeRoute, u64> {
+    let mut transfer_limits = vec_map::empty();
+    // 5M limit on Sui -> Ethereum mainnet
+    transfer_limits.insert(
+        chain_ids::get_route(chain_ids::eth_mainnet(), chain_ids::sui_mainnet()),
+        5_000_000 * USD_VALUE_MULTIPLIER
+    );
+
+    // MAX limit for testnet and devnet
+    transfer_limits.insert(
+        chain_ids::get_route(chain_ids::eth_sepolia(), chain_ids::sui_testnet()),
+        MAX_TRANSFER_LIMIT
+    );
+
+    transfer_limits.insert(
+        chain_ids::get_route(chain_ids::eth_sepolia(), chain_ids::sui_custom()),
+        MAX_TRANSFER_LIMIT
+    );
+
+    transfer_limits.insert(
+        chain_ids::get_route(chain_ids::eth_custom(), chain_ids::sui_testnet()),
+        MAX_TRANSFER_LIMIT
+    );
+
+    transfer_limits.insert(
+        chain_ids::get_route(chain_ids::eth_custom(), chain_ids::sui_custom()),
+        MAX_TRANSFER_LIMIT
+    );
+
+    transfer_limits
+}
+
+ + + +
diff --git a/crates/sui-framework/docs/bridge/message.md b/crates/sui-framework/docs/bridge/message.md new file mode 100644 index 0000000000000..cee24aeb8a176 --- /dev/null +++ b/crates/sui-framework/docs/bridge/message.md @@ -0,0 +1,1750 @@ +--- +title: Module `0xb::message` +--- + + + +- [Struct `BridgeMessage`](#0xb_message_BridgeMessage) +- [Struct `BridgeMessageKey`](#0xb_message_BridgeMessageKey) +- [Struct `TokenPayload`](#0xb_message_TokenPayload) +- [Struct `EmergencyOp`](#0xb_message_EmergencyOp) +- [Struct `Blocklist`](#0xb_message_Blocklist) +- [Struct `UpdateBridgeLimit`](#0xb_message_UpdateBridgeLimit) +- [Struct `UpdateAssetPrice`](#0xb_message_UpdateAssetPrice) +- [Struct `AddTokenOnSui`](#0xb_message_AddTokenOnSui) +- [Constants](#@Constants_0) +- [Function `extract_token_bridge_payload`](#0xb_message_extract_token_bridge_payload) +- [Function `extract_emergency_op_payload`](#0xb_message_extract_emergency_op_payload) +- [Function `extract_blocklist_payload`](#0xb_message_extract_blocklist_payload) +- [Function `extract_update_bridge_limit`](#0xb_message_extract_update_bridge_limit) +- [Function `extract_update_asset_price`](#0xb_message_extract_update_asset_price) +- [Function `extract_add_tokens_on_sui`](#0xb_message_extract_add_tokens_on_sui) +- [Function `serialize_message`](#0xb_message_serialize_message) +- [Function `create_token_bridge_message`](#0xb_message_create_token_bridge_message) +- [Function `create_emergency_op_message`](#0xb_message_create_emergency_op_message) +- [Function `create_blocklist_message`](#0xb_message_create_blocklist_message) +- [Function `create_update_bridge_limit_message`](#0xb_message_create_update_bridge_limit_message) +- [Function `create_update_asset_price_message`](#0xb_message_create_update_asset_price_message) +- [Function `create_add_tokens_on_sui_message`](#0xb_message_create_add_tokens_on_sui_message) +- [Function `create_key`](#0xb_message_create_key) +- [Function `key`](#0xb_message_key) +- [Function `message_version`](#0xb_message_message_version) +- [Function `message_type`](#0xb_message_message_type) +- [Function `seq_num`](#0xb_message_seq_num) +- [Function `source_chain`](#0xb_message_source_chain) +- [Function `token_target_chain`](#0xb_message_token_target_chain) +- [Function `token_target_address`](#0xb_message_token_target_address) +- [Function `token_type`](#0xb_message_token_type) +- [Function `token_amount`](#0xb_message_token_amount) +- [Function `emergency_op_type`](#0xb_message_emergency_op_type) +- [Function `blocklist_type`](#0xb_message_blocklist_type) +- [Function `blocklist_validator_addresses`](#0xb_message_blocklist_validator_addresses) +- [Function `update_bridge_limit_payload_sending_chain`](#0xb_message_update_bridge_limit_payload_sending_chain) +- [Function `update_bridge_limit_payload_receiving_chain`](#0xb_message_update_bridge_limit_payload_receiving_chain) +- [Function `update_bridge_limit_payload_limit`](#0xb_message_update_bridge_limit_payload_limit) +- [Function `update_asset_price_payload_token_id`](#0xb_message_update_asset_price_payload_token_id) +- [Function `update_asset_price_payload_new_price`](#0xb_message_update_asset_price_payload_new_price) +- [Function `is_native`](#0xb_message_is_native) +- [Function `token_ids`](#0xb_message_token_ids) +- [Function `token_type_names`](#0xb_message_token_type_names) +- [Function `token_prices`](#0xb_message_token_prices) +- [Function `emergency_op_pause`](#0xb_message_emergency_op_pause) +- [Function `emergency_op_unpause`](#0xb_message_emergency_op_unpause) +- [Function `required_voting_power`](#0xb_message_required_voting_power) +- [Function `reverse_bytes`](#0xb_message_reverse_bytes) +- [Function `peel_u64_be`](#0xb_message_peel_u64_be) + + +
use 0x1::ascii;
+use 0x1::vector;
+use 0x2::bcs;
+use 0xb::chain_ids;
+use 0xb::message_types;
+
+ + + + + +## Struct `BridgeMessage` + + + +
struct BridgeMessage has copy, drop, store
+
+ + + +
+Fields + + +
+
+message_type: u8 +
+
+ +
+
+message_version: u8 +
+
+ +
+
+seq_num: u64 +
+
+ +
+
+source_chain: u8 +
+
+ +
+
+payload: vector<u8> +
+
+ +
+
+ + +
+ + + +## Struct `BridgeMessageKey` + + + +
struct BridgeMessageKey has copy, drop, store
+
+ + + +
+Fields + + +
+
+source_chain: u8 +
+
+ +
+
+message_type: u8 +
+
+ +
+
+bridge_seq_num: u64 +
+
+ +
+
+ + +
+ + + +## Struct `TokenPayload` + + + +
struct TokenPayload has drop
+
+ + + +
+Fields + + +
+
+sender_address: vector<u8> +
+
+ +
+
+target_chain: u8 +
+
+ +
+
+target_address: vector<u8> +
+
+ +
+
+token_type: u8 +
+
+ +
+
+amount: u64 +
+
+ +
+
+ + +
+ + + +## Struct `EmergencyOp` + + + +
struct EmergencyOp has drop
+
+ + + +
+Fields + + +
+
+op_type: u8 +
+
+ +
+
+ + +
+ + + +## Struct `Blocklist` + + + +
struct Blocklist has drop
+
+ + + +
+Fields + + +
+
+blocklist_type: u8 +
+
+ +
+
+validator_eth_addresses: vector<vector<u8>> +
+
+ +
+
+ + +
+ + + +## Struct `UpdateBridgeLimit` + + + +
struct UpdateBridgeLimit has drop
+
+ + + +
+Fields + + +
+
+receiving_chain: u8 +
+
+ +
+
+sending_chain: u8 +
+
+ +
+
+limit: u64 +
+
+ +
+
+ + +
+ + + +## Struct `UpdateAssetPrice` + + + +
struct UpdateAssetPrice has drop
+
+ + + +
+Fields + + +
+
+token_id: u8 +
+
+ +
+
+new_price: u64 +
+
+ +
+
+ + +
+ + + +## Struct `AddTokenOnSui` + + + +
struct AddTokenOnSui has drop
+
+ + + +
+Fields + + +
+
+native_token: bool +
+
+ +
+
+token_ids: vector<u8> +
+
+ +
+
+token_type_names: vector<ascii::String> +
+
+ +
+
+token_prices: vector<u64> +
+
+ +
+
+ + +
+ + + +## Constants + + + + + + +
const CURRENT_MESSAGE_VERSION: u8 = 1;
+
+ + + + + + + +
const ECDSA_ADDRESS_LENGTH: u64 = 20;
+
+ + + + + + + +
const EEmptyList: u64 = 2;
+
+ + + + + + + +
const EInvalidAddressLength: u64 = 1;
+
+ + + + + + + +
const EInvalidEmergencyOpType: u64 = 4;
+
+ + + + + + + +
const EInvalidMessageType: u64 = 3;
+
+ + + + + + + +
const EInvalidPayloadLength: u64 = 5;
+
+ + + + + + + +
const ETrailingBytes: u64 = 0;
+
+ + + + + + + +
const PAUSE: u8 = 0;
+
+ + + + + + + +
const UNPAUSE: u8 = 1;
+
+ + + + + +## Function `extract_token_bridge_payload` + + + +
public fun extract_token_bridge_payload(message: &message::BridgeMessage): message::TokenPayload
+
+ + + +
+Implementation + + +
public fun extract_token_bridge_payload(message: &BridgeMessage): TokenPayload {
+    let mut bcs = bcs::new(message.payload);
+    let sender_address = bcs.peel_vec_u8();
+    let target_chain = bcs.peel_u8();
+    let target_address = bcs.peel_vec_u8();
+    let token_type = bcs.peel_u8();
+    let amount = peel_u64_be(&mut bcs);
+
+    // TODO: add test case for invalid chain id
+    // TODO: replace with `chain_ids::is_valid_chain_id()`
+    chain_ids::assert_valid_chain_id(target_chain);
+    assert!(bcs.into_remainder_bytes().is_empty(), ETrailingBytes);
+
+    TokenPayload {
+        sender_address,
+        target_chain,
+        target_address,
+        token_type,
+        amount
+    }
+}
+
+ + + +
+ + + +## Function `extract_emergency_op_payload` + +Emergency op payload is just a single byte + + +
public fun extract_emergency_op_payload(message: &message::BridgeMessage): message::EmergencyOp
+
+ + + +
+Implementation + + +
public fun extract_emergency_op_payload(message: &BridgeMessage): EmergencyOp {
+    assert!(message.payload.length() == 1, ETrailingBytes);
+    EmergencyOp { op_type: message.payload[0] }
+}
+
+ + + +
+ + + +## Function `extract_blocklist_payload` + + + +
public fun extract_blocklist_payload(message: &message::BridgeMessage): message::Blocklist
+
+ + + +
+Implementation + + +
public fun extract_blocklist_payload(message: &BridgeMessage): Blocklist {
+    // blocklist payload should consist of one byte blocklist type, and list of 33 bytes ecdsa pub keys
+    let mut bcs = bcs::new(message.payload);
+    let blocklist_type = bcs.peel_u8();
+    let mut address_count = bcs.peel_u8();
+
+    // TODO: add test case for 0 value
+    assert!(address_count != 0, EEmptyList);
+
+    let mut validator_eth_addresses = vector[];
+    while (address_count > 0) {
+        let (mut address, mut i) = (vector[], 0);
+        while (i < ECDSA_ADDRESS_LENGTH) {
+            address.push_back(bcs.peel_u8());
+            i = i + 1;
+        };
+        validator_eth_addresses.push_back(address);
+        address_count = address_count - 1;
+    };
+
+    assert!(bcs.into_remainder_bytes().is_empty(), ETrailingBytes);
+
+    Blocklist {
+        blocklist_type,
+        validator_eth_addresses
+    }
+}
+
+ + + +
+ + + +## Function `extract_update_bridge_limit` + + + +
public fun extract_update_bridge_limit(message: &message::BridgeMessage): message::UpdateBridgeLimit
+
+ + + +
+Implementation + + +
public fun extract_update_bridge_limit(message: &BridgeMessage): UpdateBridgeLimit {
+    let mut bcs = bcs::new(message.payload);
+    let sending_chain = bcs.peel_u8();
+    let limit = peel_u64_be(&mut bcs);
+
+    // TODO: add test case for invalid chain id
+    chain_ids::assert_valid_chain_id(sending_chain);
+    assert!(bcs.into_remainder_bytes().is_empty(), ETrailingBytes);
+
+    UpdateBridgeLimit {
+        receiving_chain: message.source_chain,
+        sending_chain,
+        limit
+    }
+}
+
+ + + +
+ + + +## Function `extract_update_asset_price` + + + +
public fun extract_update_asset_price(message: &message::BridgeMessage): message::UpdateAssetPrice
+
+ + + +
+Implementation + + +
public fun extract_update_asset_price(message: &BridgeMessage): UpdateAssetPrice {
+    let mut bcs = bcs::new(message.payload);
+    let token_id = bcs.peel_u8();
+    let new_price = peel_u64_be(&mut bcs);
+
+    assert!(bcs.into_remainder_bytes().is_empty(), ETrailingBytes);
+
+    UpdateAssetPrice {
+        token_id,
+        new_price
+    }
+}
+
+ + + +
+ + + +## Function `extract_add_tokens_on_sui` + + + +
public fun extract_add_tokens_on_sui(message: &message::BridgeMessage): message::AddTokenOnSui
+
+ + + +
+Implementation + + +
public fun extract_add_tokens_on_sui(message: &BridgeMessage): AddTokenOnSui {
+    let mut bcs = bcs::new(message.payload);
+    let native_token = bcs.peel_bool();
+    let token_ids = bcs.peel_vec_u8();
+    let token_type_names_bytes = bcs.peel_vec_vec_u8();
+    let token_prices = bcs.peel_vec_u64();
+
+    let mut n = 0;
+    let mut token_type_names = vector[];
+    while (n < token_type_names_bytes.length()){
+        token_type_names.push_back(ascii::string(*token_type_names_bytes.borrow(n)));
+        n = n + 1;
+    };
+    assert!(bcs.into_remainder_bytes().is_empty(), ETrailingBytes);
+    AddTokenOnSui {
+        native_token,
+        token_ids,
+        token_type_names,
+        token_prices
+    }
+}
+
+ + + +
+ + + +## Function `serialize_message` + + + +
public fun serialize_message(message: message::BridgeMessage): vector<u8>
+
+ + + +
+Implementation + + +
public fun serialize_message(message: BridgeMessage): vector<u8> {
+    let BridgeMessage {
+        message_type,
+        message_version,
+        seq_num,
+        source_chain,
+        payload
+    } = message;
+
+    let mut message = vector[
+        message_type,
+        message_version,
+    ];
+
+    // bcs serializes u64 as 8 bytes
+    message.append(reverse_bytes(bcs::to_bytes(&seq_num)));
+    message.push_back(source_chain);
+    message.append(payload);
+    message
+}
+
+ + + +
+ + + +## Function `create_token_bridge_message` + +Token Transfer Message Format: +[message_type: u8] +[version:u8] +[nonce:u64] +[source_chain: u8] +[sender_address_length:u8] +[sender_address: byte[]] +[target_chain:u8] +[target_address_length:u8] +[target_address: byte[]] +[token_type:u8] +[amount:u64] + + +
public fun create_token_bridge_message(source_chain: u8, seq_num: u64, sender_address: vector<u8>, target_chain: u8, target_address: vector<u8>, token_type: u8, amount: u64): message::BridgeMessage
+
+ + + +
+Implementation + + +
public fun create_token_bridge_message(
+    source_chain: u8,
+    seq_num: u64,
+    sender_address: vector<u8>,
+    target_chain: u8,
+    target_address: vector<u8>,
+    token_type: u8,
+    amount: u64
+): BridgeMessage {
+    // TODO: add test case for invalid chain id
+    // TODO: add test case for invalid chain id
+    // TODO: replace with `chain_ids::is_valid_chain_id()`
+    chain_ids::assert_valid_chain_id(source_chain);
+    chain_ids::assert_valid_chain_id(target_chain);
+
+    let mut payload = vector[];
+
+    // sender address should be less than 255 bytes so can fit into u8
+    payload.push_back((vector::length(&sender_address) as u8));
+    payload.append(sender_address);
+    payload.push_back(target_chain);
+    // target address should be less than 255 bytes so can fit into u8
+    payload.push_back((vector::length(&target_address) as u8));
+    payload.append(target_address);
+    payload.push_back(token_type);
+    // bcs serialzies u64 as 8 bytes
+    payload.append(reverse_bytes(bcs::to_bytes(&amount)));
+
+    assert!(vector::length(&payload) == 64, EInvalidPayloadLength);
+
+    BridgeMessage {
+        message_type: message_types::token(),
+        message_version: CURRENT_MESSAGE_VERSION,
+        seq_num,
+        source_chain,
+        payload,
+    }
+}
+
+ + + +
+ + + +## Function `create_emergency_op_message` + +Emergency Op Message Format: +[message_type: u8] +[version:u8] +[nonce:u64] +[chain_id: u8] +[op_type: u8] + + +
public fun create_emergency_op_message(source_chain: u8, seq_num: u64, op_type: u8): message::BridgeMessage
+
+ + + +
+Implementation + + +
public fun create_emergency_op_message(
+    source_chain: u8,
+    seq_num: u64,
+    op_type: u8,
+): BridgeMessage {
+    // TODO: add test case for invalid chain id
+    // TODO: replace with `chain_ids::is_valid_chain_id()`
+    chain_ids::assert_valid_chain_id(source_chain);
+
+    BridgeMessage {
+        message_type: message_types::emergency_op(),
+        message_version: CURRENT_MESSAGE_VERSION,
+        seq_num,
+        source_chain,
+        payload: vector[op_type],
+    }
+}
+
+ + + +
+ + + +## Function `create_blocklist_message` + +Blocklist Message Format: +[message_type: u8] +[version:u8] +[nonce:u64] +[chain_id: u8] +[blocklist_type: u8] +[validator_length: u8] +[validator_ecdsa_addresses: byte[][]] + + +
public fun create_blocklist_message(source_chain: u8, seq_num: u64, blocklist_type: u8, validator_ecdsa_addresses: vector<vector<u8>>): message::BridgeMessage
+
+ + + +
+Implementation + + +
public fun create_blocklist_message(
+    source_chain: u8,
+    seq_num: u64,
+    // 0: block, 1: unblock
+    blocklist_type: u8,
+    validator_ecdsa_addresses: vector<vector<u8>>,
+): BridgeMessage {
+    // TODO: add test case for invalid chain id
+    // TODO: replace with `chain_ids::is_valid_chain_id()`
+    chain_ids::assert_valid_chain_id(source_chain);
+
+    let address_length = validator_ecdsa_addresses.length();
+    let mut payload = vector[blocklist_type, (address_length as u8)];
+    let mut i = 0;
+
+    while (i < address_length) {
+        let address = validator_ecdsa_addresses[i];
+        assert!(address.length() == ECDSA_ADDRESS_LENGTH, EInvalidAddressLength);
+        payload.append(address);
+
+        i = i + 1;
+    };
+
+    BridgeMessage {
+        message_type: message_types::committee_blocklist(),
+        message_version: CURRENT_MESSAGE_VERSION,
+        seq_num,
+        source_chain,
+        payload,
+    }
+}
+
+ + + +
+ + + +## Function `create_update_bridge_limit_message` + +Update bridge limit Message Format: +[message_type: u8] +[version:u8] +[nonce:u64] +[receiving_chain_id: u8] +[sending_chain_id: u8] +[new_limit: u64] + + +
public fun create_update_bridge_limit_message(receiving_chain: u8, seq_num: u64, sending_chain: u8, new_limit: u64): message::BridgeMessage
+
+ + + +
+Implementation + + +
public fun create_update_bridge_limit_message(
+    receiving_chain: u8,
+    seq_num: u64,
+    sending_chain: u8,
+    new_limit: u64,
+): BridgeMessage {
+    // TODO: add test case for invalid chain id
+    // TODO: add test case for invalid chain id
+    // TODO: replace with `chain_ids::is_valid_chain_id()`
+    chain_ids::assert_valid_chain_id(receiving_chain);
+    chain_ids::assert_valid_chain_id(sending_chain);
+
+    let mut payload = vector[sending_chain];
+    payload.append(reverse_bytes(bcs::to_bytes(&new_limit)));
+
+    BridgeMessage {
+        message_type: message_types::update_bridge_limit(),
+        message_version: CURRENT_MESSAGE_VERSION,
+        seq_num,
+        source_chain: receiving_chain,
+        payload,
+    }
+}
+
+ + + +
+ + + +## Function `create_update_asset_price_message` + +Update asset price message +[message_type: u8] +[version:u8] +[nonce:u64] +[chain_id: u8] +[token_id: u8] +[new_price:u64] + + +
public fun create_update_asset_price_message(token_id: u8, source_chain: u8, seq_num: u64, new_price: u64): message::BridgeMessage
+
+ + + +
+Implementation + + +
public fun create_update_asset_price_message(
+    token_id: u8,
+    source_chain: u8,
+    seq_num: u64,
+    new_price: u64,
+): BridgeMessage {
+    // TODO: add test case for invalid chain id
+    // TODO: replace with `chain_ids::is_valid_chain_id()`
+    chain_ids::assert_valid_chain_id(source_chain);
+
+    let mut payload = vector[token_id];
+    payload.append(reverse_bytes(bcs::to_bytes(&new_price)));
+    BridgeMessage {
+        message_type: message_types::update_asset_price(),
+        message_version: CURRENT_MESSAGE_VERSION,
+        seq_num,
+        source_chain,
+        payload,
+    }
+}
+
+ + + +
+ + + +## Function `create_add_tokens_on_sui_message` + +Update Sui token message +[message_type:u8] +[version:u8] +[nonce:u64] +[chain_id: u8] +[native_token:bool] +[token_ids:vector] +[token_type_name:vector] +[token_prices:vector] + + +
public fun create_add_tokens_on_sui_message(source_chain: u8, seq_num: u64, native_token: bool, token_ids: vector<u8>, type_names: vector<ascii::String>, token_prices: vector<u64>): message::BridgeMessage
+
+ + + +
+Implementation + + +
public fun create_add_tokens_on_sui_message(
+    source_chain: u8,
+    seq_num: u64,
+    native_token: bool,
+    token_ids: vector<u8>,
+    type_names: vector<String>,
+    token_prices: vector<u64>,
+): BridgeMessage {
+    chain_ids::assert_valid_chain_id(source_chain);
+    let mut payload = bcs::to_bytes(&native_token);
+    payload.append(bcs::to_bytes(&token_ids));
+    payload.append(bcs::to_bytes(&type_names));
+    payload.append(bcs::to_bytes(&token_prices));
+    BridgeMessage {
+        message_type: message_types::add_tokens_on_sui(),
+        message_version: CURRENT_MESSAGE_VERSION,
+        seq_num,
+        source_chain,
+        payload,
+    }
+}
+
+ + + +
+ + + +## Function `create_key` + + + +
public fun create_key(source_chain: u8, message_type: u8, bridge_seq_num: u64): message::BridgeMessageKey
+
+ + + +
+Implementation + + +
public fun create_key(source_chain: u8, message_type: u8, bridge_seq_num: u64): BridgeMessageKey {
+    BridgeMessageKey { source_chain, message_type, bridge_seq_num }
+}
+
+ + + +
+ + + +## Function `key` + + + +
public fun key(self: &message::BridgeMessage): message::BridgeMessageKey
+
+ + + +
+Implementation + + +
public fun key(self: &BridgeMessage): BridgeMessageKey {
+    create_key(self.source_chain, self.message_type, self.seq_num)
+}
+
+ + + +
+ + + +## Function `message_version` + + + +
public fun message_version(self: &message::BridgeMessage): u8
+
+ + + +
+Implementation + + +
public fun message_version(self: &BridgeMessage): u8 {
+    self.message_version
+}
+
+ + + +
+ + + +## Function `message_type` + + + +
public fun message_type(self: &message::BridgeMessage): u8
+
+ + + +
+Implementation + + +
public fun message_type(self: &BridgeMessage): u8 {
+    self.message_type
+}
+
+ + + +
+ + + +## Function `seq_num` + + + +
public fun seq_num(self: &message::BridgeMessage): u64
+
+ + + +
+Implementation + + +
public fun seq_num(self: &BridgeMessage): u64 {
+    self.seq_num
+}
+
+ + + +
+ + + +## Function `source_chain` + + + +
public fun source_chain(self: &message::BridgeMessage): u8
+
+ + + +
+Implementation + + +
public fun source_chain(self: &BridgeMessage): u8 {
+    self.source_chain
+}
+
+ + + +
+ + + +## Function `token_target_chain` + + + +
public fun token_target_chain(self: &message::TokenPayload): u8
+
+ + + +
+Implementation + + +
public fun token_target_chain(self: &TokenPayload): u8 {
+    self.target_chain
+}
+
+ + + +
+ + + +## Function `token_target_address` + + + +
public fun token_target_address(self: &message::TokenPayload): vector<u8>
+
+ + + +
+Implementation + + +
public fun token_target_address(self: &TokenPayload): vector<u8> {
+    self.target_address
+}
+
+ + + +
+ + + +## Function `token_type` + + + +
public fun token_type(self: &message::TokenPayload): u8
+
+ + + +
+Implementation + + +
public fun token_type(self: &TokenPayload): u8 {
+    self.token_type
+}
+
+ + + +
+ + + +## Function `token_amount` + + + +
public fun token_amount(self: &message::TokenPayload): u64
+
+ + + +
+Implementation + + +
public fun token_amount(self: &TokenPayload): u64 {
+    self.amount
+}
+
+ + + +
+ + + +## Function `emergency_op_type` + + + +
public fun emergency_op_type(self: &message::EmergencyOp): u8
+
+ + + +
+Implementation + + +
public fun emergency_op_type(self: &EmergencyOp): u8 {
+    self.op_type
+}
+
+ + + +
+ + + +## Function `blocklist_type` + + + +
public fun blocklist_type(self: &message::Blocklist): u8
+
+ + + +
+Implementation + + +
public fun blocklist_type(self: &Blocklist): u8 {
+    self.blocklist_type
+}
+
+ + + +
+ + + +## Function `blocklist_validator_addresses` + + + +
public fun blocklist_validator_addresses(self: &message::Blocklist): &vector<vector<u8>>
+
+ + + +
+Implementation + + +
public fun blocklist_validator_addresses(self: &Blocklist): &vector<vector<u8>> {
+    &self.validator_eth_addresses
+}
+
+ + + +
+ + + +## Function `update_bridge_limit_payload_sending_chain` + + + +
public fun update_bridge_limit_payload_sending_chain(self: &message::UpdateBridgeLimit): u8
+
+ + + +
+Implementation + + +
public fun update_bridge_limit_payload_sending_chain(self: &UpdateBridgeLimit): u8 {
+    self.sending_chain
+}
+
+ + + +
+ + + +## Function `update_bridge_limit_payload_receiving_chain` + + + +
public fun update_bridge_limit_payload_receiving_chain(self: &message::UpdateBridgeLimit): u8
+
+ + + +
+Implementation + + +
public fun update_bridge_limit_payload_receiving_chain(self: &UpdateBridgeLimit): u8 {
+    self.receiving_chain
+}
+
+ + + +
+ + + +## Function `update_bridge_limit_payload_limit` + + + +
public fun update_bridge_limit_payload_limit(self: &message::UpdateBridgeLimit): u64
+
+ + + +
+Implementation + + +
public fun update_bridge_limit_payload_limit(self: &UpdateBridgeLimit): u64 {
+    self.limit
+}
+
+ + + +
+ + + +## Function `update_asset_price_payload_token_id` + + + +
public fun update_asset_price_payload_token_id(self: &message::UpdateAssetPrice): u8
+
+ + + +
+Implementation + + +
public fun update_asset_price_payload_token_id(self: &UpdateAssetPrice): u8 {
+    self.token_id
+}
+
+ + + +
+ + + +## Function `update_asset_price_payload_new_price` + + + +
public fun update_asset_price_payload_new_price(self: &message::UpdateAssetPrice): u64
+
+ + + +
+Implementation + + +
public fun update_asset_price_payload_new_price(self: &UpdateAssetPrice): u64 {
+    self.new_price
+}
+
+ + + +
+ + + +## Function `is_native` + + + +
public fun is_native(self: &message::AddTokenOnSui): bool
+
+ + + +
+Implementation + + +
public fun is_native(self: &AddTokenOnSui): bool {
+    self.native_token
+}
+
+ + + +
+ + + +## Function `token_ids` + + + +
public fun token_ids(self: &message::AddTokenOnSui): vector<u8>
+
+ + + +
+Implementation + + +
public fun token_ids(self: &AddTokenOnSui): vector<u8> {
+    self.token_ids
+}
+
+ + + +
+ + + +## Function `token_type_names` + + + +
public fun token_type_names(self: &message::AddTokenOnSui): vector<ascii::String>
+
+ + + +
+Implementation + + +
public fun token_type_names(self: &AddTokenOnSui): vector<String> {
+    self.token_type_names
+}
+
+ + + +
+ + + +## Function `token_prices` + + + +
public fun token_prices(self: &message::AddTokenOnSui): vector<u64>
+
+ + + +
+Implementation + + +
public fun token_prices(self: &AddTokenOnSui): vector<u64> {
+    self.token_prices
+}
+
+ + + +
+ + + +## Function `emergency_op_pause` + + + +
public fun emergency_op_pause(): u8
+
+ + + +
+Implementation + + +
public fun emergency_op_pause(): u8 {
+    PAUSE
+}
+
+ + + +
+ + + +## Function `emergency_op_unpause` + + + +
public fun emergency_op_unpause(): u8
+
+ + + +
+Implementation + + +
public fun emergency_op_unpause(): u8 {
+    UNPAUSE
+}
+
+ + + +
+ + + +## Function `required_voting_power` + +Return the required signature threshold for the message, values are voting power in the scale of 10000 + + +
public fun required_voting_power(self: &message::BridgeMessage): u64
+
+ + + +
+Implementation + + +
public fun required_voting_power(self: &BridgeMessage): u64 {
+    let message_type = message_type(self);
+
+    if (message_type == message_types::token()) {
+        3334
+    } else if (message_type == message_types::emergency_op()) {
+        let payload = extract_emergency_op_payload(self);
+        if (payload.op_type == PAUSE) {
+            450
+        } else if (payload.op_type == UNPAUSE) {
+            5001
+        } else {
+            abort EInvalidEmergencyOpType
+        }
+    } else if (message_type == message_types::committee_blocklist()) {
+        5001
+    } else if (message_type == message_types::update_asset_price()) {
+        5001
+    } else if (message_type == message_types::update_bridge_limit()) {
+        5001
+    } else if (message_type == message_types::add_tokens_on_sui()) {
+        5001
+    } else {
+        abort EInvalidMessageType
+    }
+}
+
+ + + +
+ + + +## Function `reverse_bytes` + + + +
fun reverse_bytes(bytes: vector<u8>): vector<u8>
+
+ + + +
+Implementation + + +
fun reverse_bytes(mut bytes: vector<u8>): vector<u8> {
+    vector::reverse(&mut bytes);
+    bytes
+}
+
+ + + +
+ + + +## Function `peel_u64_be` + + + +
fun peel_u64_be(bcs: &mut bcs::BCS): u64
+
+ + + +
+Implementation + + +
fun peel_u64_be(bcs: &mut BCS): u64 {
+    let (mut value, mut i) = (0u64, 64u8);
+    while (i > 0) {
+        i = i - 8;
+        let byte = (bcs::peel_u8(bcs) as u64);
+        value = value + (byte << i);
+    };
+    value
+}
+
+ + + +
diff --git a/crates/sui-framework/docs/bridge/message_types.md b/crates/sui-framework/docs/bridge/message_types.md new file mode 100644 index 0000000000000..0b9ba315209c0 --- /dev/null +++ b/crates/sui-framework/docs/bridge/message_types.md @@ -0,0 +1,209 @@ +--- +title: Module `0xb::message_types` +--- + + + +- [Constants](#@Constants_0) +- [Function `token`](#0xb_message_types_token) +- [Function `committee_blocklist`](#0xb_message_types_committee_blocklist) +- [Function `emergency_op`](#0xb_message_types_emergency_op) +- [Function `update_bridge_limit`](#0xb_message_types_update_bridge_limit) +- [Function `update_asset_price`](#0xb_message_types_update_asset_price) +- [Function `add_tokens_on_sui`](#0xb_message_types_add_tokens_on_sui) + + +
+ + + + + +## Constants + + + + + + +
const ADD_TOKENS_ON_SUI: u8 = 6;
+
+ + + + + + + +
const COMMITTEE_BLOCKLIST: u8 = 1;
+
+ + + + + + + +
const EMERGENCY_OP: u8 = 2;
+
+ + + + + + + +
const TOKEN: u8 = 0;
+
+ + + + + + + +
const UPDATE_ASSET_PRICE: u8 = 4;
+
+ + + + + + + +
const UPDATE_BRIDGE_LIMIT: u8 = 3;
+
+ + + + + +## Function `token` + + + +
public fun token(): u8
+
+ + + +
+Implementation + + +
public fun token(): u8 { TOKEN }
+
+ + + +
+ + + +## Function `committee_blocklist` + + + +
public fun committee_blocklist(): u8
+
+ + + +
+Implementation + + +
public fun committee_blocklist(): u8 { COMMITTEE_BLOCKLIST }
+
+ + + +
+ + + +## Function `emergency_op` + + + +
public fun emergency_op(): u8
+
+ + + +
+Implementation + + +
public fun emergency_op(): u8 { EMERGENCY_OP }
+
+ + + +
+ + + +## Function `update_bridge_limit` + + + +
public fun update_bridge_limit(): u8
+
+ + + +
+Implementation + + +
public fun update_bridge_limit(): u8 { UPDATE_BRIDGE_LIMIT }
+
+ + + +
+ + + +## Function `update_asset_price` + + + +
public fun update_asset_price(): u8
+
+ + + +
+Implementation + + +
public fun update_asset_price(): u8 { UPDATE_ASSET_PRICE }
+
+ + + +
+ + + +## Function `add_tokens_on_sui` + + + +
public fun add_tokens_on_sui(): u8
+
+ + + +
+Implementation + + +
public fun add_tokens_on_sui(): u8 { ADD_TOKENS_ON_SUI }
+
+ + + +
diff --git a/crates/sui-framework/docs/bridge/treasury.md b/crates/sui-framework/docs/bridge/treasury.md new file mode 100644 index 0000000000000..2b16080b9f608 --- /dev/null +++ b/crates/sui-framework/docs/bridge/treasury.md @@ -0,0 +1,672 @@ +--- +title: Module `0xb::treasury` +--- + + + +- [Struct `BridgeTreasury`](#0xb_treasury_BridgeTreasury) +- [Struct `BridgeTokenMetadata`](#0xb_treasury_BridgeTokenMetadata) +- [Struct `ForeignTokenRegistration`](#0xb_treasury_ForeignTokenRegistration) +- [Struct `UpdateTokenPriceEvent`](#0xb_treasury_UpdateTokenPriceEvent) +- [Struct `NewTokenEvent`](#0xb_treasury_NewTokenEvent) +- [Struct `TokenRegistrationEvent`](#0xb_treasury_TokenRegistrationEvent) +- [Constants](#@Constants_0) +- [Function `token_id`](#0xb_treasury_token_id) +- [Function `decimal_multiplier`](#0xb_treasury_decimal_multiplier) +- [Function `notional_value`](#0xb_treasury_notional_value) +- [Function `register_foreign_token`](#0xb_treasury_register_foreign_token) +- [Function `add_new_token`](#0xb_treasury_add_new_token) +- [Function `create`](#0xb_treasury_create) +- [Function `burn`](#0xb_treasury_burn) +- [Function `mint`](#0xb_treasury_mint) +- [Function `update_asset_notional_price`](#0xb_treasury_update_asset_notional_price) +- [Function `get_token_metadata`](#0xb_treasury_get_token_metadata) + + +
use 0x1::ascii;
+use 0x1::option;
+use 0x1::type_name;
+use 0x2::address;
+use 0x2::bag;
+use 0x2::coin;
+use 0x2::event;
+use 0x2::hex;
+use 0x2::math;
+use 0x2::object;
+use 0x2::object_bag;
+use 0x2::package;
+use 0x2::transfer;
+use 0x2::tx_context;
+use 0x2::vec_map;
+
+ + + + + +## Struct `BridgeTreasury` + + + +
struct BridgeTreasury has store
+
+ + + +
+Fields + + +
+
+treasuries: object_bag::ObjectBag +
+
+ +
+
+supported_tokens: vec_map::VecMap<type_name::TypeName, treasury::BridgeTokenMetadata> +
+
+ +
+
+id_token_type_map: vec_map::VecMap<u8, type_name::TypeName> +
+
+ +
+
+waiting_room: bag::Bag +
+
+ +
+
+ + +
+ + + +## Struct `BridgeTokenMetadata` + + + +
struct BridgeTokenMetadata has copy, drop, store
+
+ + + +
+Fields + + +
+
+id: u8 +
+
+ +
+
+decimal_multiplier: u64 +
+
+ +
+
+notional_value: u64 +
+
+ +
+
+native_token: bool +
+
+ +
+
+ + +
+ + + +## Struct `ForeignTokenRegistration` + + + +
struct ForeignTokenRegistration has store
+
+ + + +
+Fields + + +
+
+type_name: type_name::TypeName +
+
+ +
+
+uc: package::UpgradeCap +
+
+ +
+
+decimal: u8 +
+
+ +
+
+ + +
+ + + +## Struct `UpdateTokenPriceEvent` + + + +
struct UpdateTokenPriceEvent has copy, drop
+
+ + + +
+Fields + + +
+
+token_id: u8 +
+
+ +
+
+new_price: u64 +
+
+ +
+
+ + +
+ + + +## Struct `NewTokenEvent` + + + +
struct NewTokenEvent has copy, drop
+
+ + + +
+Fields + + +
+
+token_id: u8 +
+
+ +
+
+type_name: type_name::TypeName +
+
+ +
+
+native_token: bool +
+
+ +
+
+decimal_multiplier: u64 +
+
+ +
+
+notional_value: u64 +
+
+ +
+
+ + +
+ + + +## Struct `TokenRegistrationEvent` + + + +
struct TokenRegistrationEvent has copy, drop
+
+ + + +
+Fields + + +
+
+type_name: type_name::TypeName +
+
+ +
+
+decimal: u8 +
+
+ +
+
+native_token: bool +
+
+ +
+
+ + +
+ + + +## Constants + + + + + + +
const EInvalidNotionalValue: u64 = 4;
+
+ + + + + + + +
const EInvalidUpgradeCap: u64 = 2;
+
+ + + + + + + +
const ETokenSupplyNonZero: u64 = 3;
+
+ + + + + + + +
const EUnsupportedTokenType: u64 = 1;
+
+ + + + + +## Function `token_id` + + + +
public fun token_id<T>(self: &treasury::BridgeTreasury): u8
+
+ + + +
+Implementation + + +
public fun token_id<T>(self: &BridgeTreasury): u8 {
+    let metadata = self.get_token_metadata<T>();
+    metadata.id
+}
+
+ + + +
+ + + +## Function `decimal_multiplier` + + + +
public fun decimal_multiplier<T>(self: &treasury::BridgeTreasury): u64
+
+ + + +
+Implementation + + +
public fun decimal_multiplier<T>(self: &BridgeTreasury): u64 {
+    let metadata = self.get_token_metadata<T>();
+    metadata.decimal_multiplier
+}
+
+ + + +
+ + + +## Function `notional_value` + + + +
public fun notional_value<T>(self: &treasury::BridgeTreasury): u64
+
+ + + +
+Implementation + + +
public fun notional_value<T>(self: &BridgeTreasury): u64 {
+    let metadata = self.get_token_metadata<T>();
+    metadata.notional_value
+}
+
+ + + +
+ + + +## Function `register_foreign_token` + + + +
public(friend) fun register_foreign_token<T>(self: &mut treasury::BridgeTreasury, tc: coin::TreasuryCap<T>, uc: package::UpgradeCap, metadata: &coin::CoinMetadata<T>)
+
+ + + +
+Implementation + + +
public(package) fun register_foreign_token<T>(
+    self: &mut BridgeTreasury,
+    tc: TreasuryCap<T>,
+    uc: UpgradeCap,
+    metadata: &CoinMetadata<T>,
+) {
+    // Make sure TreasuryCap has not been minted before.
+    assert!(coin::total_supply(&tc) == 0, ETokenSupplyNonZero);
+    let type_name = type_name::get<T>();
+    let address_bytes = hex::decode(ascii::into_bytes(type_name::get_address(&type_name)));
+    let coin_address = address::from_bytes(address_bytes);
+    // Make sure upgrade cap is for the Coin package
+    // FIXME: add test
+    assert!(
+        object::id_to_address(&package::upgrade_package(&uc))
+            == coin_address, EInvalidUpgradeCap
+    );
+    let registration = ForeignTokenRegistration {
+        type_name,
+        uc,
+        decimal: coin::get_decimals(metadata),
+    };
+    self.waiting_room.add(type_name::into_string(type_name), registration);
+    self.treasuries.add(type_name, tc);
+
+    emit(TokenRegistrationEvent{
+        type_name,
+        decimal: coin::get_decimals(metadata),
+        native_token: false
+    });
+}
+
+ + + +
+ + + +## Function `add_new_token` + + + +
public(friend) fun add_new_token(self: &mut treasury::BridgeTreasury, token_name: ascii::String, token_id: u8, native_token: bool, notional_value: u64)
+
+ + + +
+Implementation + + +
public(package) fun add_new_token(
+    self: &mut BridgeTreasury,
+    token_name: String,
+    token_id:u8,
+    native_token: bool,
+    notional_value: u64,
+) {
+    if (!native_token){
+        assert!(notional_value > 0, EInvalidNotionalValue);
+        let ForeignTokenRegistration{
+            type_name,
+            uc,
+            decimal,
+        } = bag::remove<String, ForeignTokenRegistration>(&mut self.waiting_room, token_name);
+        let decimal_multiplier = math::pow(10, decimal);
+        self.supported_tokens.insert(
+            type_name,
+            BridgeTokenMetadata{
+                id: token_id,
+                decimal_multiplier,
+                notional_value,
+                native_token
+            },
+        );
+        self.id_token_type_map.insert(token_id, type_name);
+
+        // Freeze upgrade cap to prevent changes to the coin
+        transfer::public_freeze_object(uc);
+
+        emit(NewTokenEvent{
+            token_id,
+            type_name,
+            native_token,
+            decimal_multiplier,
+            notional_value
+        })
+    } else {
+        // Not implemented for V1
+    }
+}
+
+ + + +
+ + + +## Function `create` + + + +
public(friend) fun create(ctx: &mut tx_context::TxContext): treasury::BridgeTreasury
+
+ + + +
+Implementation + + +
public(package) fun create(ctx: &mut TxContext): BridgeTreasury {
+    BridgeTreasury {
+        treasuries: object_bag::new(ctx),
+        supported_tokens: vec_map::empty(),
+        id_token_type_map: vec_map::empty(),
+        waiting_room: bag::new(ctx),
+    }
+}
+
+ + + +
+ + + +## Function `burn` + + + +
public(friend) fun burn<T>(self: &mut treasury::BridgeTreasury, token: coin::Coin<T>)
+
+ + + +
+Implementation + + +
public(package) fun burn<T>(self: &mut BridgeTreasury, token: Coin<T>) {
+    let treasury = &mut self.treasuries[type_name::get<T>()];
+    coin::burn(treasury, token);
+}
+
+ + + +
+ + + +## Function `mint` + + + +
public(friend) fun mint<T>(self: &mut treasury::BridgeTreasury, amount: u64, ctx: &mut tx_context::TxContext): coin::Coin<T>
+
+ + + +
+Implementation + + +
public(package) fun mint<T>(
+    self: &mut BridgeTreasury,
+    amount: u64,
+    ctx: &mut TxContext,
+): Coin<T> {
+    let treasury = &mut self.treasuries[type_name::get<T>()];
+    coin::mint(treasury, amount, ctx)
+}
+
+ + + +
+ + + +## Function `update_asset_notional_price` + + + +
public(friend) fun update_asset_notional_price(self: &mut treasury::BridgeTreasury, token_id: u8, new_usd_price: u64)
+
+ + + +
+Implementation + + +
public(package) fun update_asset_notional_price(
+    self: &mut BridgeTreasury,
+    token_id: u8,
+    new_usd_price: u64,
+) {
+    let type_name = self.id_token_type_map.try_get(&token_id);
+    assert!(type_name.is_some(), EUnsupportedTokenType);
+    assert!(new_usd_price > 0, EInvalidNotionalValue);
+    let type_name = type_name.destroy_some();
+    let metadata = self.supported_tokens.get_mut(&type_name);
+    metadata.notional_value = new_usd_price;
+
+    emit(UpdateTokenPriceEvent {
+        token_id,
+        new_price: new_usd_price,
+    })
+}
+
+ + + +
+ + + +## Function `get_token_metadata` + + + +
fun get_token_metadata<T>(self: &treasury::BridgeTreasury): treasury::BridgeTokenMetadata
+
+ + + +
+Implementation + + +
fun get_token_metadata<T>(self: &BridgeTreasury): BridgeTokenMetadata {
+    let coin_type = type_name::get<T>();
+    let metadata = self.supported_tokens.try_get(&coin_type);
+    assert!(metadata.is_some(), EUnsupportedTokenType);
+    metadata.destroy_some()
+}
+
+ + + +
diff --git a/crates/sui-framework/docs/sui-framework/object.md b/crates/sui-framework/docs/sui-framework/object.md index 7bebc36eef2ee..924198167761e 100644 --- a/crates/sui-framework/docs/sui-framework/object.md +++ b/crates/sui-framework/docs/sui-framework/object.md @@ -17,6 +17,7 @@ Sui object identifiers - [Function `authenticator_state`](#0x2_object_authenticator_state) - [Function `randomness_state`](#0x2_object_randomness_state) - [Function `sui_deny_list_object_id`](#0x2_object_sui_deny_list_object_id) +- [Function `bridge`](#0x2_object_bridge) - [Function `uid_as_inner`](#0x2_object_uid_as_inner) - [Function `uid_to_inner`](#0x2_object_uid_to_inner) - [Function `uid_to_bytes`](#0x2_object_uid_to_bytes) @@ -131,6 +132,16 @@ The hardcoded ID for the singleton AuthenticatorState Object. + + +The hardcoded ID for the Bridge Object. + + +
const SUI_BRIDGE_ID: address = 9;
+
+ + + The hardcoded ID for the singleton Clock Object. @@ -410,6 +421,34 @@ This should only be called once from + +## Function `bridge` + +Create the UID for the singleton Bridge object. +This should only be called once from bridge. + + +
fun bridge(): object::UID
+
+ + + +
+Implementation + + +
fun bridge(): UID {
+    UID {
+        id: ID { bytes: SUI_BRIDGE_ID }
+    }
+}
+
+ + +
diff --git a/crates/sui-framework/docs/sui-system/sui_system.md b/crates/sui-framework/docs/sui-system/sui_system.md index cd17b3e160ab1..2c961095d9bae 100644 --- a/crates/sui-framework/docs/sui-system/sui_system.md +++ b/crates/sui-framework/docs/sui-system/sui_system.md @@ -83,6 +83,7 @@ the SuiSystemStateInner version, or vice versa. - [Function `load_system_state`](#0x3_sui_system_load_system_state) - [Function `load_system_state_mut`](#0x3_sui_system_load_system_state_mut) - [Function `load_inner_maybe_upgrade`](#0x3_sui_system_load_inner_maybe_upgrade) +- [Function `validator_voting_powers`](#0x3_sui_system_validator_voting_powers)
use 0x1::option;
@@ -94,6 +95,7 @@ the SuiSystemStateInner version, or vice versa.
 use 0x2::table;
 use 0x2::transfer;
 use 0x2::tx_context;
+use 0x2::vec_map;
 use 0x3::stake_subsidy;
 use 0x3::staking_pool;
 use 0x3::sui_system_state_inner;
@@ -1489,4 +1491,30 @@ gas coins.
 
 
 
+
+
+
+
+## Function `validator_voting_powers`
+
+Returns the voting power of the active validators, values are voting power in the scale of 10000.
+
+
+
fun validator_voting_powers(wrapper: &mut sui_system::SuiSystemState): vec_map::VecMap<address, u64>
+
+ + + +
+Implementation + + +
fun validator_voting_powers(wrapper: &mut SuiSystemState): VecMap<address, u64> {
+    let self = load_system_state(wrapper);
+    sui_system_state_inner::active_validator_voting_powers(self)
+}
+
+ + +
diff --git a/crates/sui-framework/docs/sui-system/sui_system_state_inner.md b/crates/sui-framework/docs/sui-system/sui_system_state_inner.md index 100edbd3a78dd..d5f6b84d2efcc 100644 --- a/crates/sui-framework/docs/sui-system/sui_system_state_inner.md +++ b/crates/sui-framework/docs/sui-system/sui_system_state_inner.md @@ -54,6 +54,7 @@ title: Module `0x3::sui_system_state_inner` - [Function `genesis_system_state_version`](#0x3_sui_system_state_inner_genesis_system_state_version) - [Function `epoch_start_timestamp_ms`](#0x3_sui_system_state_inner_epoch_start_timestamp_ms) - [Function `validator_stake_amount`](#0x3_sui_system_state_inner_validator_stake_amount) +- [Function `active_validator_voting_powers`](#0x3_sui_system_state_inner_active_validator_voting_powers) - [Function `validator_staking_pool_id`](#0x3_sui_system_state_inner_validator_staking_pool_id) - [Function `validator_staking_pool_mappings`](#0x3_sui_system_state_inner_validator_staking_pool_mappings) - [Function `get_reporters_of`](#0x3_sui_system_state_inner_get_reporters_of) @@ -65,6 +66,7 @@ title: Module `0x3::sui_system_state_inner`
use 0x1::option;
+use 0x1::vector;
 use 0x2::bag;
 use 0x2::balance;
 use 0x2::coin;
@@ -2368,6 +2370,39 @@ Aborts if validator_addr is not an active validator.
 
 
 
+
+
+
+
+## Function `active_validator_voting_powers`
+
+Returns the voting power for validator_addr.
+Aborts if validator_addr is not an active validator.
+
+
+
public(friend) fun active_validator_voting_powers(self: &sui_system_state_inner::SuiSystemStateInnerV2): vec_map::VecMap<address, u64>
+
+ + + +
+Implementation + + +
public(package) fun active_validator_voting_powers(self: &SuiSystemStateInnerV2): VecMap<address, u64> {
+    let mut active_validators = active_validator_addresses(self);
+    let mut voting_powers = vec_map::empty();
+    while (!vector::is_empty(&active_validators)) {
+        let validator = vector::pop_back(&mut active_validators);
+        let voting_power = validator_set::validator_voting_power(&self.validators, validator);
+        vec_map::insert(&mut voting_powers, validator, voting_power);
+    };
+    voting_powers
+}
+
+ + +
diff --git a/crates/sui-framework/docs/sui-system/validator_set.md b/crates/sui-framework/docs/sui-system/validator_set.md index 18abc209f90e2..62abe399d49b5 100644 --- a/crates/sui-framework/docs/sui-system/validator_set.md +++ b/crates/sui-framework/docs/sui-system/validator_set.md @@ -26,6 +26,7 @@ title: Module `0x3::validator_set` - [Function `total_stake`](#0x3_validator_set_total_stake) - [Function `validator_total_stake_amount`](#0x3_validator_set_validator_total_stake_amount) - [Function `validator_stake_amount`](#0x3_validator_set_validator_stake_amount) +- [Function `validator_voting_power`](#0x3_validator_set_validator_voting_power) - [Function `validator_staking_pool_id`](#0x3_validator_set_validator_staking_pool_id) - [Function `staking_pool_mappings`](#0x3_validator_set_staking_pool_mappings) - [Function `pool_exchange_rates`](#0x3_validator_set_pool_exchange_rates) @@ -1324,6 +1325,31 @@ gas price, weighted by stake. + + + + +## Function `validator_voting_power` + + + +
public fun validator_voting_power(self: &validator_set::ValidatorSet, validator_address: address): u64
+
+ + + +
+Implementation + + +
public fun validator_voting_power(self: &ValidatorSet, validator_address: address): u64 {
+    let validator = get_validator_ref(&self.active_validators, validator_address);
+    validator.voting_power()
+}
+
+ + +
diff --git a/crates/sui-framework/packages/bridge/Move.toml b/crates/sui-framework/packages/bridge/Move.toml new file mode 100644 index 0000000000000..33643047e127f --- /dev/null +++ b/crates/sui-framework/packages/bridge/Move.toml @@ -0,0 +1,14 @@ +[package] +name = "Bridge" +version = "0.0.1" +published-at = "0xb" +edition = "2024.beta" + +[dependencies] +MoveStdlib = { local = "../move-stdlib" } +Sui = { local = "../sui-framework" } +SuiSystem = { local = "../sui-system" } + +[addresses] +bridge = "0xb" + diff --git a/crates/sui-framework/packages/bridge/sources/bridge.move b/crates/sui-framework/packages/bridge/sources/bridge.move new file mode 100644 index 0000000000000..a07e1b1faff26 --- /dev/null +++ b/crates/sui-framework/packages/bridge/sources/bridge.move @@ -0,0 +1,773 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module bridge::bridge { + use sui::address; + use sui::clock::Clock; + use sui::coin::{Coin, TreasuryCap, CoinMetadata}; + use sui::event::emit; + use sui::linked_table::{Self, LinkedTable}; + use sui::package::UpgradeCap; + use sui::vec_map::{Self, VecMap}; + use sui::versioned::{Self, Versioned}; + use sui_system::sui_system::SuiSystemState; + + use bridge::chain_ids; + use bridge::committee::{Self, BridgeCommittee}; + use bridge::limiter::{Self, TransferLimiter}; + use bridge::message::{ + Self, BridgeMessage, BridgeMessageKey, EmergencyOp, UpdateAssetPrice, + UpdateBridgeLimit, AddTokenOnSui + }; + use bridge::message_types; + use bridge::treasury::{Self, BridgeTreasury}; + + const MESSAGE_VERSION: u8 = 1; + + // Transfer Status + const TRANSFER_STATUS_PENDING: u8 = 0; + const TRANSFER_STATUS_APPROVED: u8 = 1; + const TRANSFER_STATUS_CLAIMED: u8 = 2; + const TRANSFER_STATUS_NOT_FOUND: u8 = 3; + + ////////////////////////////////////////////////////// + // Types + // + + public struct Bridge has key { + id: UID, + inner: Versioned, + } + + public struct BridgeInner has store { + bridge_version: u64, + message_version: u8, + chain_id: u8, + // nonce for replay protection + // key: message type, value: next sequence number + sequence_nums: VecMap, + // committee + committee: BridgeCommittee, + // Bridge treasury for mint/burn bridged tokens + treasury: BridgeTreasury, + token_transfer_records: LinkedTable, + limiter: TransferLimiter, + paused: bool, + } + + public struct TokenDepositedEvent has copy, drop { + seq_num: u64, + source_chain: u8, + sender_address: vector, + target_chain: u8, + target_address: vector, + token_type: u8, + amount: u64, + } + + public struct EmergencyOpEvent has copy, drop { + frozen: bool, + } + + public struct BridgeRecord has store, drop { + message: BridgeMessage, + verified_signatures: Option>>, + claimed: bool, + } + + const EUnexpectedMessageType: u64 = 0; + const EUnauthorisedClaim: u64 = 1; + const EMalformedMessageError: u64 = 2; + const EUnexpectedTokenType: u64 = 3; + const EUnexpectedChainID: u64 = 4; + const ENotSystemAddress: u64 = 5; + const EUnexpectedSeqNum: u64 = 6; + const EWrongInnerVersion: u64 = 7; + const EBridgeUnavailable: u64 = 8; + const EUnexpectedOperation: u64 = 9; + const EInvariantSuiInitializedTokenTransferShouldNotBeClaimed: u64 = 10; + const EMessageNotFoundInRecords: u64 = 11; + const EUnexpectedMessageVersion: u64 = 12; + const EBridgeAlreadyPaused: u64 = 13; + const EBridgeNotPaused: u64 = 14; + const ETokenAlreadyClaimed: u64 = 15; + const EInvalidBridgeRoute: u64 = 16; + const EMustBeTokenMessage: u64 = 17; + + const CURRENT_VERSION: u64 = 1; + + public struct TokenTransferApproved has copy, drop { + message_key: BridgeMessageKey, + } + + public struct TokenTransferClaimed has copy, drop { + message_key: BridgeMessageKey, + } + + public struct TokenTransferAlreadyApproved has copy, drop { + message_key: BridgeMessageKey, + } + + public struct TokenTransferAlreadyClaimed has copy, drop { + message_key: BridgeMessageKey, + } + + public struct TokenTransferLimitExceed has copy, drop { + message_key: BridgeMessageKey, + } + + ////////////////////////////////////////////////////// + // Internal initialization functions + // + + // this method is called once in end of epoch tx to create the bridge + #[allow(unused_function)] + fun create(id: UID, chain_id: u8, ctx: &mut TxContext) { + assert!(ctx.sender() == @0x0, ENotSystemAddress); + let bridge_inner = BridgeInner { + bridge_version: CURRENT_VERSION, + message_version: MESSAGE_VERSION, + chain_id, + sequence_nums: vec_map::empty(), + committee: committee::create(ctx), + treasury: treasury::create(ctx), + token_transfer_records: linked_table::new(ctx), + limiter: limiter::new(), + paused: false, + }; + let bridge = Bridge { + id, + inner: versioned::create(CURRENT_VERSION, bridge_inner, ctx), + }; + transfer::share_object(bridge); + } + + #[allow(unused_function)] + fun init_bridge_committee( + bridge: &mut Bridge, + active_validator_voting_power: VecMap, + min_stake_participation_percentage: u64, + ctx: &TxContext + ) { + assert!(ctx.sender() == @0x0, ENotSystemAddress); + let inner = load_inner_mut(bridge); + if (inner.committee.committee_members().is_empty()) { + inner.committee.try_create_next_committee( + active_validator_voting_power, + min_stake_participation_percentage, + ctx, + ) + } + } + + ////////////////////////////////////////////////////// + // Public functions + // + + public fun committee_registration( + bridge: &mut Bridge, + system_state: &mut SuiSystemState, + bridge_pubkey_bytes: vector, + http_rest_url: vector, + ctx: &TxContext + ) { + load_inner_mut(bridge) + .committee + .register(system_state, bridge_pubkey_bytes, http_rest_url, ctx); + } + + public fun register_foreign_token( + bridge: &mut Bridge, + tc: TreasuryCap, + uc: UpgradeCap, + metadata: &CoinMetadata, + ) { + load_inner_mut(bridge) + .treasury + .register_foreign_token(tc, uc, metadata) + } + + // Create bridge request to send token to other chain, the request will be in + // pending state until approved + public fun send_token( + bridge: &mut Bridge, + target_chain: u8, + target_address: vector, + token: Coin, + ctx: &mut TxContext + ) { + let inner = load_inner_mut(bridge); + assert!(!inner.paused, EBridgeUnavailable); + assert!(chain_ids::is_valid_route(inner.chain_id, target_chain), EInvalidBridgeRoute); + let amount = token.balance().value(); + + let bridge_seq_num = inner.get_current_seq_num_and_increment(message_types::token()); + let token_id = inner.treasury.token_id(); + let token_amount = token.balance().value(); + + // create bridge message + let message = message::create_token_bridge_message( + inner.chain_id, + bridge_seq_num, + address::to_bytes(ctx.sender()), + target_chain, + target_address, + token_id, + amount, + ); + + // burn / escrow token, unsupported coins will fail in this step + inner.treasury.burn(token); + + // Store pending bridge request + inner.token_transfer_records.push_back( + message.key(), + BridgeRecord { + message, + verified_signatures: option::none(), + claimed: false, + }, + ); + + // emit event + emit( + TokenDepositedEvent { + seq_num: bridge_seq_num, + source_chain: inner.chain_id, + sender_address: address::to_bytes(ctx.sender()), + target_chain, + target_address, + token_type: token_id, + amount: token_amount, + }, + ); + } + + // Record bridge message approvals in Sui, called by the bridge client + // If already approved, return early instead of aborting. + public fun approve_token_transfer( + bridge: &mut Bridge, + message: BridgeMessage, + signatures: vector>, + ) { + let inner = load_inner_mut(bridge); + assert!(!inner.paused, EBridgeUnavailable); + // verify signatures + inner.committee.verify_signatures(message, signatures); + + assert!(message.message_type() == message_types::token(), EMustBeTokenMessage); + assert!(message.message_version() == MESSAGE_VERSION, EUnexpectedMessageVersion); + let token_payload = message.extract_token_bridge_payload(); + let target_chain = token_payload.token_target_chain(); + assert!( + message.source_chain() == inner.chain_id || target_chain == inner.chain_id, + EUnexpectedChainID, + ); + + let message_key = message.key(); + // retrieve pending message if source chain is Sui, the initial message + // must exist on chain + if (message.source_chain() == inner.chain_id) { + let record = &mut inner.token_transfer_records[message_key]; + + assert!(record.message == message, EMalformedMessageError); + assert!(!record.claimed, EInvariantSuiInitializedTokenTransferShouldNotBeClaimed); + + // If record already has verified signatures, it means the message has been approved + // Then we exit early. + if (record.verified_signatures.is_some()) { + emit(TokenTransferAlreadyApproved { message_key }); + return + }; + // Store approval + record.verified_signatures = option::some(signatures) + } else { + // At this point, if this message is in token_transfer_records, we know + // it's already approved because we only add a message to token_transfer_records + // after verifying the signatures + if (inner.token_transfer_records.contains(message_key)) { + emit(TokenTransferAlreadyApproved { message_key }); + return + }; + // Store message and approval + inner.token_transfer_records.push_back( + message_key, + BridgeRecord { + message, + verified_signatures: option::some(signatures), + claimed: false + }, + ); + }; + + emit(TokenTransferApproved { message_key }); + } + + // This function can only be called by the token recipient + // Abort if the token has already been claimed. + public fun claim_token( + bridge: &mut Bridge, + clock: &Clock, + source_chain: u8, + bridge_seq_num: u64, + ctx: &mut TxContext, + ): Coin { + let (maybe_token, owner) = bridge.claim_token_internal( + clock, + source_chain, + bridge_seq_num, + ctx, + ); + // Only token owner can claim the token + assert!(ctx.sender() == owner, EUnauthorisedClaim); + assert!(maybe_token.is_some(), ETokenAlreadyClaimed); + maybe_token.destroy_some() + } + + // This function can be called by anyone to claim and transfer the token to the recipient + // If the token has already been claimed, it will return instead of aborting. + public fun claim_and_transfer_token( + bridge: &mut Bridge, + clock: &Clock, + source_chain: u8, + bridge_seq_num: u64, + ctx: &mut TxContext, + ) { + let (token, owner) = bridge.claim_token_internal(clock, source_chain, bridge_seq_num, ctx); + if (token.is_some()) { + transfer::public_transfer(token.destroy_some(), owner) + } else { + token.destroy_none(); + }; + } + + public fun execute_system_message( + bridge: &mut Bridge, + message: BridgeMessage, + signatures: vector>, + ) { + let message_type = message.message_type(); + + // TODO: test version mismatch + assert!(message.message_version() == MESSAGE_VERSION, EUnexpectedMessageVersion); + let inner = load_inner_mut(bridge); + + assert!(message.source_chain() == inner.chain_id, EUnexpectedChainID); + + // check system ops seq number and increment it + let expected_seq_num = inner.get_current_seq_num_and_increment(message_type); + assert!(message.seq_num() == expected_seq_num, EUnexpectedSeqNum); + + inner.committee.verify_signatures(message, signatures); + + if (message_type == message_types::emergency_op()) { + let payload = message.extract_emergency_op_payload(); + inner.execute_emergency_op(payload); + } else if (message_type == message_types::committee_blocklist()) { + let payload = message.extract_blocklist_payload(); + inner.committee.execute_blocklist(payload); + } else if (message_type == message_types::update_bridge_limit()) { + let payload = message.extract_update_bridge_limit(); + inner.execute_update_bridge_limit(payload); + } else if (message_type == message_types::update_asset_price()) { + let payload = message.extract_update_asset_price(); + inner.execute_update_asset_price(payload); + } else if (message_type == message_types::add_tokens_on_sui()) { + let payload = message.extract_add_tokens_on_sui(); + inner.execute_add_tokens_on_sui(payload); + } else { + abort EUnexpectedMessageType + }; + } + + public fun get_token_transfer_action_status( + bridge: &Bridge, + source_chain: u8, + bridge_seq_num: u64, + ): u8 { + let inner = load_inner(bridge); + let key = message::create_key( + source_chain, + message_types::token(), + bridge_seq_num + ); + + if (!inner.token_transfer_records.contains(key)) { + return TRANSFER_STATUS_NOT_FOUND + }; + + let record = &inner.token_transfer_records[key]; + if (record.claimed) { + return TRANSFER_STATUS_CLAIMED + }; + + if (record.verified_signatures.is_some()) { + return TRANSFER_STATUS_APPROVED + }; + + TRANSFER_STATUS_PENDING + } + + ////////////////////////////////////////////////////// + // Internal functions + // + + fun load_inner( + bridge: &Bridge, + ): &BridgeInner { + let version = bridge.inner.version(); + + // TODO: Replace this with a lazy update function when we add a new version of the inner object. + assert!(version == CURRENT_VERSION, EWrongInnerVersion); + let inner: &BridgeInner = bridge.inner.load_value(); + assert!(inner.bridge_version == version, EWrongInnerVersion); + inner + } + + fun load_inner_mut(bridge: &mut Bridge): &mut BridgeInner { + let version = bridge.inner.version(); + // TODO: Replace this with a lazy update function when we add a new version of the inner object. + assert!(version == CURRENT_VERSION, EWrongInnerVersion); + let inner: &mut BridgeInner = bridge.inner.load_value_mut(); + assert!(inner.bridge_version == version, EWrongInnerVersion); + inner + } + + // Claim token from approved bridge message + // Returns Some(Coin) if coin can be claimed. If already claimed, return None + fun claim_token_internal( + bridge: &mut Bridge, + clock: &Clock, + source_chain: u8, + bridge_seq_num: u64, + ctx: &mut TxContext, + ): (Option>, address) { + let inner = load_inner_mut(bridge); + assert!(!inner.paused, EBridgeUnavailable); + + let key = message::create_key(source_chain, message_types::token(), bridge_seq_num); + assert!(inner.token_transfer_records.contains(key), EMessageNotFoundInRecords); + + // retrieve approved bridge message + let record = &mut inner.token_transfer_records[key]; + // ensure this is a token bridge message + assert!( + &record.message.message_type() == message_types::token(), + EUnexpectedMessageType, + ); + // Ensure it's signed + assert!(record.verified_signatures.is_some(), EUnauthorisedClaim); + + // extract token message + let token_payload = record.message.extract_token_bridge_payload(); + // get owner address + let owner = address::from_bytes(token_payload.token_target_address()); + + // If already claimed, exit early + if (record.claimed) { + emit(TokenTransferAlreadyClaimed { message_key: key }); + return (option::none(), owner) + }; + + let target_chain = token_payload.token_target_chain(); + // ensure target chain matches bridge.chain_id + assert!(target_chain == inner.chain_id, EUnexpectedChainID); + + // TODO: why do we check validity of the route here? what if inconsistency? + // Ensure route is valid + // TODO: add unit tests + // `get_route` abort if route is invalid + let route = chain_ids::get_route(source_chain, target_chain); + // check token type + assert!( + treasury::token_id(&inner.treasury) == token_payload.token_type(), + EUnexpectedTokenType, + ); + + let amount = token_payload.token_amount(); + // Make sure transfer is within limit. + if (!inner + .limiter + .check_and_record_sending_transfer( + &inner.treasury, + clock, + route, + amount, + ) + ) { + emit(TokenTransferLimitExceed { message_key: key }); + return (option::none(), owner) + }; + + // claim from treasury + let token = inner.treasury.mint(amount, ctx); + + // Record changes + record.claimed = true; + emit(TokenTransferClaimed { message_key: key }); + + (option::some(token), owner) + } + + fun execute_emergency_op(inner: &mut BridgeInner, payload: EmergencyOp) { + let op = payload.emergency_op_type(); + if (op == message::emergency_op_pause()) { + assert!(!inner.paused, EBridgeAlreadyPaused); + inner.paused = true; + emit(EmergencyOpEvent { frozen: true }); + } else if (op == message::emergency_op_unpause()) { + assert!(inner.paused, EBridgeNotPaused); + inner.paused = false; + emit(EmergencyOpEvent { frozen: false }); + } else { + abort EUnexpectedOperation + }; + } + + fun execute_update_bridge_limit(inner: &mut BridgeInner, payload: UpdateBridgeLimit) { + let receiving_chain = payload.update_bridge_limit_payload_receiving_chain(); + assert!(receiving_chain == inner.chain_id, EUnexpectedChainID); + let route = chain_ids::get_route( + payload.update_bridge_limit_payload_sending_chain(), + receiving_chain + ); + + inner.limiter.update_route_limit( + &route, + payload.update_bridge_limit_payload_limit() + ) + } + + fun execute_update_asset_price(inner: &mut BridgeInner, payload: UpdateAssetPrice) { + inner.treasury.update_asset_notional_price( + payload.update_asset_price_payload_token_id(), + payload.update_asset_price_payload_new_price() + ) + } + + fun execute_add_tokens_on_sui(inner: &mut BridgeInner, payload: AddTokenOnSui) { + // FIXME: assert native_token to be false and add test + let native_token = payload.is_native(); + let mut token_ids = payload.token_ids(); + let mut token_type_names = payload.token_type_names(); + let mut token_prices = payload.token_prices(); + + // Make sure token data is consistent + assert!(token_ids.length() == token_type_names.length(), EMalformedMessageError); + assert!(token_ids.length() == token_prices.length(), EMalformedMessageError); + + while (vector::length(&token_ids) > 0) { + let token_id = token_ids.pop_back(); + let token_type_name = token_type_names.pop_back(); + let token_price = token_prices.pop_back(); + inner.treasury.add_new_token(token_type_name, token_id, native_token, token_price) + } + } + + // Verify seq number matches the next expected seq number for the message type, + // and increment it. + fun get_current_seq_num_and_increment(bridge: &mut BridgeInner, msg_type: u8): u64 { + if (!bridge.sequence_nums.contains(&msg_type)) { + bridge.sequence_nums.insert(msg_type, 1); + return 0 + }; + + let entry = &mut bridge.sequence_nums[&msg_type]; + let seq_num = *entry; + *entry = seq_num + 1; + seq_num + } + + #[allow(unused_function)] + fun get_token_transfer_action_signatures( + bridge: &Bridge, + source_chain: u8, + bridge_seq_num: u64, + ): Option>> { + let inner = load_inner(bridge); + let key = message::create_key( + source_chain, + message_types::token(), + bridge_seq_num + ); + + if (!inner.token_transfer_records.contains(key)) { + return option::none() + }; + + let record = &inner.token_transfer_records[key]; + record.verified_signatures + } + + ////////////////////////////////////////////////////// + // Test functions + // + + #[test_only] + public fun create_bridge_for_testing(id: UID, chain_id: u8, ctx: &mut TxContext) { + create(id, chain_id, ctx); + } + + #[test_only] + public fun new_for_testing(chain_id: u8, ctx: &mut TxContext): Bridge { + let id = object::new(ctx); + let bridge_inner = BridgeInner { + bridge_version: CURRENT_VERSION, + message_version: MESSAGE_VERSION, + chain_id, + sequence_nums: vec_map::empty(), + committee: committee::create(ctx), + treasury: treasury::create(ctx), + token_transfer_records: linked_table::new(ctx), + limiter: limiter::new(), + paused: false, + }; + let mut bridge = Bridge { + id, + inner: versioned::create(CURRENT_VERSION, bridge_inner, ctx), + }; + bridge.setup_treasury_for_testing(); + bridge + } + + #[test_only] + public fun setup_treasury_for_testing(bridge: &mut Bridge) { + bridge.load_inner_mut().treasury.setup_for_testing(); + } + + #[test_only] + public fun test_init_bridge_committee( + bridge: &mut Bridge, + active_validator_voting_power: VecMap, + min_stake_participation_percentage: u64, + ctx: &TxContext + ) { + init_bridge_committee( + bridge, + active_validator_voting_power, + min_stake_participation_percentage, + ctx, + ); + } + + #[test_only] + public fun new_bridge_record_for_testing( + message: BridgeMessage, + verified_signatures: Option>>, + claimed: bool, + ): BridgeRecord { + BridgeRecord { + message, + verified_signatures, + claimed + } + } + + #[test_only] + public fun test_load_inner_mut(bridge: &mut Bridge): &mut BridgeInner { + bridge.load_inner_mut() + } + + #[test_only] + public fun test_load_inner(bridge: &Bridge): &BridgeInner { + bridge.load_inner() + } + + #[test_only] + public fun test_get_token_transfer_action_signatures( + bridge: &mut Bridge, + source_chain: u8, + bridge_seq_num: u64, + ): Option>> { + bridge.get_token_transfer_action_signatures(source_chain, bridge_seq_num) + } + + #[test_only] + public fun inner_limiter(bridge_inner: &BridgeInner): &TransferLimiter { + &bridge_inner.limiter + } + + #[test_only] + public fun inner_treasury(bridge_inner: &BridgeInner): &BridgeTreasury { + &bridge_inner.treasury + } + + #[test_only] + public fun inner_paused(bridge_inner: &BridgeInner): bool { + bridge_inner.paused + } + + #[test_only] + public fun inner_token_transfer_records( + bridge_inner: &mut BridgeInner, + ): &mut LinkedTable { + &mut bridge_inner.token_transfer_records + } + + #[test_only] + public fun test_execute_emergency_op( + bridge_inner: &mut BridgeInner, + payload: EmergencyOp, + ) { + bridge_inner.execute_emergency_op(payload) + } + + #[test_only] + public fun sequence_nums(bridge_inner: &BridgeInner): &VecMap { + &bridge_inner.sequence_nums + } + + #[test_only] + public fun assert_paused(bridge_inner: &BridgeInner, error: u64) { + assert!(bridge_inner.paused, error); + } + + #[test_only] + public fun assert_not_paused(bridge_inner: &BridgeInner, error: u64) { + assert!(!bridge_inner.paused, error); + } + + #[test_only] + public fun test_get_current_seq_num_and_increment( + bridge_inner: &mut BridgeInner, + msg_type: u8, + ): u64 { + get_current_seq_num_and_increment(bridge_inner, msg_type) + } + + #[test_only] + public fun test_execute_update_bridge_limit( + inner: &mut BridgeInner, + payload: UpdateBridgeLimit, + ) { + execute_update_bridge_limit(inner, payload) + } + + #[test_only] + public fun test_execute_update_asset_price( + inner: &mut BridgeInner, + payload: UpdateAssetPrice, + ) { + execute_update_asset_price(inner, payload) + } + + #[test_only] + public fun transfer_status_pending(): u8 { + TRANSFER_STATUS_PENDING + } + + #[test_only] + public fun transfer_status_approved(): u8 { + TRANSFER_STATUS_APPROVED + } + + #[test_only] + public fun transfer_status_claimed(): u8 { + TRANSFER_STATUS_CLAIMED + } + + #[test_only] + public fun transfer_status_not_found(): u8 { + TRANSFER_STATUS_NOT_FOUND + } +} diff --git a/crates/sui-framework/packages/bridge/sources/chain_ids.move b/crates/sui-framework/packages/bridge/sources/chain_ids.move new file mode 100644 index 0000000000000..88f4cbdb480ef --- /dev/null +++ b/crates/sui-framework/packages/bridge/sources/chain_ids.move @@ -0,0 +1,192 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module bridge::chain_ids { + + // Chain IDs + const SuiMainnet: u8 = 0; + const SuiTestnet: u8 = 1; + const SuiCustom: u8 = 2; + + const EthMainnet: u8 = 10; + const EthSepolia: u8 = 11; + const EthCustom: u8 = 12; + + const EInvalidBridgeRoute: u64 = 0; + + ////////////////////////////////////////////////////// + // Types + // + + public struct BridgeRoute has copy, drop, store { + source: u8, + destination: u8, + } + + ////////////////////////////////////////////////////// + // Public functions + // + + public fun sui_mainnet(): u8 { SuiMainnet } + public fun sui_testnet(): u8 { SuiTestnet } + public fun sui_custom(): u8 { SuiCustom } + + public fun eth_mainnet(): u8 { EthMainnet } + public fun eth_sepolia(): u8 { EthSepolia } + public fun eth_custom(): u8 { EthCustom } + + public use fun route_source as BridgeRoute.source; + public fun route_source(route: &BridgeRoute): &u8 { + &route.source + } + + public use fun route_destination as BridgeRoute.destination; + public fun route_destination(route: &BridgeRoute): &u8 { + &route.destination + } + + public fun assert_valid_chain_id(id: u8) { + assert!( + id == SuiMainnet || + id == SuiTestnet || + id == SuiCustom || + id == EthMainnet || + id == EthSepolia || + id == EthCustom, + EInvalidBridgeRoute + ) + } + + public fun valid_routes(): vector { + vector[ + BridgeRoute { source: SuiMainnet, destination: EthMainnet }, + BridgeRoute { source: EthMainnet, destination: SuiMainnet }, + + BridgeRoute { source: SuiTestnet, destination: EthSepolia }, + BridgeRoute { source: SuiTestnet, destination: EthCustom }, + BridgeRoute { source: SuiCustom, destination: EthCustom }, + BridgeRoute { source: SuiCustom, destination: EthSepolia }, + BridgeRoute { source: EthSepolia, destination: SuiTestnet }, + BridgeRoute { source: EthSepolia, destination: SuiCustom }, + BridgeRoute { source: EthCustom, destination: SuiTestnet }, + BridgeRoute { source: EthCustom, destination: SuiCustom } + ] + } + + public fun is_valid_route(source: u8, destination: u8): bool { + let route = BridgeRoute { source, destination }; + valid_routes().contains(&route) + } + + // Checks and return BridgeRoute if the route is supported by the bridge. + public fun get_route(source: u8, destination: u8): BridgeRoute { + let route = BridgeRoute { source, destination }; + assert!(valid_routes().contains(&route), EInvalidBridgeRoute); + route + } + + ////////////////////////////////////////////////////// + // Test functions + // + + #[test] + fun test_chains_ok() { + assert_valid_chain_id(SuiMainnet); + assert_valid_chain_id(SuiTestnet); + assert_valid_chain_id(SuiCustom); + assert_valid_chain_id(EthMainnet); + assert_valid_chain_id(EthSepolia); + assert_valid_chain_id(EthCustom); + } + + #[test] + #[expected_failure(abort_code = EInvalidBridgeRoute)] + fun test_chains_error() { + assert_valid_chain_id(100); + } + + #[test] + #[expected_failure(abort_code = EInvalidBridgeRoute)] + fun test_sui_chains_error() { + // this will break if we add one more sui chain id and should be corrected + assert_valid_chain_id(4); + } + + #[test] + #[expected_failure(abort_code = EInvalidBridgeRoute)] + fun test_eth_chains_error() { + // this will break if we add one more eth chain id and should be corrected + assert_valid_chain_id(13); + } + + #[test] + fun test_routes() { + let valid_routes = vector[ + BridgeRoute { source: SuiMainnet, destination: EthMainnet }, + BridgeRoute { source: EthMainnet, destination: SuiMainnet }, + + BridgeRoute { source: SuiTestnet, destination: EthSepolia }, + BridgeRoute { source: SuiTestnet, destination: EthCustom }, + BridgeRoute { source: SuiCustom, destination: EthCustom }, + BridgeRoute { source: SuiCustom, destination: EthSepolia }, + BridgeRoute { source: EthSepolia, destination: SuiTestnet }, + BridgeRoute { source: EthSepolia, destination: SuiCustom }, + BridgeRoute { source: EthCustom, destination: SuiTestnet }, + BridgeRoute { source: EthCustom, destination: SuiCustom } + ]; + let mut size = valid_routes.length(); + while (size > 0) { + size = size - 1; + let route = valid_routes[size]; + assert!(is_valid_route(route.source, route.destination), 100); // sould not assert + } + } + + #[test] + #[expected_failure(abort_code = EInvalidBridgeRoute)] + fun test_routes_err_sui_1() { + get_route(SuiMainnet, SuiMainnet); + } + + #[test] + #[expected_failure(abort_code = EInvalidBridgeRoute)] + fun test_routes_err_sui_2() { + get_route(SuiMainnet, SuiTestnet); + } + + #[test] + #[expected_failure(abort_code = EInvalidBridgeRoute)] + fun test_routes_err_sui_3() { + get_route(SuiMainnet, EthSepolia); + } + + #[test] + #[expected_failure(abort_code = EInvalidBridgeRoute)] + fun test_routes_err_sui_4() { + get_route(SuiMainnet, EthCustom); + } + + #[test] + #[expected_failure(abort_code = EInvalidBridgeRoute)] + fun test_routes_err_eth_1() { + get_route(EthMainnet, EthMainnet); + } + + #[test] + #[expected_failure(abort_code = EInvalidBridgeRoute)] + fun test_routes_err_eth_2() { + get_route(EthMainnet, EthCustom); + } + + #[test] + #[expected_failure(abort_code = EInvalidBridgeRoute)] + fun test_routes_err_eth_3() { + get_route(EthMainnet, SuiCustom); + } + + #[test] + #[expected_failure(abort_code = EInvalidBridgeRoute)] + fun test_routes_err_eth_4() { + get_route(EthMainnet, SuiTestnet); + } +} diff --git a/crates/sui-framework/packages/bridge/sources/committee.move b/crates/sui-framework/packages/bridge/sources/committee.move new file mode 100644 index 0000000000000..11714ceff35c1 --- /dev/null +++ b/crates/sui-framework/packages/bridge/sources/committee.move @@ -0,0 +1,349 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[allow(unused_use)] +module bridge::committee { + use sui::ecdsa_k1; + use sui::event::emit; + use sui::vec_map::{Self, VecMap}; + use sui::vec_set; + use sui_system::sui_system; + use sui_system::sui_system::SuiSystemState; + + use bridge::crypto; + use bridge::message::{Self, Blocklist, BridgeMessage}; + + const ESignatureBelowThreshold: u64 = 0; + const EDuplicatedSignature: u64 = 1; + const EInvalidSignature: u64 = 2; + const ENotSystemAddress: u64 = 3; + const EValidatorBlocklistContainsUnknownKey: u64 = 4; + const ESenderNotActiveValidator: u64 = 5; + const EInvalidPubkeyLength: u64 = 6; + const ECommitteeAlreadyInitiated: u64 = 7; + const EDuplicatePubkey: u64 = 8; + + const SUI_MESSAGE_PREFIX: vector = b"SUI_BRIDGE_MESSAGE"; + + const ECDSA_COMPRESSED_PUBKEY_LENGTH: u64 = 33; + + ////////////////////////////////////////////////////// + // Types + // + + public struct BlocklistValidatorEvent has copy, drop { + blocklisted: bool, + public_keys: vector>, + } + + public struct BridgeCommittee has store { + // commitee pub key and weight + members: VecMap, CommitteeMember>, + // Committee member registrations for the next committee creation. + member_registrations: VecMap, + // Epoch when the current committee was updated, + // the voting power for each of the committee members are snapshot from this epoch. + // This is mainly for verification/auditing purposes, it might not be useful for bridge operations. + last_committee_update_epoch: u64, + } + + public struct CommitteeUpdateEvent has copy, drop { + // commitee pub key and weight + members: VecMap, CommitteeMember>, + stake_participation_percentage: u64 + } + + public struct CommitteeMember has copy, drop, store { + /// The Sui Address of the validator + sui_address: address, + /// The public key bytes of the bridge key + bridge_pubkey_bytes: vector, + /// Voting power, values are voting power in the scale of 10000. + voting_power: u64, + /// The HTTP REST URL the member's node listens to + /// it looks like b'https://127.0.0.1:9191' + http_rest_url: vector, + /// If this member is blocklisted + blocklisted: bool, + } + + public struct CommitteeMemberRegistration has copy, drop, store { + /// The Sui Address of the validator + sui_address: address, + /// The public key bytes of the bridge key + bridge_pubkey_bytes: vector, + /// The HTTP REST URL the member's node listens to + /// it looks like b'https://127.0.0.1:9191' + http_rest_url: vector, + } + + ////////////////////////////////////////////////////// + // Public functions + // + + public fun verify_signatures( + self: &BridgeCommittee, + message: BridgeMessage, + signatures: vector>, + ) { + let (mut i, signature_counts) = (0, vector::length(&signatures)); + let mut seen_pub_key = vec_set::empty>(); + let required_voting_power = message.required_voting_power(); + // add prefix to the message bytes + let mut message_bytes = SUI_MESSAGE_PREFIX; + message_bytes.append(message.serialize_message()); + + let mut threshold = 0; + while (i < signature_counts) { + let pubkey = ecdsa_k1::secp256k1_ecrecover(&signatures[i], &message_bytes, 0); + + // check duplicate + // and make sure pub key is part of the committee + assert!(!seen_pub_key.contains(&pubkey), EDuplicatedSignature); + assert!(self.members.contains(&pubkey), EInvalidSignature); + + // get committee signature weight and check pubkey is part of the committee + let member = &self.members[&pubkey]; + if (!member.blocklisted) { + threshold = threshold + member.voting_power; + }; + seen_pub_key.insert(pubkey); + i = i + 1; + }; + + assert!(threshold >= required_voting_power, ESignatureBelowThreshold); + } + + ////////////////////////////////////////////////////// + // Internal functions + // + + public(package) fun create(ctx: &TxContext): BridgeCommittee { + assert!(tx_context::sender(ctx) == @0x0, ENotSystemAddress); + BridgeCommittee { + members: vec_map::empty(), + member_registrations: vec_map::empty(), + last_committee_update_epoch: 0, + } + } + + public(package) fun register( + self: &mut BridgeCommittee, + system_state: &mut SuiSystemState, + bridge_pubkey_bytes: vector, + http_rest_url: vector, + ctx: &TxContext + ) { + // We disallow registration after committee initiated in v1 + assert!(self.members.is_empty(), ECommitteeAlreadyInitiated); + // Ensure pubkey is valid + assert!(bridge_pubkey_bytes.length() == ECDSA_COMPRESSED_PUBKEY_LENGTH, EInvalidPubkeyLength); + // sender must be the same sender that created the validator object, this is to prevent DDoS from non-validator actor. + let sender = ctx.sender(); + let validators = system_state.active_validator_addresses(); + + assert!(validators.contains(&sender), ESenderNotActiveValidator); + // Sender is active validator, record the registration + + // In case validator need to update the info + let registration = if (self.member_registrations.contains(&sender)) { + let registration = &mut self.member_registrations[&sender]; + registration.http_rest_url = http_rest_url; + registration.bridge_pubkey_bytes = bridge_pubkey_bytes; + *registration + } else { + let registration = CommitteeMemberRegistration { + sui_address: sender, + bridge_pubkey_bytes, + http_rest_url, + }; + self.member_registrations.insert(sender, registration); + registration + }; + + // check uniqueness of the bridge pubkey. + // `try_create_next_committee` will abort if bridge_pubkey_bytes are not unique and + // that will fail the end of epoch transaction (possibly "forever", well, we + // need to deploy proper validator changes to stop end of epoch from failing). + check_uniqueness_bridge_keys(self, bridge_pubkey_bytes); + + emit(registration) + } + + // This method will try to create the next committee using the registration and system state, + // if the total stake fails to meet the minimum required percentage, it will skip the update. + // This is to ensure we don't fail the end of epoch transaction. + public(package) fun try_create_next_committee( + self: &mut BridgeCommittee, + active_validator_voting_power: VecMap, + min_stake_participation_percentage: u64, + ctx: &TxContext + ) { + let mut i = 0; + let mut new_members = vec_map::empty(); + let mut stake_participation_percentage = 0; + + while (i < self.member_registrations.size()) { + // retrieve registration + let (_, registration) = self.member_registrations.get_entry_by_idx(i); + // Find validator stake amount from system state + + // Process registration if it's active validator + let voting_power = active_validator_voting_power.try_get(®istration.sui_address); + if (voting_power.is_some()) { + let voting_power = voting_power.destroy_some(); + stake_participation_percentage = stake_participation_percentage + voting_power; + + let member = CommitteeMember { + sui_address: registration.sui_address, + bridge_pubkey_bytes: registration.bridge_pubkey_bytes, + voting_power: (voting_power as u64), + http_rest_url: registration.http_rest_url, + blocklisted: false, + }; + + new_members.insert(registration.bridge_pubkey_bytes, member) + }; + + i = i + 1; + }; + + // Make sure the new committee represent enough stakes, percentage are accurate to 2DP + if (stake_participation_percentage >= min_stake_participation_percentage) { + // Clear registrations + self.member_registrations = vec_map::empty(); + // Store new committee info + self.members = new_members; + self.last_committee_update_epoch = ctx.epoch(); + + emit(CommitteeUpdateEvent { + members: new_members, + stake_participation_percentage + }) + } + } + + // This function applys the blocklist to the committee members, we won't need to run this very often so this is not gas optimised. + // TODO: add tests for this function + public(package) fun execute_blocklist(self: &mut BridgeCommittee, blocklist: Blocklist) { + let blocklisted = blocklist.blocklist_type() != 1; + let eth_addresses = blocklist.blocklist_validator_addresses(); + let list_len = eth_addresses.length(); + let mut list_idx = 0; + let mut member_idx = 0; + let mut pub_keys = vector[]; + + while (list_idx < list_len) { + let target_address = ð_addresses[list_idx]; + let mut found = false; + + while (member_idx < self.members.size()) { + let (pub_key, member) = self.members.get_entry_by_idx_mut(member_idx); + let eth_address = crypto::ecdsa_pub_key_to_eth_address(pub_key); + + if (*target_address == eth_address) { + member.blocklisted = blocklisted; + pub_keys.push_back(*pub_key); + found = true; + member_idx = 0; + break + }; + + member_idx = member_idx + 1; + }; + + assert!(found, EValidatorBlocklistContainsUnknownKey); + list_idx = list_idx + 1; + }; + + emit(BlocklistValidatorEvent { + blocklisted, + public_keys: pub_keys, + }) + } + + public(package) fun committee_members( + self: &BridgeCommittee, + ): &VecMap, CommitteeMember> { + &self.members + } + + // Assert if `bridge_pubkey_bytes` is duplicated in `member_registrations`. + // Dupicate keys would cause `try_create_next_committee` to fail and, + // in consequence, an end of epoch transaction to fail (safe mode run). + // This check will ensure the creation of the committee is correct. + fun check_uniqueness_bridge_keys(self: &BridgeCommittee, bridge_pubkey_bytes: vector) { + let mut count = self.member_registrations.size(); + // bridge_pubkey_bytes must be found once and once only + let mut bridge_key_found = false; + while (count > 0) { + count = count - 1; + let (_, registration) = self.member_registrations.get_entry_by_idx(count); + if (registration.bridge_pubkey_bytes == bridge_pubkey_bytes) { + assert!(!bridge_key_found, EDuplicatePubkey); + bridge_key_found = true; // bridge_pubkey_bytes found, we must not have another one + } + }; + } + + ////////////////////////////////////////////////////// + // Test functions + // + + #[test_only] + public(package) fun members(self: &BridgeCommittee): &VecMap, CommitteeMember> { + &self.members + } + + #[test_only] + public(package) fun voting_power(member: &CommitteeMember): u64 { + member.voting_power + } + + #[test_only] + public(package) fun member_registrations( + self: &BridgeCommittee, + ): &VecMap { + &self.member_registrations + } + + #[test_only] + public(package) fun blocklisted(member: &CommitteeMember): bool { + member.blocklisted + } + + #[test_only] + public(package) fun bridge_pubkey_bytes(registration: &CommitteeMemberRegistration): &vector { + ®istration.bridge_pubkey_bytes + } + + #[test_only] + public(package) fun make_bridge_committee( + members: VecMap, CommitteeMember>, + member_registrations: VecMap, + last_committee_update_epoch: u64, + ): BridgeCommittee { + BridgeCommittee { + members, + member_registrations, + last_committee_update_epoch, + } + } + + #[test_only] + public(package) fun make_committee_member( + sui_address: address, + bridge_pubkey_bytes: vector, + voting_power: u64, + http_rest_url: vector, + blocklisted: bool, + ): CommitteeMember { + CommitteeMember { + sui_address, + bridge_pubkey_bytes, + voting_power, + http_rest_url, + blocklisted, + } + } +} diff --git a/crates/sui-framework/packages/bridge/sources/crypto.move b/crates/sui-framework/packages/bridge/sources/crypto.move new file mode 100644 index 0000000000000..286bbef75bcfb --- /dev/null +++ b/crates/sui-framework/packages/bridge/sources/crypto.move @@ -0,0 +1,41 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module bridge::crypto { + use sui::ecdsa_k1; + use sui::hash::keccak256; + #[test_only] + use sui::hex; + + public(package) fun ecdsa_pub_key_to_eth_address(compressed_pub_key: &vector): vector { + // Decompress pub key + let decompressed = ecdsa_k1::decompress_pubkey(compressed_pub_key); + + // Skip the first byte + let (mut i, mut decompressed_64) = (1, vector[]); + while (i < 65) { + decompressed_64.push_back(decompressed[i]); + i = i + 1; + }; + + // Hash + let hash = keccak256(&decompressed_64); + + // Take last 20 bytes + let mut address = vector[]; + let mut i = 12; + while (i < 32) { + address.push_back(hash[i]); + i = i + 1; + }; + address + } + + #[test] + fun test_pub_key_to_eth_address() { + let validator_pub_key = hex::decode(b"029bef8d556d80e43ae7e0becb3a7e6838b95defe45896ed6075bb9035d06c9964"); + let expected_address = hex::decode(b"b14d3c4f5fbfbcfb98af2d330000d49c95b93aa7"); + + assert!(ecdsa_pub_key_to_eth_address(&validator_pub_key) == expected_address, 0); + } +} diff --git a/crates/sui-framework/packages/bridge/sources/limiter.move b/crates/sui-framework/packages/bridge/sources/limiter.move new file mode 100644 index 0000000000000..391476f3921b8 --- /dev/null +++ b/crates/sui-framework/packages/bridge/sources/limiter.move @@ -0,0 +1,279 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module bridge::limiter { + use sui::clock::{Self, Clock}; + use sui::event::emit; + use sui::vec_map::{Self, VecMap}; + + use bridge::chain_ids::{Self, BridgeRoute}; + use bridge::treasury::BridgeTreasury; + + const ELimitNotFoundForRoute: u64 = 0; + + // TODO: U64::MAX, make this configurable? + const MAX_TRANSFER_LIMIT: u64 = 18_446_744_073_709_551_615; + + const USD_VALUE_MULTIPLIER: u64 = 100000000; // 8 DP accuracy + + ////////////////////////////////////////////////////// + // Types + // + + public struct TransferLimiter has store { + transfer_limits: VecMap, + // Per hour transfer amount for each bridge route + transfer_records: VecMap, + } + + public struct TransferRecord has store { + hour_head: u64, + hour_tail: u64, + per_hour_amounts: vector, + // total amount in USD, 4 DP accuracy, so 10000 => 1USD + total_amount: u64 + } + + public struct UpdateRouteLimitEvent has copy, drop { + sending_chain: u8, + receiving_chain: u8, + new_limit: u64, + } + + ////////////////////////////////////////////////////// + // Public functions + // + + // Abort if the route limit is not found + public fun get_route_limit(self: &TransferLimiter, route: &BridgeRoute): u64 { + self.transfer_limits[route] + } + + ////////////////////////////////////////////////////// + // Internal functions + // + + public(package) fun new(): TransferLimiter { + // hardcoded limit for bridge genesis + TransferLimiter { + transfer_limits: initial_transfer_limits(), + transfer_records: vec_map::empty() + } + } + + public(package) fun check_and_record_sending_transfer( + self: &mut TransferLimiter, + treasury: &BridgeTreasury, + clock: &Clock, + route: BridgeRoute, + amount: u64 + ): bool { + // Create record for route if not exists + if (!self.transfer_records.contains(&route)) { + self.transfer_records.insert(route, TransferRecord { + hour_head: 0, + hour_tail: 0, + per_hour_amounts: vector[], + total_amount: 0 + }) + }; + let record = self.transfer_records.get_mut(&route); + let current_hour_since_epoch = current_hour_since_epoch(clock); + + record.adjust_transfer_records(current_hour_since_epoch); + + // Get limit for the route + let route_limit = self.transfer_limits.try_get(&route); + assert!(route_limit.is_some(), ELimitNotFoundForRoute); + let route_limit = route_limit.destroy_some(); + let route_limit_adjusted = + (route_limit as u128) * (treasury.decimal_multiplier() as u128); + + // Compute notional amount + // Upcast to u128 to prevent overflow, to not miss out on small amounts. + let value = (treasury.notional_value() as u128); + let notional_amount_with_token_multiplier = value * (amount as u128); + + // Check if transfer amount exceed limit + // Upscale them to the token's decimal. + if ((record.total_amount as u128) + * (treasury.decimal_multiplier() as u128) + + notional_amount_with_token_multiplier > route_limit_adjusted + ) { + return false + }; + + // Now scale down to notional value + let notional_amount = notional_amount_with_token_multiplier + / (treasury.decimal_multiplier() as u128); + // Should be safe to downcast to u64 after dividing by the decimals + let notional_amount = (notional_amount as u64); + + // Record transfer value + let new_amount = record.per_hour_amounts.pop_back() + notional_amount; + record.per_hour_amounts.push_back(new_amount); + record.total_amount = record.total_amount + notional_amount; + true + } + + public(package) fun update_route_limit( + self: &mut TransferLimiter, + route: &BridgeRoute, + new_usd_limit: u64 + ) { + let receiving_chain = *route.destination(); + + if (!self.transfer_limits.contains(route)) { + self.transfer_limits.insert(*route, new_usd_limit); + } else { + *&mut self.transfer_limits[route] = new_usd_limit; + }; + + emit(UpdateRouteLimitEvent { + sending_chain: *route.source(), + receiving_chain, + new_limit: new_usd_limit, + }) + } + + // Current hour since unix epoch + fun current_hour_since_epoch(clock: &Clock): u64 { + clock::timestamp_ms(clock) / 3600000 + } + + fun adjust_transfer_records(self: &mut TransferRecord, current_hour_since_epoch: u64) { + if (self.hour_head == current_hour_since_epoch) { + return // nothing to backfill + }; + + let target_tail = current_hour_since_epoch - 23; + + // If `hour_head` is even older than 24 hours ago, it means all items in + // `per_hour_amounts` are to be evicted. + if (self.hour_head < target_tail) { + self.per_hour_amounts = vector[]; + self.total_amount = 0; + self.hour_tail = target_tail; + self.hour_head = target_tail; + // Don't forget to insert this hour's record + self.per_hour_amounts.push_back(0); + } else { + // self.hour_head is within 24 hour range. + // some items in `per_hour_amounts` are still valid, we remove stale hours. + while (self.hour_tail < target_tail) { + self.total_amount = self.total_amount - self.per_hour_amounts.remove(0); + self.hour_tail = self.hour_tail + 1; + } + }; + + // Backfill from hour_head to current hour + while (self.hour_head < current_hour_since_epoch) { + self.per_hour_amounts.push_back(0); + self.hour_head = self.hour_head + 1; + } + } + + // It's tedious to list every pair, but it's safer to do so so we don't + // accidentally turn off limiter for a new production route in the future. + // Note limiter only takes effects on the receiving chain, so we only need to + // specify routes from Ethereum to Sui. + fun initial_transfer_limits(): VecMap { + let mut transfer_limits = vec_map::empty(); + // 5M limit on Sui -> Ethereum mainnet + transfer_limits.insert( + chain_ids::get_route(chain_ids::eth_mainnet(), chain_ids::sui_mainnet()), + 5_000_000 * USD_VALUE_MULTIPLIER + ); + + // MAX limit for testnet and devnet + transfer_limits.insert( + chain_ids::get_route(chain_ids::eth_sepolia(), chain_ids::sui_testnet()), + MAX_TRANSFER_LIMIT + ); + + transfer_limits.insert( + chain_ids::get_route(chain_ids::eth_sepolia(), chain_ids::sui_custom()), + MAX_TRANSFER_LIMIT + ); + + transfer_limits.insert( + chain_ids::get_route(chain_ids::eth_custom(), chain_ids::sui_testnet()), + MAX_TRANSFER_LIMIT + ); + + transfer_limits.insert( + chain_ids::get_route(chain_ids::eth_custom(), chain_ids::sui_custom()), + MAX_TRANSFER_LIMIT + ); + + transfer_limits + } + + ////////////////////////////////////////////////////// + // Test functions + // + + #[test_only] + public(package) fun transfer_limits(limiter: &TransferLimiter): &VecMap { + &limiter.transfer_limits + } + + #[test_only] + public(package) fun transfer_limits_mut( + limiter: &mut TransferLimiter, + ): &mut VecMap { + &mut limiter.transfer_limits + } + + #[test_only] + public(package) fun transfer_records( + limiter: &TransferLimiter, + ): &VecMap { + &limiter.transfer_records + } + + #[test_only] + public(package) fun transfer_records_mut( + limiter: &mut TransferLimiter, + ): &mut VecMap { + &mut limiter.transfer_records + } + + #[test_only] + public(package) fun usd_value_multiplier(): u64 { + USD_VALUE_MULTIPLIER + } + + #[test_only] + public(package) fun max_transfer_limit(): u64 { + MAX_TRANSFER_LIMIT + } + + #[test_only] + public(package) fun make_transfer_limiter(): TransferLimiter { + TransferLimiter { + transfer_limits: vec_map::empty(), + transfer_records: vec_map::empty(), + } + } + + #[test_only] + public(package) fun total_amount(record: &TransferRecord): u64 { + record.total_amount + } + + #[test_only] + public(package) fun per_hour_amounts(record: &TransferRecord): &vector { + &record.per_hour_amounts + } + + #[test_only] + public(package) fun hour_head(record: &TransferRecord): u64 { + record.hour_head + } + + #[test_only] + public(package) fun hour_tail(record: &TransferRecord): u64 { + record.hour_tail + } +} diff --git a/crates/sui-framework/packages/bridge/sources/message.move b/crates/sui-framework/packages/bridge/sources/message.move new file mode 100644 index 0000000000000..43dc44cc97c58 --- /dev/null +++ b/crates/sui-framework/packages/bridge/sources/message.move @@ -0,0 +1,647 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module bridge::message { + use std::ascii::{Self, String}; + use sui::bcs::{Self, BCS}; + + use bridge::chain_ids; + use bridge::message_types; + + const CURRENT_MESSAGE_VERSION: u8 = 1; + const ECDSA_ADDRESS_LENGTH: u64 = 20; + + const ETrailingBytes: u64 = 0; + const EInvalidAddressLength: u64 = 1; + const EEmptyList: u64 = 2; + const EInvalidMessageType: u64 = 3; + const EInvalidEmergencyOpType: u64 = 4; + const EInvalidPayloadLength: u64 = 5; + + // Emergency Op types + const PAUSE: u8 = 0; + const UNPAUSE: u8 = 1; + + ////////////////////////////////////////////////////// + // Types + // + + public struct BridgeMessage has copy, drop, store { + message_type: u8, + message_version: u8, + seq_num: u64, + source_chain: u8, + payload: vector + } + + public struct BridgeMessageKey has copy, drop, store { + source_chain: u8, + message_type: u8, + bridge_seq_num: u64 + } + + public struct TokenPayload has drop { + sender_address: vector, + target_chain: u8, + target_address: vector, + token_type: u8, + amount: u64 + } + + public struct EmergencyOp has drop { + op_type: u8 + } + + public struct Blocklist has drop { + blocklist_type: u8, + validator_eth_addresses: vector> + } + + // Update the limit for route from sending_chain to receiving_chain + // This message is supposed to be processed by `chain` or the receiving chain + public struct UpdateBridgeLimit has drop { + // The receiving chain, also the chain that checks and processes this message + receiving_chain: u8, + // The sending chain + sending_chain: u8, + limit: u64 + } + + public struct UpdateAssetPrice has drop { + token_id: u8, + new_price: u64 + } + + public struct AddTokenOnSui has drop { + native_token: bool, + token_ids: vector, + token_type_names: vector, + token_prices: vector + } + + ////////////////////////////////////////////////////// + // Public functions + // + + // Note: `bcs::peel_vec_u8` *happens* to work here because + // `sender_address` and `target_address` are no longer than 255 bytes. + // Therefore their length can be represented by a single byte. + // See `create_token_bridge_message` for the actual encoding rule. + public fun extract_token_bridge_payload(message: &BridgeMessage): TokenPayload { + let mut bcs = bcs::new(message.payload); + let sender_address = bcs.peel_vec_u8(); + let target_chain = bcs.peel_u8(); + let target_address = bcs.peel_vec_u8(); + let token_type = bcs.peel_u8(); + let amount = peel_u64_be(&mut bcs); + + // TODO: add test case for invalid chain id + // TODO: replace with `chain_ids::is_valid_chain_id()` + chain_ids::assert_valid_chain_id(target_chain); + assert!(bcs.into_remainder_bytes().is_empty(), ETrailingBytes); + + TokenPayload { + sender_address, + target_chain, + target_address, + token_type, + amount + } + } + + /// Emergency op payload is just a single byte + public fun extract_emergency_op_payload(message: &BridgeMessage): EmergencyOp { + assert!(message.payload.length() == 1, ETrailingBytes); + EmergencyOp { op_type: message.payload[0] } + } + + public fun extract_blocklist_payload(message: &BridgeMessage): Blocklist { + // blocklist payload should consist of one byte blocklist type, and list of 33 bytes ecdsa pub keys + let mut bcs = bcs::new(message.payload); + let blocklist_type = bcs.peel_u8(); + let mut address_count = bcs.peel_u8(); + + // TODO: add test case for 0 value + assert!(address_count != 0, EEmptyList); + + let mut validator_eth_addresses = vector[]; + while (address_count > 0) { + let (mut address, mut i) = (vector[], 0); + while (i < ECDSA_ADDRESS_LENGTH) { + address.push_back(bcs.peel_u8()); + i = i + 1; + }; + validator_eth_addresses.push_back(address); + address_count = address_count - 1; + }; + + assert!(bcs.into_remainder_bytes().is_empty(), ETrailingBytes); + + Blocklist { + blocklist_type, + validator_eth_addresses + } + } + + public fun extract_update_bridge_limit(message: &BridgeMessage): UpdateBridgeLimit { + let mut bcs = bcs::new(message.payload); + let sending_chain = bcs.peel_u8(); + let limit = peel_u64_be(&mut bcs); + + // TODO: add test case for invalid chain id + chain_ids::assert_valid_chain_id(sending_chain); + assert!(bcs.into_remainder_bytes().is_empty(), ETrailingBytes); + + UpdateBridgeLimit { + receiving_chain: message.source_chain, + sending_chain, + limit + } + } + + public fun extract_update_asset_price(message: &BridgeMessage): UpdateAssetPrice { + let mut bcs = bcs::new(message.payload); + let token_id = bcs.peel_u8(); + let new_price = peel_u64_be(&mut bcs); + + assert!(bcs.into_remainder_bytes().is_empty(), ETrailingBytes); + + UpdateAssetPrice { + token_id, + new_price + } + } + + public fun extract_add_tokens_on_sui(message: &BridgeMessage): AddTokenOnSui { + let mut bcs = bcs::new(message.payload); + let native_token = bcs.peel_bool(); + let token_ids = bcs.peel_vec_u8(); + let token_type_names_bytes = bcs.peel_vec_vec_u8(); + let token_prices = bcs.peel_vec_u64(); + + let mut n = 0; + let mut token_type_names = vector[]; + while (n < token_type_names_bytes.length()){ + token_type_names.push_back(ascii::string(*token_type_names_bytes.borrow(n))); + n = n + 1; + }; + assert!(bcs.into_remainder_bytes().is_empty(), ETrailingBytes); + AddTokenOnSui { + native_token, + token_ids, + token_type_names, + token_prices + } + } + + public fun serialize_message(message: BridgeMessage): vector { + let BridgeMessage { + message_type, + message_version, + seq_num, + source_chain, + payload + } = message; + + let mut message = vector[ + message_type, + message_version, + ]; + + // bcs serializes u64 as 8 bytes + message.append(reverse_bytes(bcs::to_bytes(&seq_num))); + message.push_back(source_chain); + message.append(payload); + message + } + + /// Token Transfer Message Format: + /// [message_type: u8] + /// [version:u8] + /// [nonce:u64] + /// [source_chain: u8] + /// [sender_address_length:u8] + /// [sender_address: byte[]] + /// [target_chain:u8] + /// [target_address_length:u8] + /// [target_address: byte[]] + /// [token_type:u8] + /// [amount:u64] + public fun create_token_bridge_message( + source_chain: u8, + seq_num: u64, + sender_address: vector, + target_chain: u8, + target_address: vector, + token_type: u8, + amount: u64 + ): BridgeMessage { + // TODO: add test case for invalid chain id + // TODO: add test case for invalid chain id + // TODO: replace with `chain_ids::is_valid_chain_id()` + chain_ids::assert_valid_chain_id(source_chain); + chain_ids::assert_valid_chain_id(target_chain); + + let mut payload = vector[]; + + // sender address should be less than 255 bytes so can fit into u8 + payload.push_back((vector::length(&sender_address) as u8)); + payload.append(sender_address); + payload.push_back(target_chain); + // target address should be less than 255 bytes so can fit into u8 + payload.push_back((vector::length(&target_address) as u8)); + payload.append(target_address); + payload.push_back(token_type); + // bcs serialzies u64 as 8 bytes + payload.append(reverse_bytes(bcs::to_bytes(&amount))); + + assert!(vector::length(&payload) == 64, EInvalidPayloadLength); + + BridgeMessage { + message_type: message_types::token(), + message_version: CURRENT_MESSAGE_VERSION, + seq_num, + source_chain, + payload, + } + } + + /// Emergency Op Message Format: + /// [message_type: u8] + /// [version:u8] + /// [nonce:u64] + /// [chain_id: u8] + /// [op_type: u8] + public fun create_emergency_op_message( + source_chain: u8, + seq_num: u64, + op_type: u8, + ): BridgeMessage { + // TODO: add test case for invalid chain id + // TODO: replace with `chain_ids::is_valid_chain_id()` + chain_ids::assert_valid_chain_id(source_chain); + + BridgeMessage { + message_type: message_types::emergency_op(), + message_version: CURRENT_MESSAGE_VERSION, + seq_num, + source_chain, + payload: vector[op_type], + } + } + + /// Blocklist Message Format: + /// [message_type: u8] + /// [version:u8] + /// [nonce:u64] + /// [chain_id: u8] + /// [blocklist_type: u8] + /// [validator_length: u8] + /// [validator_ecdsa_addresses: byte[][]] + public fun create_blocklist_message( + source_chain: u8, + seq_num: u64, + // 0: block, 1: unblock + blocklist_type: u8, + validator_ecdsa_addresses: vector>, + ): BridgeMessage { + // TODO: add test case for invalid chain id + // TODO: replace with `chain_ids::is_valid_chain_id()` + chain_ids::assert_valid_chain_id(source_chain); + + let address_length = validator_ecdsa_addresses.length(); + let mut payload = vector[blocklist_type, (address_length as u8)]; + let mut i = 0; + + while (i < address_length) { + let address = validator_ecdsa_addresses[i]; + assert!(address.length() == ECDSA_ADDRESS_LENGTH, EInvalidAddressLength); + payload.append(address); + + i = i + 1; + }; + + BridgeMessage { + message_type: message_types::committee_blocklist(), + message_version: CURRENT_MESSAGE_VERSION, + seq_num, + source_chain, + payload, + } + } + + /// Update bridge limit Message Format: + /// [message_type: u8] + /// [version:u8] + /// [nonce:u64] + /// [receiving_chain_id: u8] + /// [sending_chain_id: u8] + /// [new_limit: u64] + public fun create_update_bridge_limit_message( + receiving_chain: u8, + seq_num: u64, + sending_chain: u8, + new_limit: u64, + ): BridgeMessage { + // TODO: add test case for invalid chain id + // TODO: add test case for invalid chain id + // TODO: replace with `chain_ids::is_valid_chain_id()` + chain_ids::assert_valid_chain_id(receiving_chain); + chain_ids::assert_valid_chain_id(sending_chain); + + let mut payload = vector[sending_chain]; + payload.append(reverse_bytes(bcs::to_bytes(&new_limit))); + + BridgeMessage { + message_type: message_types::update_bridge_limit(), + message_version: CURRENT_MESSAGE_VERSION, + seq_num, + source_chain: receiving_chain, + payload, + } + } + + /// Update asset price message + /// [message_type: u8] + /// [version:u8] + /// [nonce:u64] + /// [chain_id: u8] + /// [token_id: u8] + /// [new_price:u64] + public fun create_update_asset_price_message( + token_id: u8, + source_chain: u8, + seq_num: u64, + new_price: u64, + ): BridgeMessage { + // TODO: add test case for invalid chain id + // TODO: replace with `chain_ids::is_valid_chain_id()` + chain_ids::assert_valid_chain_id(source_chain); + + let mut payload = vector[token_id]; + payload.append(reverse_bytes(bcs::to_bytes(&new_price))); + BridgeMessage { + message_type: message_types::update_asset_price(), + message_version: CURRENT_MESSAGE_VERSION, + seq_num, + source_chain, + payload, + } + } + + /// Update Sui token message + /// [message_type:u8] + /// [version:u8] + /// [nonce:u64] + /// [chain_id: u8] + /// [native_token:bool] + /// [token_ids:vector] + /// [token_type_name:vector] + /// [token_prices:vector] + public fun create_add_tokens_on_sui_message( + source_chain: u8, + seq_num: u64, + native_token: bool, + token_ids: vector, + type_names: vector, + token_prices: vector, + ): BridgeMessage { + chain_ids::assert_valid_chain_id(source_chain); + let mut payload = bcs::to_bytes(&native_token); + payload.append(bcs::to_bytes(&token_ids)); + payload.append(bcs::to_bytes(&type_names)); + payload.append(bcs::to_bytes(&token_prices)); + BridgeMessage { + message_type: message_types::add_tokens_on_sui(), + message_version: CURRENT_MESSAGE_VERSION, + seq_num, + source_chain, + payload, + } + } + + public fun create_key(source_chain: u8, message_type: u8, bridge_seq_num: u64): BridgeMessageKey { + BridgeMessageKey { source_chain, message_type, bridge_seq_num } + } + + public fun key(self: &BridgeMessage): BridgeMessageKey { + create_key(self.source_chain, self.message_type, self.seq_num) + } + + // BridgeMessage getters + public fun message_version(self: &BridgeMessage): u8 { + self.message_version + } + + public fun message_type(self: &BridgeMessage): u8 { + self.message_type + } + + public fun seq_num(self: &BridgeMessage): u64 { + self.seq_num + } + + public fun source_chain(self: &BridgeMessage): u8 { + self.source_chain + } + + public fun token_target_chain(self: &TokenPayload): u8 { + self.target_chain + } + + public fun token_target_address(self: &TokenPayload): vector { + self.target_address + } + + public fun token_type(self: &TokenPayload): u8 { + self.token_type + } + + public fun token_amount(self: &TokenPayload): u64 { + self.amount + } + + // EmergencyOpPayload getters + public fun emergency_op_type(self: &EmergencyOp): u8 { + self.op_type + } + + public fun blocklist_type(self: &Blocklist): u8 { + self.blocklist_type + } + + public fun blocklist_validator_addresses(self: &Blocklist): &vector> { + &self.validator_eth_addresses + } + + public fun update_bridge_limit_payload_sending_chain(self: &UpdateBridgeLimit): u8 { + self.sending_chain + } + + public fun update_bridge_limit_payload_receiving_chain(self: &UpdateBridgeLimit): u8 { + self.receiving_chain + } + + public fun update_bridge_limit_payload_limit(self: &UpdateBridgeLimit): u64 { + self.limit + } + + public fun update_asset_price_payload_token_id(self: &UpdateAssetPrice): u8 { + self.token_id + } + + public fun update_asset_price_payload_new_price(self: &UpdateAssetPrice): u64 { + self.new_price + } + + public fun is_native(self: &AddTokenOnSui): bool { + self.native_token + } + + public fun token_ids(self: &AddTokenOnSui): vector { + self.token_ids + } + + public fun token_type_names(self: &AddTokenOnSui): vector { + self.token_type_names + } + + public fun token_prices(self: &AddTokenOnSui): vector { + self.token_prices + } + + public fun emergency_op_pause(): u8 { + PAUSE + } + + public fun emergency_op_unpause(): u8 { + UNPAUSE + } + + /// Return the required signature threshold for the message, values are voting power in the scale of 10000 + public fun required_voting_power(self: &BridgeMessage): u64 { + let message_type = message_type(self); + + if (message_type == message_types::token()) { + 3334 + } else if (message_type == message_types::emergency_op()) { + let payload = extract_emergency_op_payload(self); + if (payload.op_type == PAUSE) { + 450 + } else if (payload.op_type == UNPAUSE) { + 5001 + } else { + abort EInvalidEmergencyOpType + } + } else if (message_type == message_types::committee_blocklist()) { + 5001 + } else if (message_type == message_types::update_asset_price()) { + 5001 + } else if (message_type == message_types::update_bridge_limit()) { + 5001 + } else if (message_type == message_types::add_tokens_on_sui()) { + 5001 + } else { + abort EInvalidMessageType + } + } + + ////////////////////////////////////////////////////// + // Internal functions + // + + fun reverse_bytes(mut bytes: vector): vector { + vector::reverse(&mut bytes); + bytes + } + + fun peel_u64_be(bcs: &mut BCS): u64 { + let (mut value, mut i) = (0u64, 64u8); + while (i > 0) { + i = i - 8; + let byte = (bcs::peel_u8(bcs) as u64); + value = value + (byte << i); + }; + value + } + + ////////////////////////////////////////////////////// + // Test functions + // + + #[test_only] + public(package) fun peel_u64_be_for_testing(bcs: &mut BCS): u64 { + peel_u64_be(bcs) + } + + #[test_only] + public(package) fun make_generic_message( + message_type: u8, + message_version: u8, + seq_num: u64, + source_chain: u8, + payload: vector, + ): BridgeMessage { + BridgeMessage { + message_type, + message_version, + seq_num, + source_chain, + payload, + } + } + + #[test_only] + public(package) fun make_payload( + sender_address: vector, + target_chain: u8, + target_address: vector, + token_type: u8, + amount: u64 + ): TokenPayload { + TokenPayload { + sender_address, + target_chain, + target_address, + token_type, + amount, + } + } + + #[test_only] + public(package) fun deserialize_message_test_only(message: vector): BridgeMessage { + let mut bcs = bcs::new(message); + let message_type = bcs::peel_u8(&mut bcs); + let message_version = bcs::peel_u8(&mut bcs); + let seq_num = peel_u64_be_for_testing(&mut bcs); + let source_chain = bcs::peel_u8(&mut bcs); + let payload = bcs::into_remainder_bytes(bcs); + make_generic_message( + message_type, + message_version, + seq_num, + source_chain, + payload, + ) + } + + #[test_only] + public(package) fun reverse_bytes_test(bytes: vector): vector { + reverse_bytes(bytes) + } + + #[test_only] + public(package) fun make_add_token_on_sui( + native_token: bool, + token_ids: vector, + token_type_names: vector, + token_prices: vector, + ): AddTokenOnSui { + AddTokenOnSui { + native_token, + token_ids, + token_type_names, + token_prices, + } + } +} diff --git a/crates/sui-framework/packages/bridge/sources/message_types.move b/crates/sui-framework/packages/bridge/sources/message_types.move new file mode 100644 index 0000000000000..3ff85f65aec9f --- /dev/null +++ b/crates/sui-framework/packages/bridge/sources/message_types.move @@ -0,0 +1,24 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module bridge::message_types { + // message types + const TOKEN: u8 = 0; + const COMMITTEE_BLOCKLIST: u8 = 1; + const EMERGENCY_OP: u8 = 2; + const UPDATE_BRIDGE_LIMIT: u8 = 3; + const UPDATE_ASSET_PRICE: u8 = 4; + const ADD_TOKENS_ON_SUI: u8 = 6; + + public fun token(): u8 { TOKEN } + + public fun committee_blocklist(): u8 { COMMITTEE_BLOCKLIST } + + public fun emergency_op(): u8 { EMERGENCY_OP } + + public fun update_bridge_limit(): u8 { UPDATE_BRIDGE_LIMIT } + + public fun update_asset_price(): u8 { UPDATE_ASSET_PRICE } + + public fun add_tokens_on_sui(): u8 { ADD_TOKENS_ON_SUI } +} diff --git a/crates/sui-framework/packages/bridge/sources/treasury.move b/crates/sui-framework/packages/bridge/sources/treasury.move new file mode 100644 index 0000000000000..12e8d3c7e02e5 --- /dev/null +++ b/crates/sui-framework/packages/bridge/sources/treasury.move @@ -0,0 +1,274 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module bridge::treasury { + use std::ascii; + use std::ascii::String; + use std::type_name; + use std::type_name::TypeName; + + use sui::address; + use sui::bag; + use sui::bag::Bag; + use sui::coin::{Self, Coin, TreasuryCap, CoinMetadata}; + use sui::event::emit; + use sui::hex; + use sui::math; + use sui::object_bag::{Self, ObjectBag}; + use sui::package; + use sui::package::UpgradeCap; + use sui::vec_map; + use sui::vec_map::VecMap; + + const EUnsupportedTokenType: u64 = 1; + const EInvalidUpgradeCap: u64 = 2; + const ETokenSupplyNonZero: u64 = 3; + const EInvalidNotionalValue: u64 = 4; + + #[test_only] + const USD_VALUE_MULTIPLIER: u64 = 100000000; // 8 DP accuracy + + ////////////////////////////////////////////////////// + // Types + // + + public struct BridgeTreasury has store { + // token treasuries, values are TreasuryCaps for native bridge V1. + treasuries: ObjectBag, + supported_tokens: VecMap, + // Mapping token id to type name + id_token_type_map: VecMap, + // Bag for storing potential new token waiting to be approved + waiting_room: Bag + } + + public struct BridgeTokenMetadata has store, copy, drop { + id: u8, + decimal_multiplier: u64, + notional_value: u64, + native_token: bool + } + + public struct ForeignTokenRegistration has store { + type_name: TypeName, + uc: UpgradeCap, + decimal: u8, + } + + public struct UpdateTokenPriceEvent has copy, drop { + token_id: u8, + new_price: u64, + } + + public struct NewTokenEvent has copy, drop { + token_id: u8, + type_name: TypeName, + native_token: bool, + decimal_multiplier: u64, + notional_value: u64 + } + + public struct TokenRegistrationEvent has copy, drop { + type_name: TypeName, + decimal: u8, + native_token: bool + } + + public fun token_id(self: &BridgeTreasury): u8 { + let metadata = self.get_token_metadata(); + metadata.id + } + + public fun decimal_multiplier(self: &BridgeTreasury): u64 { + let metadata = self.get_token_metadata(); + metadata.decimal_multiplier + } + + public fun notional_value(self: &BridgeTreasury): u64 { + let metadata = self.get_token_metadata(); + metadata.notional_value + } + + ////////////////////////////////////////////////////// + // Internal functions + // + + public(package) fun register_foreign_token( + self: &mut BridgeTreasury, + tc: TreasuryCap, + uc: UpgradeCap, + metadata: &CoinMetadata, + ) { + // Make sure TreasuryCap has not been minted before. + assert!(coin::total_supply(&tc) == 0, ETokenSupplyNonZero); + let type_name = type_name::get(); + let address_bytes = hex::decode(ascii::into_bytes(type_name::get_address(&type_name))); + let coin_address = address::from_bytes(address_bytes); + // Make sure upgrade cap is for the Coin package + // FIXME: add test + assert!( + object::id_to_address(&package::upgrade_package(&uc)) + == coin_address, EInvalidUpgradeCap + ); + let registration = ForeignTokenRegistration { + type_name, + uc, + decimal: coin::get_decimals(metadata), + }; + self.waiting_room.add(type_name::into_string(type_name), registration); + self.treasuries.add(type_name, tc); + + emit(TokenRegistrationEvent{ + type_name, + decimal: coin::get_decimals(metadata), + native_token: false + }); + } + + public(package) fun add_new_token( + self: &mut BridgeTreasury, + token_name: String, + token_id:u8, + native_token: bool, + notional_value: u64, + ) { + if (!native_token){ + assert!(notional_value > 0, EInvalidNotionalValue); + let ForeignTokenRegistration{ + type_name, + uc, + decimal, + } = bag::remove(&mut self.waiting_room, token_name); + let decimal_multiplier = math::pow(10, decimal); + self.supported_tokens.insert( + type_name, + BridgeTokenMetadata{ + id: token_id, + decimal_multiplier, + notional_value, + native_token + }, + ); + self.id_token_type_map.insert(token_id, type_name); + + // Freeze upgrade cap to prevent changes to the coin + transfer::public_freeze_object(uc); + + emit(NewTokenEvent{ + token_id, + type_name, + native_token, + decimal_multiplier, + notional_value + }) + } else { + // Not implemented for V1 + } + } + + public(package) fun create(ctx: &mut TxContext): BridgeTreasury { + BridgeTreasury { + treasuries: object_bag::new(ctx), + supported_tokens: vec_map::empty(), + id_token_type_map: vec_map::empty(), + waiting_room: bag::new(ctx), + } + } + + public(package) fun burn(self: &mut BridgeTreasury, token: Coin) { + let treasury = &mut self.treasuries[type_name::get()]; + coin::burn(treasury, token); + } + + public(package) fun mint( + self: &mut BridgeTreasury, + amount: u64, + ctx: &mut TxContext, + ): Coin { + let treasury = &mut self.treasuries[type_name::get()]; + coin::mint(treasury, amount, ctx) + } + + public(package) fun update_asset_notional_price( + self: &mut BridgeTreasury, + token_id: u8, + new_usd_price: u64, + ) { + let type_name = self.id_token_type_map.try_get(&token_id); + assert!(type_name.is_some(), EUnsupportedTokenType); + assert!(new_usd_price > 0, EInvalidNotionalValue); + let type_name = type_name.destroy_some(); + let metadata = self.supported_tokens.get_mut(&type_name); + metadata.notional_value = new_usd_price; + + emit(UpdateTokenPriceEvent { + token_id, + new_price: new_usd_price, + }) + } + + fun get_token_metadata(self: &BridgeTreasury): BridgeTokenMetadata { + let coin_type = type_name::get(); + let metadata = self.supported_tokens.try_get(&coin_type); + assert!(metadata.is_some(), EUnsupportedTokenType); + metadata.destroy_some() + } + + ////////////////////////////////////////////////////// + // Test functions + // + + #[test_only] + public struct ETH {} + #[test_only] + public struct BTC {} + #[test_only] + public struct USDT {} + #[test_only] + public struct USDC {} + + #[test_only] + public fun new_for_testing(ctx: &mut TxContext): BridgeTreasury { + create(ctx) + } + + #[test_only] + public fun mock_for_test(ctx: &mut TxContext): BridgeTreasury { + let mut treasury = new_for_testing(ctx); + treasury.setup_for_testing(); + treasury + } + + #[test_only] + public fun setup_for_testing(treasury: &mut BridgeTreasury) { + treasury.supported_tokens.insert(type_name::get(), BridgeTokenMetadata{ + id: 1, + decimal_multiplier: 100_000_000, + notional_value: 50_000 * USD_VALUE_MULTIPLIER, + native_token: false, + }); + treasury.supported_tokens.insert(type_name::get(), BridgeTokenMetadata{ + id: 2, + decimal_multiplier: 100_000_000, + notional_value: 3_000 * USD_VALUE_MULTIPLIER, + native_token: false, + }); + treasury.supported_tokens.insert(type_name::get(), BridgeTokenMetadata{ + id: 3, + decimal_multiplier: 1_000_000, + notional_value: USD_VALUE_MULTIPLIER, + native_token: false, + }); + treasury.supported_tokens.insert(type_name::get(), BridgeTokenMetadata{ + id: 4, + decimal_multiplier: 1_000_000, + notional_value: USD_VALUE_MULTIPLIER, + native_token: false, + }); + + treasury.id_token_type_map.insert(1, type_name::get()); + treasury.id_token_type_map.insert(2, type_name::get()); + treasury.id_token_type_map.insert(3, type_name::get()); + treasury.id_token_type_map.insert(4, type_name::get()); + } +} diff --git a/crates/sui-framework/packages/bridge/tests/bridge_tests.move b/crates/sui-framework/packages/bridge/tests/bridge_tests.move new file mode 100644 index 0000000000000..7e9beecd05187 --- /dev/null +++ b/crates/sui-framework/packages/bridge/tests/bridge_tests.move @@ -0,0 +1,713 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[test_only] +module bridge::bridge_tests { + use bridge::bridge::{ + assert_not_paused, assert_paused, create_bridge_for_testing, execute_system_message, + get_token_transfer_action_status, inner_limiter, inner_paused, + inner_treasury, inner_token_transfer_records, new_bridge_record_for_testing, + new_for_testing, send_token, test_execute_emergency_op, test_init_bridge_committee, + test_get_current_seq_num_and_increment, test_execute_update_asset_price, + test_execute_update_bridge_limit, test_get_token_transfer_action_signatures, + test_load_inner_mut, transfer_status_approved, transfer_status_claimed, + transfer_status_not_found, transfer_status_pending, + Bridge, + }; + use bridge::chain_ids; + use bridge::message::{Self, create_blocklist_message}; + use bridge::message_types; + use bridge::treasury::{BTC, ETH}; + + use sui::address; + use sui::balance; + use sui::coin::{Self, Coin}; + use sui::hex; + use sui::test_scenario::{Self, Scenario}; + use sui::test_utils::destroy; + + use sui_system::{ + governance_test_utils::{ + advance_epoch_with_reward_amounts, + create_sui_system_state_for_testing, + create_validator_for_testing, + }, + sui_system::{ + validator_voting_powers_for_testing, + SuiSystemState, + }, + }; + + #[test_only] + const VALIDATOR1_PUBKEY: vector = b"029bef8d556d80e43ae7e0becb3a7e6838b95defe45896ed6075bb9035d06c9964"; + #[test_only] + const VALIDATOR2_PUBKEY: vector = b"033e99a541db69bd32040dfe5037fbf5210dafa8151a71e21c5204b05d95ce0a62"; + + // common error start code for unexpected errors in tests (assertions). + // If more than one assert in a test needs to use an unexpected error code, + // use this as the starting error and add 1 to subsequent errors + const UNEXPECTED_ERROR: u64 = 10293847; + // use on tests that fail to save cleanup + const TEST_DONE: u64 = 74839201; + + // + // Utility functions + // + + // Info to set up a validator + public struct ValidatorInfo has copy, drop { + validator: address, + stake_amount: u64, + } + + // Add a validator to the chain + fun setup_validators( + scenario: &mut Scenario, + validators_info: vector, + sender: address, + ) { + scenario.next_tx(sender); + let ctx = scenario.ctx(); + let mut validators = vector::empty(); + let mut count = validators_info.length(); + while (count > 0) { + count = count - 1; + validators.push_back(create_validator_for_testing( + validators_info[count].validator, + validators_info[count].stake_amount, + ctx, + )); + }; + create_sui_system_state_for_testing(validators, 0, 0, ctx); + advance_epoch_with_reward_amounts(0, 0, scenario); + } + + // Set up an environment for the bridge with a set of + // validators, a bridge with a treasury and a committee. + // Save the Bridge as a shared object. + fun create_bridge_default(scenario: &mut Scenario) { + let sender = @0x0; + + let validators = vector[ + ValidatorInfo { validator: @0xA, stake_amount: 100 }, + ValidatorInfo { validator: @0xB, stake_amount: 100 }, + ValidatorInfo { validator: @0xC, stake_amount: 100 }, + ]; + setup_validators(scenario, validators, sender); + + create_bridge(scenario, sender); + } + + // Create a bridge and set up a treasury + fun create_bridge(scenario: &mut Scenario, sender: address) { + scenario.next_tx(sender); + let ctx = scenario.ctx(); + let chain_id = chain_ids::sui_testnet(); + create_bridge_for_testing(object::new(ctx), chain_id, ctx); + + scenario.next_tx(sender); + let mut bridge = scenario.take_shared(); + bridge.setup_treasury_for_testing(); + + test_scenario::return_shared(bridge); + } + + // Register two committee members + fun register_committee(scenario: &mut Scenario) { + scenario.next_tx(@0x0); + let mut bridge = scenario.take_shared(); + let mut system_state = test_scenario::take_shared(scenario); + + // register committee member `0xA` + scenario.next_tx(@0xA); + bridge.committee_registration( + &mut system_state, + hex::decode(VALIDATOR1_PUBKEY), + b"", + scenario.ctx(), + ); + + // register committee member `0xC` + scenario.next_tx(@0xC); + bridge.committee_registration( + &mut system_state, + hex::decode(VALIDATOR2_PUBKEY), + b"", + scenario.ctx(), + ); + + test_scenario::return_shared(bridge); + test_scenario::return_shared(system_state); + } + + // Init the bridge committee + fun init_committee(scenario: &mut Scenario, sender: address) { + // init committee + scenario.next_tx(sender); + let mut bridge = scenario.take_shared(); + let mut system_state = test_scenario::take_shared(scenario); + let voting_powers = validator_voting_powers_for_testing(&mut system_state); + bridge.test_init_bridge_committee( + voting_powers, + 50, + scenario.ctx(), + ); + test_scenario::return_shared(bridge); + test_scenario::return_shared(system_state); + } + + // Freeze the bridge + fun freeze_bridge(bridge: &mut Bridge, error: u64) { + let inner = bridge.test_load_inner_mut(); + // freeze it + let msg = message::create_emergency_op_message( + chain_ids::sui_testnet(), + 0, // seq num + 0, // freeze op + ); + let payload = msg.extract_emergency_op_payload(); + inner.test_execute_emergency_op(payload); + inner.assert_paused(error); + } + + // unfreeze the bridge + fun unfreeze_bridge(bridge: &mut Bridge, error: u64) { + let inner = bridge.test_load_inner_mut(); + // unfreeze it + let msg = message::create_emergency_op_message( + chain_ids::sui_testnet(), + 1, // seq num, this is supposed to be the next seq num but it's not what we test here + 1, // unfreeze op + ); + let payload = msg.extract_emergency_op_payload(); + inner.test_execute_emergency_op(payload); + inner.assert_not_paused(error); + } + + #[test] + fun test_bridge_create() { + let mut scenario = test_scenario::begin(@0x0); + create_bridge_default(&mut scenario); + + scenario.next_tx(@0xAAAA); + let mut bridge = scenario.take_shared(); + let inner = bridge.test_load_inner_mut(); + inner.assert_not_paused(UNEXPECTED_ERROR); + assert!(inner.inner_token_transfer_records().length() == 0, UNEXPECTED_ERROR + 1); + + test_scenario::return_shared(bridge); + scenario.end(); + } + + #[test] + #[expected_failure(abort_code = bridge::bridge::ENotSystemAddress)] + fun test_bridge_create_non_system_addr() { + let mut scenario = test_scenario::begin(@0x1); + create_bridge(&mut scenario, @0x1); + + abort TEST_DONE + } + + #[test] + fun test_init_committee() { + let mut scenario = test_scenario::begin(@0x0); + + create_bridge_default(&mut scenario); + register_committee(&mut scenario); + init_committee(&mut scenario, @0x0); + + scenario.end(); + } + + #[test] + fun test_init_committee_twice() { + let mut scenario = test_scenario::begin(@0x0); + + create_bridge_default(&mut scenario); + register_committee(&mut scenario); + init_committee(&mut scenario, @0x0); + init_committee(&mut scenario, @0x0); // second time is a no-op + + scenario.end(); + } + + #[test] + #[expected_failure(abort_code = bridge::bridge::ENotSystemAddress)] + fun test_init_committee_non_system_addr() { + let mut scenario = test_scenario::begin(@0x0); + + create_bridge_default(&mut scenario); + register_committee(&mut scenario); + init_committee(&mut scenario, @0xA); + + + abort TEST_DONE + } + + #[test] + #[expected_failure(abort_code = bridge::committee::ECommitteeAlreadyInitiated)] + fun test_register_committee_after_init() { + let mut scenario = test_scenario::begin(@0x0); + + create_bridge_default(&mut scenario); + register_committee(&mut scenario); + init_committee(&mut scenario, @0x0); + register_committee(&mut scenario); + + + abort TEST_DONE + } + + // #[test] + // fun test_register_foreign_token() { + // let mut scenario = test_scenario::begin(@0x0); + + // create_bridge_default(&mut scenario); + // register_committee(&mut scenario); + // init_committee(&mut scenario, @0x0); + + // scenario.next_tx(@0xAAAA); + // let mut bridge = scenario.take_shared(); + // bridge.register_foreign_token( + // tc: TreasuryCap, + // uc: UpgradeCap, + // metadata: &CoinMetadata, + // ); + + // scenario.end(); + // } + + // #[test] + // fun test_execute_send_token() { + // let mut scenario = test_scenario::begin(@0x0); + + // create_bridge_default(&mut scenario); + // register_committee(&mut scenario); + // init_committee(&mut scenario, @0x0); + + // scenario.next_tx(@0xAAAA); + // let mut bridge = scenario.take_shared(); + // let eth_address = b"01234"; // it does not really matter + // let btc: Coin = coin::mint_for_testing(1, scenario.ctx()); + // bridge.send_token( + // chain_ids::eth_sepolia(), + // eth_address, + // btc, + // scenario.ctx(), + // ); + // test_scenario::return_shared(bridge); + + // scenario.end(); + // } + + #[test] + #[expected_failure(abort_code = bridge::bridge::EBridgeUnavailable)] + fun test_execute_send_token_frozen() { + let mut scenario = test_scenario::begin(@0x0); + let ctx = scenario.ctx(); + let chain_id = chain_ids::sui_testnet(); + let mut bridge = new_for_testing(chain_id, ctx); + + assert!(!bridge.test_load_inner_mut().inner_paused(), UNEXPECTED_ERROR); + freeze_bridge(&mut bridge, UNEXPECTED_ERROR + 1); + + let eth_address = b"01234"; // it does not really matter + let btc: Coin = coin::mint_for_testing(1, ctx); + bridge.send_token( + chain_ids::eth_sepolia(), + eth_address, + btc, + ctx, + ); + + abort TEST_DONE + } + + #[test] + #[expected_failure(abort_code = bridge::bridge::EInvalidBridgeRoute)] + fun test_execute_send_token_invalid_route() { + let mut scenario = test_scenario::begin(@0x0); + let ctx = scenario.ctx(); + let chain_id = chain_ids::sui_testnet(); + let mut bridge = new_for_testing(chain_id, ctx); + + let eth_address = b"01234"; // it does not really matter + let btc: Coin = coin::mint_for_testing(1, ctx); + bridge.send_token( + chain_ids::eth_mainnet(), + eth_address, + btc, + ctx, + ); + + abort TEST_DONE + } + + #[test] + #[expected_failure(abort_code = bridge::bridge::EUnexpectedChainID)] + fun test_system_msg_incorrect_chain_id() { + let mut scenario = test_scenario::begin(@0x0); + let ctx = scenario.ctx(); + let chain_id = chain_ids::sui_testnet(); + let mut bridge = new_for_testing(chain_id, ctx); + let blocklist = create_blocklist_message(chain_ids::sui_mainnet(), 0, 0, vector[]); + bridge.execute_system_message(blocklist, vector[]); + + abort TEST_DONE + } + + #[test] + fun test_get_seq_num_and_increment() { + let mut scenario = test_scenario::begin(@0x0); + let ctx = scenario.ctx(); + let chain_id = chain_ids::sui_testnet(); + let mut bridge = new_for_testing(chain_id, ctx); + + let inner = bridge.test_load_inner_mut(); + assert!( + inner.test_get_current_seq_num_and_increment( + message_types::committee_blocklist(), + ) == 0, + UNEXPECTED_ERROR, + ); + assert!( + inner.sequence_nums()[&message_types::committee_blocklist()] == 1, + UNEXPECTED_ERROR + 1, + ); + assert!( + inner.test_get_current_seq_num_and_increment( + message_types::committee_blocklist(), + ) == 1, + UNEXPECTED_ERROR + 2, + ); + // other message type nonce does not change + assert!( + !inner.sequence_nums().contains(&message_types::token()), + UNEXPECTED_ERROR + 3, + ); + assert!( + !inner.sequence_nums().contains(&message_types::emergency_op()), + UNEXPECTED_ERROR + 4, + ); + assert!( + !inner.sequence_nums().contains(&message_types::update_bridge_limit()), + UNEXPECTED_ERROR + 5, + ); + assert!( + !inner.sequence_nums().contains(&message_types::update_asset_price()), + UNEXPECTED_ERROR + 6, + ); + assert!( + inner.test_get_current_seq_num_and_increment(message_types::token()) == 0, + UNEXPECTED_ERROR + 7, + ); + assert!( + inner.test_get_current_seq_num_and_increment( + message_types::emergency_op(), + ) == 0, + UNEXPECTED_ERROR + 8, + ); + assert!( + inner.test_get_current_seq_num_and_increment( + message_types::update_bridge_limit(), + ) == 0, + UNEXPECTED_ERROR + 6, + ); + assert!( + inner.test_get_current_seq_num_and_increment( + message_types::update_asset_price(), + ) == 0, + UNEXPECTED_ERROR + 7, + ); + + destroy(bridge); + scenario.end(); + } + + #[test] + fun test_update_limit() { + let mut scenario = test_scenario::begin(@0x0); + let ctx = scenario.ctx(); + let chain_id = chain_ids::sui_mainnet(); + let mut bridge = new_for_testing(chain_id, ctx); + let inner = bridge.test_load_inner_mut(); + + // Assert the starting limit is a different value + assert!( + inner.inner_limiter().get_route_limit( + &chain_ids::get_route( + chain_ids::eth_mainnet(), + chain_ids::sui_mainnet(), + ), + ) != 1, + UNEXPECTED_ERROR, + ); + // now shrink to 1 for SUI mainnet -> ETH mainnet + let msg = message::create_update_bridge_limit_message( + chain_ids::sui_mainnet(), // receiving_chain + 0, + chain_ids::eth_mainnet(), // sending_chain + 1, + ); + let payload = msg.extract_update_bridge_limit(); + inner.test_execute_update_bridge_limit(payload); + + // should be 1 now + assert!( + inner.inner_limiter().get_route_limit( + &chain_ids::get_route( + chain_ids::eth_mainnet(), + chain_ids::sui_mainnet() + ), + ) == 1, + UNEXPECTED_ERROR + 1, + ); + // other routes are not impacted + assert!( + inner.inner_limiter().get_route_limit( + &chain_ids::get_route( + chain_ids::eth_sepolia(), + chain_ids::sui_testnet(), + ), + ) != 1, + UNEXPECTED_ERROR + 2, + ); + + destroy(bridge); + scenario.end(); + } + + #[test] + #[expected_failure(abort_code = bridge::bridge::EUnexpectedChainID)] + fun test_execute_update_bridge_limit_abort_with_unexpected_chain_id() { + let mut scenario = test_scenario::begin(@0x0); + let ctx = scenario.ctx(); + let chain_id = chain_ids::sui_testnet(); + let mut bridge = new_for_testing(chain_id, ctx); + let inner = bridge.test_load_inner_mut(); + + // shrink to 1 for SUI mainnet -> ETH mainnet + let msg = message::create_update_bridge_limit_message( + chain_ids::sui_mainnet(), // receiving_chain + 0, + chain_ids::eth_mainnet(), // sending_chain + 1, + ); + let payload = msg.extract_update_bridge_limit(); + // This abort because the receiving_chain (sui_mainnet) is not the same as + // the bridge's chain_id (sui_devnet) + inner.test_execute_update_bridge_limit(payload); + + abort TEST_DONE + } + + + #[test] + fun test_update_asset_price() { + let mut scenario = test_scenario::begin(@0x0); + let ctx = scenario.ctx(); + let chain_id = chain_ids::sui_testnet(); + let mut bridge = new_for_testing(chain_id, ctx); + let inner = bridge.test_load_inner_mut(); + + // Assert the starting limit is a different value + assert!( + inner.inner_treasury().notional_value() != 1_001_000_000, + UNEXPECTED_ERROR, + ); + // now change it to 100_001_000 + let msg = message::create_update_asset_price_message( + inner.inner_treasury().token_id(), + chain_ids::sui_mainnet(), + 0, + 1_001_000_000, + ); + let payload = msg.extract_update_asset_price(); + inner.test_execute_update_asset_price(payload); + + // should be 1_001_000_000 now + assert!( + inner.inner_treasury().notional_value() == 1_001_000_000, + UNEXPECTED_ERROR + 1, + ); + // other assets are not impacted + assert!( + inner.inner_treasury().notional_value() != 1_001_000_000, + UNEXPECTED_ERROR + 2, + ); + + destroy(bridge); + scenario.end(); + } + + #[test] + fun test_test_execute_emergency_op() { + let mut scenario = test_scenario::begin(@0x0); + let ctx = scenario.ctx(); + let chain_id = chain_ids::sui_testnet(); + let mut bridge = new_for_testing(chain_id, ctx); + + assert!(!bridge.test_load_inner_mut().inner_paused(), UNEXPECTED_ERROR); + freeze_bridge(&mut bridge, UNEXPECTED_ERROR + 1); + + assert!(bridge.test_load_inner_mut().inner_paused(), UNEXPECTED_ERROR + 2); + unfreeze_bridge(&mut bridge, UNEXPECTED_ERROR + 3); + + destroy(bridge); + scenario.end(); + } + + #[test] + #[expected_failure(abort_code = bridge::bridge::EBridgeNotPaused)] + fun test_test_execute_emergency_op_abort_when_not_frozen() { + let mut scenario = test_scenario::begin(@0x0); + let ctx = scenario.ctx(); + let chain_id = chain_ids::sui_testnet(); + let mut bridge = new_for_testing(chain_id, ctx); + + assert!(!bridge.test_load_inner_mut().inner_paused(), UNEXPECTED_ERROR); + // unfreeze it, should abort + unfreeze_bridge(&mut bridge, UNEXPECTED_ERROR + 1); + + abort TEST_DONE + } + + #[test] + #[expected_failure(abort_code = bridge::bridge::EBridgeAlreadyPaused)] + fun test_test_execute_emergency_op_abort_when_already_frozen() { + let mut scenario = test_scenario::begin(@0x0); + let ctx = scenario.ctx(); + let chain_id = chain_ids::sui_testnet(); + let mut bridge = new_for_testing(chain_id, ctx); + let inner = bridge.test_load_inner_mut(); + + // initially it's unfrozen + assert!(!inner.inner_paused(), UNEXPECTED_ERROR); + // freeze it + let msg = message::create_emergency_op_message( + chain_ids::sui_testnet(), + 0, // seq num + 0, // freeze op + ); + let payload = msg.extract_emergency_op_payload(); + inner.test_execute_emergency_op(payload); + + // should be frozen now + assert!(inner.inner_paused(), UNEXPECTED_ERROR + 1); + + // freeze it again, should abort + let msg = message::create_emergency_op_message( + chain_ids::sui_testnet(), + 1, // seq num, should be the next seq num but it's not what we test here + 0, // unfreeze op + ); + let payload = msg.extract_emergency_op_payload(); + inner.test_execute_emergency_op(payload); + + abort TEST_DONE + } + + #[test] + fun test_get_token_transfer_action_status() { + let mut scenario = test_scenario::begin(@0x0); + let ctx = scenario.ctx(); + let chain_id = chain_ids::sui_testnet(); + let mut bridge = new_for_testing(chain_id, ctx); + let coin = coin::mint_for_testing(12345, ctx); + + // Test when pending + let message = message::create_token_bridge_message( + chain_ids::sui_testnet(), // source chain + 10, // seq_num + address::to_bytes(ctx.sender()), // sender address + chain_ids::eth_sepolia(), // target_chain + hex::decode(b"00000000000000000000000000000000000000c8"), // target_address + 1u8, // token_type + coin.balance().value(), + ); + + let key = message.key(); + bridge.test_load_inner_mut().inner_token_transfer_records().push_back( + key, + new_bridge_record_for_testing(message, option::none(), false), + ); + assert!( + bridge.get_token_transfer_action_status(chain_id, 10) + == transfer_status_pending(), + UNEXPECTED_ERROR, + ); + assert!( + bridge.test_get_token_transfer_action_signatures(chain_id, 10) == option::none(), + UNEXPECTED_ERROR + 1, + ); + + // Test when ready for claim + let message = message::create_token_bridge_message( + chain_ids::sui_testnet(), // source chain + 11, // seq_num + address::to_bytes(ctx.sender()), // sender address + chain_ids::eth_sepolia(), // target_chain + hex::decode(b"00000000000000000000000000000000000000c8"), // target_address + 1u8, // token_type + balance::value(coin::balance(&coin)) + ); + let key = message.key(); + bridge.test_load_inner_mut().inner_token_transfer_records().push_back( + key, + new_bridge_record_for_testing(message, option::some(vector[]), false), + ); + assert!( + bridge.get_token_transfer_action_status(chain_id, 11) + == transfer_status_approved(), + UNEXPECTED_ERROR + 2, + ); + assert!( + bridge.test_get_token_transfer_action_signatures(chain_id, 11) + == option::some(vector[]), + UNEXPECTED_ERROR + 3, + ); + + // Test when already claimed + let message = message::create_token_bridge_message( + chain_ids::sui_testnet(), // source chain + 12, // seq_num + address::to_bytes(ctx.sender()), // sender address + chain_ids::eth_sepolia(), // target_chain + hex::decode(b"00000000000000000000000000000000000000c8"), // target_address + 1u8, // token_type + balance::value(coin::balance(&coin)) + ); + let key = message.key(); + bridge.test_load_inner_mut().inner_token_transfer_records().push_back( + key, + new_bridge_record_for_testing(message, option::some(vector[b"1234"]), true), + ); + assert!( + bridge.get_token_transfer_action_status(chain_id, 12) + == transfer_status_claimed(), + UNEXPECTED_ERROR + 3, + ); + assert!( + bridge.test_get_token_transfer_action_signatures(chain_id, 12) + == option::some(vector[b"1234"]), + UNEXPECTED_ERROR + 4, + ); + + // Test when message not found + assert!( + bridge.get_token_transfer_action_status(chain_id, 13) + == transfer_status_not_found(), + UNEXPECTED_ERROR + 5, + ); + assert!( + bridge.test_get_token_transfer_action_signatures(chain_id, 13) + == option::none(), + UNEXPECTED_ERROR + 6, + ); + + destroy(bridge); + coin.burn_for_testing(); + scenario.end(); + } +} + diff --git a/crates/sui-framework/packages/bridge/tests/commitee_test.move b/crates/sui-framework/packages/bridge/tests/commitee_test.move new file mode 100644 index 0000000000000..55b9a5080befa --- /dev/null +++ b/crates/sui-framework/packages/bridge/tests/commitee_test.move @@ -0,0 +1,563 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[test_only] +module bridge::committee_test { + + use sui::vec_map; + use sui_system::sui_system; + use sui_system::sui_system::SuiSystemState; + + use bridge::committee::{ + BridgeCommittee, CommitteeMember, blocklisted, bridge_pubkey_bytes, create, + members, member_registrations, + register, try_create_next_committee, verify_signatures, voting_power, + }; + use bridge::committee::execute_blocklist; + use bridge::committee::make_committee_member; + use bridge::committee::make_bridge_committee; + use bridge::crypto; + use bridge::message; + + use sui::{hex, test_scenario, test_utils::{Self, assert_eq}}; + use bridge::chain_ids; + use sui_system::governance_test_utils::{ + advance_epoch_with_reward_amounts, + create_sui_system_state_for_testing, + create_validator_for_testing + }; + + // This is a token transfer message for testing + const TEST_MSG: vector = + b"00010a0000000000000000200000000000000000000000000000000000000000000000000000000000000064012000000000000000000000000000000000000000000000000000000000000000c8033930000000000000"; + + const VALIDATOR1_PUBKEY: vector = b"029bef8d556d80e43ae7e0becb3a7e6838b95defe45896ed6075bb9035d06c9964"; + const VALIDATOR2_PUBKEY: vector = b"033e99a541db69bd32040dfe5037fbf5210dafa8151a71e21c5204b05d95ce0a62"; + const VALIDATOR3_PUBKEY: vector = b"033e99a541db69bd32040dfe5037fbf5210dafa8151a71e21c5204b05d95ce0a63"; + + #[test] + fun test_verify_signatures_good_path() { + let committee = setup_test(); + let msg = message::deserialize_message_test_only(hex::decode(TEST_MSG)); + // good path + committee.verify_signatures( + msg, + vector[hex::decode( + b"8ba030a450cb1e36f61e572645fc9da1dea5f79b6db663a21ab63286d7fc29af447433abdd0c0b35ab751154ac5b612ae64d3be810f0d9e10ff68e764514ced300" + ), hex::decode( + b"439379cc7b3ee3ebe1ff59d011dafc1caac47da6919b089c90f6a24e8c284b963b20f1f5421385456e57ac6b69c4b5f0d345aa09b8bc96d88d87051c7349e83801" + )], + ); + + // Clean up + test_utils::destroy(committee) + } + + #[test] + #[expected_failure(abort_code = bridge::committee::EDuplicatedSignature)] + fun test_verify_signatures_duplicated_sig() { + let committee = setup_test(); + let msg = message::deserialize_message_test_only(hex::decode(TEST_MSG)); + // good path + committee.verify_signatures( + msg, + vector[hex::decode( + b"439379cc7b3ee3ebe1ff59d011dafc1caac47da6919b089c90f6a24e8c284b963b20f1f5421385456e57ac6b69c4b5f0d345aa09b8bc96d88d87051c7349e83801" + ), hex::decode( + b"439379cc7b3ee3ebe1ff59d011dafc1caac47da6919b089c90f6a24e8c284b963b20f1f5421385456e57ac6b69c4b5f0d345aa09b8bc96d88d87051c7349e83801" + )], + ); + abort 0 + } + + #[test] + #[expected_failure(abort_code = bridge::committee::EInvalidSignature)] + fun test_verify_signatures_invalid_signature() { + let committee = setup_test(); + let msg = message::deserialize_message_test_only(hex::decode(TEST_MSG)); + // good path + committee.verify_signatures( + msg, + vector[hex::decode( + b"6ffb3e5ce04dd138611c49520fddfbd6778879c2db4696139f53a487043409536c369c6ffaca165ce3886723cfa8b74f3e043e226e206ea25e313ea2215e6caf01" + )], + ); + abort 0 + } + + #[test] + #[expected_failure(abort_code = bridge::committee::ESignatureBelowThreshold)] + fun test_verify_signatures_below_threshold() { + let committee = setup_test(); + let msg = message::deserialize_message_test_only(hex::decode(TEST_MSG)); + // good path + committee.verify_signatures( + msg, + vector[hex::decode( + b"439379cc7b3ee3ebe1ff59d011dafc1caac47da6919b089c90f6a24e8c284b963b20f1f5421385456e57ac6b69c4b5f0d345aa09b8bc96d88d87051c7349e83801" + )], + ); + abort 0 + } + + #[test] + fun test_init_committee() { + let mut scenario = test_scenario::begin(@0x0); + let ctx = test_scenario::ctx(&mut scenario); + let mut committee = create(ctx); + + let validators = vector[ + create_validator_for_testing(@0xA, 100, ctx), + create_validator_for_testing(@0xC, 100, ctx) + ]; + create_sui_system_state_for_testing(validators, 0, 0, ctx); + advance_epoch_with_reward_amounts(0, 0, &mut scenario); + test_scenario::next_tx(&mut scenario, @0x0); + + let mut system_state = test_scenario::take_shared(&scenario); + + // validator registration + committee.register( + &mut system_state, + hex::decode(VALIDATOR1_PUBKEY), + b"", + &tx(@0xA, 0), + ); + committee.register( + &mut system_state, + hex::decode(VALIDATOR2_PUBKEY), + b"", + &tx(@0xC, 0), + ); + + // Check committee before creation + assert!(committee.members().is_empty(), 0); + + let ctx = test_scenario::ctx(&mut scenario); + let voting_powers = system_state.validator_voting_powers_for_testing(); + committee.try_create_next_committee(voting_powers, 6000, ctx); + + assert_eq(2, committee.members().size()); + let (_, member0) = committee.members().get_entry_by_idx(0); + let (_, member1) = committee.members().get_entry_by_idx(1); + assert_eq(5000, member0.voting_power()); + assert_eq(5000, member1.voting_power()); + + let members = committee.members(); + assert!(members.size() == 2, 0); // must succeed + + test_utils::destroy(committee); + test_scenario::return_shared(system_state); + test_scenario::end(scenario); + } + + #[test] + #[expected_failure(abort_code = bridge::committee::ENotSystemAddress)] + fun test_init_non_system_sender() { + let mut scenario = test_scenario::begin(@0x1); + let ctx = test_scenario::ctx(&mut scenario); + let _committee = create(ctx); + + abort 0 + } + + #[test] + #[expected_failure(abort_code = bridge::committee::ESenderNotActiveValidator)] + fun test_init_committee_not_validator() { + let mut scenario = test_scenario::begin(@0x0); + let ctx = test_scenario::ctx(&mut scenario); + let mut committee = create(ctx); + + let validators = vector[ + create_validator_for_testing(@0xA, 100, ctx), + create_validator_for_testing(@0xC, 100, ctx) + ]; + create_sui_system_state_for_testing(validators, 0, 0, ctx); + advance_epoch_with_reward_amounts(0, 0, &mut scenario); + test_scenario::next_tx(&mut scenario, @0x0); + + let mut system_state = test_scenario::take_shared(&scenario); + + // validator registration + committee.register(&mut system_state, hex::decode(VALIDATOR1_PUBKEY), b"", &tx(@0xD, 0)); + + test_utils::destroy(committee); + test_scenario::return_shared(system_state); + test_scenario::end(scenario); + } + + #[test] + #[expected_failure(abort_code = bridge::committee::EDuplicatePubkey)] + fun test_init_committee_dup_pubkey() { + let mut scenario = test_scenario::begin(@0x0); + let ctx = test_scenario::ctx(&mut scenario); + let mut committee = create(ctx); + + let validators = vector[ + create_validator_for_testing(@0xA, 100, ctx), + create_validator_for_testing(@0xC, 100, ctx) + ]; + create_sui_system_state_for_testing(validators, 0, 0, ctx); + advance_epoch_with_reward_amounts(0, 0, &mut scenario); + test_scenario::next_tx(&mut scenario, @0x0); + + let mut system_state = test_scenario::take_shared(&scenario); + + // validator registration + committee.register(&mut system_state, hex::decode(VALIDATOR1_PUBKEY), b"", &tx(@0xA, 0)); + committee.register(&mut system_state, hex::decode(VALIDATOR1_PUBKEY), b"", &tx(@0xC, 0)); + + test_utils::destroy(committee); + test_scenario::return_shared(system_state); + test_scenario::end(scenario); + } + + #[test] + fun test_init_committee_validator_become_inactive() { + let mut scenario = test_scenario::begin(@0x0); + let ctx = test_scenario::ctx(&mut scenario); + let mut committee = create(ctx); + + let validators = vector[ + create_validator_for_testing(@0xA, 100, ctx), + create_validator_for_testing(@0xC, 100, ctx), + create_validator_for_testing(@0xD, 100, ctx), + create_validator_for_testing(@0xE, 100, ctx), + create_validator_for_testing(@0xF, 100, ctx) + ]; + create_sui_system_state_for_testing(validators, 0, 0, ctx); + advance_epoch_with_reward_amounts(0, 0, &mut scenario); + test_scenario::next_tx(&mut scenario, @0x0); + + let mut system_state = test_scenario::take_shared(&scenario); + + // validator registration, 3 validators registered, should have 60% voting power in total + committee.register(&mut system_state, hex::decode(VALIDATOR1_PUBKEY), b"", &tx(@0xA, 0)); + committee.register(&mut system_state, hex::decode(VALIDATOR2_PUBKEY), b"", &tx(@0xC, 0)); + committee.register(&mut system_state, hex::decode(VALIDATOR3_PUBKEY), b"", &tx(@0xD, 0)); + + // Verify validator registration + assert_eq(3, committee.member_registrations().size()); + + // Validator 0xA become inactive, total voting power become 50% + sui_system::request_remove_validator(&mut system_state, &mut tx(@0xA, 0)); + test_scenario::return_shared(system_state); + advance_epoch_with_reward_amounts(0, 0, &mut scenario); + + let mut system_state = test_scenario::take_shared(&scenario); + + // create committee should not create a committe because of not enough stake. + let ctx = test_scenario::ctx(&mut scenario); + let voting_powers = sui_system::validator_voting_powers_for_testing(&mut system_state); + try_create_next_committee(&mut committee, voting_powers, 6000, ctx); + + assert!(committee.members().is_empty(), 0); + + test_utils::destroy(committee); + test_scenario::return_shared(system_state); + test_scenario::end(scenario); + } + + #[test] + fun test_update_committee_registration() { + let mut scenario = test_scenario::begin(@0x0); + let ctx = test_scenario::ctx(&mut scenario); + let mut committee = create(ctx); + + let validators = vector[ + create_validator_for_testing(@0xA, 100, ctx), + create_validator_for_testing(@0xC, 100, ctx) + ]; + create_sui_system_state_for_testing(validators, 0, 0, ctx); + advance_epoch_with_reward_amounts(0, 0, &mut scenario); + test_scenario::next_tx(&mut scenario, @0x0); + + let mut system_state = test_scenario::take_shared(&scenario); + + // validator registration + committee.register(&mut system_state, hex::decode(VALIDATOR1_PUBKEY), b"", &tx(@0xA, 0)); + + // Verify registration info + assert_eq(1, committee.member_registrations().size()); + let (address, registration) = committee.member_registrations().get_entry_by_idx(0); + assert_eq(@0xA, *address); + assert!( + &hex::decode(VALIDATOR1_PUBKEY) == registration.bridge_pubkey_bytes(), + 0, + ); + + // Register again with different pub key. + committee.register(&mut system_state, hex::decode(VALIDATOR2_PUBKEY), b"", &tx(@0xA, 0)); + + // Verify registration info, registration count should still be 1 + assert_eq(1, committee.member_registrations().size()); + let (address, registration) = committee.member_registrations().get_entry_by_idx(0); + assert_eq(@0xA, *address); + assert!( + &hex::decode(VALIDATOR2_PUBKEY) == registration.bridge_pubkey_bytes(), + 0, + ); + + // teardown + test_utils::destroy(committee); + test_scenario::return_shared(system_state); + test_scenario::end(scenario); + } + + #[test] + fun test_init_committee_not_enough_stake() { + let mut scenario = test_scenario::begin(@0x0); + let ctx = test_scenario::ctx(&mut scenario); + let mut committee = create(ctx); + + let validators = vector[ + create_validator_for_testing(@0xA, 100, ctx), + create_validator_for_testing(@0xC, 100, ctx) + ]; + create_sui_system_state_for_testing(validators, 0, 0, ctx); + advance_epoch_with_reward_amounts(0, 0, &mut scenario); + test_scenario::next_tx(&mut scenario, @0x0); + + let mut system_state = test_scenario::take_shared(&scenario); + + // validator registration + committee.register(&mut system_state, hex::decode(VALIDATOR1_PUBKEY), b"", &tx(@0xA, 0)); + + // Check committee before creation + assert!(committee.members().is_empty(), 0); + + let ctx = test_scenario::ctx(&mut scenario); + let voting_powers = sui_system::validator_voting_powers_for_testing(&mut system_state); + try_create_next_committee(&mut committee, voting_powers, 6000, ctx); + + // committee should be empty because registration did not reach min stake threshold. + assert!(committee.members().is_empty(), 0); + + test_utils::destroy(committee); + test_scenario::return_shared(system_state); + test_scenario::end(scenario); + } + + #[test] + #[expected_failure(abort_code = bridge::committee::ECommitteeAlreadyInitiated)] + fun test_register_already_initialized() { + let mut scenario = test_scenario::begin(@0x0); + let ctx = test_scenario::ctx(&mut scenario); + let mut committee = create(ctx); + + let validators = vector[ + create_validator_for_testing(@0xA, 100, ctx), + create_validator_for_testing(@0xC, 100, ctx) + ]; + create_sui_system_state_for_testing(validators, 0, 0, ctx); + advance_epoch_with_reward_amounts(0, 0, &mut scenario); + + test_scenario::next_tx(&mut scenario, @0x0); + let mut system_state = test_scenario::take_shared(&scenario); + committee.register(&mut system_state, hex::decode(VALIDATOR1_PUBKEY), b"", &tx(@0xA, 0)); + committee.register(&mut system_state, hex::decode(VALIDATOR2_PUBKEY), b"", &tx(@0xC, 0)); + assert!(committee.members().is_empty(), 0); + let ctx = test_scenario::ctx(&mut scenario); + let voting_powers = sui_system::validator_voting_powers_for_testing(&mut system_state); + try_create_next_committee(&mut committee, voting_powers, 6000, ctx); + + test_scenario::next_tx(&mut scenario, @0x0); + assert!(committee.members().size() == 2, 1000); // must succeed + // this fails because committee is already initiated + committee.register(&mut system_state, hex::decode(VALIDATOR1_PUBKEY), b"", &tx(@0xA, 0)); + + abort 0 + } + + #[test] + #[expected_failure(abort_code = bridge::committee::EInvalidPubkeyLength)] + fun test_register_bad_pubkey() { + let mut scenario = test_scenario::begin(@0x0); + let ctx = test_scenario::ctx(&mut scenario); + let mut committee = create(ctx); + + let validators = vector[ + create_validator_for_testing(@0xA, 100, ctx), + create_validator_for_testing(@0xC, 100, ctx) + ]; + create_sui_system_state_for_testing(validators, 0, 0, ctx); + advance_epoch_with_reward_amounts(0, 0, &mut scenario); + + test_scenario::next_tx(&mut scenario, @0x0); + let mut system_state = test_scenario::take_shared(&scenario); + committee.register(&mut system_state, hex::decode(VALIDATOR2_PUBKEY), b"", &tx(@0xC, 0)); + // this fails with invalid public key + committee.register(&mut system_state, b"029bef8", b"", &tx(@0xA, 0)); + + abort 0 + } + + + fun tx(sender: address, hint: u64): TxContext { + tx_context::new_from_hint(sender, hint, 1, 0, 0) + } + + #[test] + #[expected_failure(abort_code = bridge::committee::ESignatureBelowThreshold)] + fun test_verify_signatures_with_blocked_committee_member() { + let mut committee = setup_test(); + let msg = message::deserialize_message_test_only(hex::decode(TEST_MSG)); + // good path, this test should have passed in previous test + committee.verify_signatures( + msg, + vector[hex::decode( + b"8ba030a450cb1e36f61e572645fc9da1dea5f79b6db663a21ab63286d7fc29af447433abdd0c0b35ab751154ac5b612ae64d3be810f0d9e10ff68e764514ced300" + ), hex::decode( + b"439379cc7b3ee3ebe1ff59d011dafc1caac47da6919b089c90f6a24e8c284b963b20f1f5421385456e57ac6b69c4b5f0d345aa09b8bc96d88d87051c7349e83801" + )], + ); + + let (validator1, member) = committee.members().get_entry_by_idx(0); + assert!(!member.blocklisted(), 0); + + // Block a member + let blocklist = message::create_blocklist_message( + chain_ids::sui_testnet(), + 0, + 0, // type 0 is block + vector[crypto::ecdsa_pub_key_to_eth_address(validator1)] + ); + let blocklist = message::extract_blocklist_payload(&blocklist); + execute_blocklist(&mut committee, blocklist); + + let (_, blocked_member) = committee.members().get_entry_by_idx(0); + assert!(blocked_member.blocklisted(), 0); + + // Verify signature should fail now + committee.verify_signatures( + msg, + vector[hex::decode( + b"8ba030a450cb1e36f61e572645fc9da1dea5f79b6db663a21ab63286d7fc29af447433abdd0c0b35ab751154ac5b612ae64d3be810f0d9e10ff68e764514ced300" + ), hex::decode( + b"439379cc7b3ee3ebe1ff59d011dafc1caac47da6919b089c90f6a24e8c284b963b20f1f5421385456e57ac6b69c4b5f0d345aa09b8bc96d88d87051c7349e83801" + )], + ); + + // Clean up + test_utils::destroy(committee); + } + + #[test] + #[expected_failure(abort_code = bridge::committee::EValidatorBlocklistContainsUnknownKey)] + fun test_execute_blocklist_abort_upon_unknown_validator() { + let mut committee = setup_test(); + + // // val0 and val1 are not blocked yet + let (validator0, _) = committee.members().get_entry_by_idx(0); + // assert!(!member0.blocklisted(), 0); + // let (validator1, member1) = committee.members().get_entry_by_idx(1); + // assert!(!member1.blocklisted(), 0); + + let eth_address0 = crypto::ecdsa_pub_key_to_eth_address(validator0); + let invalid_eth_address1 = x"0000000000000000000000000000000000000000"; + + // Blocklist both + let blocklist = message::create_blocklist_message( + chain_ids::sui_testnet(), + 0, // seq + 0, // type 0 is blocklist + vector[eth_address0, invalid_eth_address1] + ); + let blocklist = message::extract_blocklist_payload(&blocklist); + execute_blocklist(&mut committee, blocklist); + + // Clean up + test_utils::destroy(committee); + } + + #[test] + fun test_execute_blocklist() { + let mut committee = setup_test(); + + // val0 and val1 are not blocked yet + let (validator0, member0) = committee.members().get_entry_by_idx(0); + assert!(!member0.blocklisted(), 0); + let (validator1, member1) = committee.members().get_entry_by_idx(1); + assert!(!member1.blocklisted(), 0); + + let eth_address0 = crypto::ecdsa_pub_key_to_eth_address(validator0); + let eth_address1 = crypto::ecdsa_pub_key_to_eth_address(validator1); + + // Blocklist both + let blocklist = message::create_blocklist_message( + chain_ids::sui_testnet(), + 0, // seq + 0, // type 0 is blocklist + vector[eth_address0, eth_address1] + ); + let blocklist = message::extract_blocklist_payload(&blocklist); + execute_blocklist(&mut committee, blocklist); + + // Blocklist both reverse order + let blocklist = message::create_blocklist_message( + chain_ids::sui_testnet(), + 0, // seq + 0, // type 0 is blocklist + vector[eth_address1, eth_address0] + ); + let blocklist = message::extract_blocklist_payload(&blocklist); + execute_blocklist(&mut committee, blocklist); + + // val 0 is blocklisted + let (_, blocked_member) = committee.members().get_entry_by_idx(0); + assert!(blocked_member.blocklisted(), 0); + // val 1 is too + let (_, blocked_member) = committee.members().get_entry_by_idx(1); + assert!(blocked_member.blocklisted(), 0); + + // unblocklist val1 + let blocklist = message::create_blocklist_message( + chain_ids::sui_testnet(), + 1, // seq, this is supposed to increment, but we don't test it here + 1, // type 1 is unblocklist + vector[eth_address1], + ); + let blocklist = message::extract_blocklist_payload(&blocklist); + execute_blocklist(&mut committee, blocklist); + + // val 0 is still blocklisted + let (_, blocked_member) = committee.members().get_entry_by_idx(0); + assert!(blocked_member.blocklisted(), 0); + // val 1 is not + let (_, blocked_member) = committee.members().get_entry_by_idx(1); + assert!(!blocked_member.blocklisted(), 0); + + // Clean up + test_utils::destroy(committee); + } + + fun setup_test(): BridgeCommittee { + let mut members = vec_map::empty, CommitteeMember>(); + + let bridge_pubkey_bytes = hex::decode(VALIDATOR1_PUBKEY); + members.insert( + bridge_pubkey_bytes, + make_committee_member( + @0xA, + bridge_pubkey_bytes, + 3333, + b"https://127.0.0.1:9191", + false, + ), + ); + + let bridge_pubkey_bytes = hex::decode(VALIDATOR2_PUBKEY); + members.insert( + bridge_pubkey_bytes, + make_committee_member( + @0xC, + bridge_pubkey_bytes, + 3333, + b"https://127.0.0.1:9192", + false, + ), + ); + + make_bridge_committee(members, vec_map::empty(), 1) + } +} \ No newline at end of file diff --git a/crates/sui-framework/packages/bridge/tests/limiter_tests.move b/crates/sui-framework/packages/bridge/tests/limiter_tests.move new file mode 100644 index 0000000000000..6d51cf3d85caf --- /dev/null +++ b/crates/sui-framework/packages/bridge/tests/limiter_tests.move @@ -0,0 +1,595 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[test_only] +module bridge::limiter_tests { + + use bridge::{ + chain_ids, + limiter::{ + check_and_record_sending_transfer, make_transfer_limiter, + max_transfer_limit, new, + transfer_limits_mut, total_amount, transfer_records, + update_route_limit, usd_value_multiplier, + }, + treasury::{Self, BTC, ETH, USDC, USDT}, + }; + + use sui::clock; + use sui::test_scenario; + use sui::test_utils::{assert_eq, destroy}; + + #[test] + fun test_24_hours_windows() { + let mut limiter = make_transfer_limiter(); + + let route = chain_ids::get_route(chain_ids::sui_custom(), chain_ids::eth_sepolia()); + + let mut scenario = test_scenario::begin(@0x1); + let ctx = test_scenario::ctx(&mut scenario); + let mut treasury = treasury::mock_for_test(ctx); + + // Global transfer limit is 100M USD + limiter.transfer_limits_mut().insert(route, 100_000_000 * usd_value_multiplier()); + // Notional price for ETH is 5 USD + let id = treasury::token_id(&treasury); + treasury.update_asset_notional_price(id, 5 * usd_value_multiplier()); + + let mut clock = clock::create_for_testing(ctx); + clock.set_for_testing(1706288001377); + + // transfer 10000 ETH every hour, the totol should be 10000 * 5 + assert!( + limiter.check_and_record_sending_transfer( + &treasury, + &clock, + route, + 10_000 * treasury.decimal_multiplier(), + ), + 0, + ); + + let record = limiter.transfer_records().get(&route); + assert!(record.total_amount() == 10000 * 5 * usd_value_multiplier(), 0); + + // transfer 1000 ETH every hour for 50 hours, the 24 hours totol should be 24000 * 10 + let mut i = 0; + while (i < 50) { + clock.increment_for_testing(60 * 60 * 1000); + assert!( + limiter.check_and_record_sending_transfer( + &treasury, + &clock, + route, + 1_000 * treasury.decimal_multiplier(), + ), + 0, + ); + i = i + 1; + }; + let record = limiter.transfer_records().get(&route); + let mut expected_value = 24000 * 5 * usd_value_multiplier(); + assert_eq(record.total_amount(), expected_value); + + // transfer 1000 * i ETH every hour for 24 hours, the 24 hours + // totol should be 300 * 1000 * 5 + let mut i = 0; + // At this point, every hour in past 24 hour has value $5000. + // In each iteration, the old $5000 gets replaced with (i * 5000) + while (i < 24) { + clock.increment_for_testing(60 * 60 * 1000); + assert!( + limiter.check_and_record_sending_transfer( + &treasury, + &clock, + route, + 1_000 * treasury.decimal_multiplier() * (i + 1), + ), + 0 + ); + + let record = limiter.transfer_records().get(&route); + + expected_value = expected_value + 1000 * 5 * i * usd_value_multiplier(); + assert_eq(record.total_amount(), expected_value); + i = i + 1; + }; + + let record = limiter.transfer_records().get(&route); + assert_eq(record.total_amount(), 300 * 1000 * 5 * usd_value_multiplier()); + + destroy(limiter); + destroy(treasury); + clock::destroy_for_testing(clock); + test_scenario::end(scenario); + } + + #[test] + fun test_24_hours_windows_multiple_route() { + let mut limiter = make_transfer_limiter(); + + let route = chain_ids::get_route(chain_ids::sui_custom(), chain_ids::eth_sepolia()); + let route2 = chain_ids::get_route(chain_ids::eth_sepolia(), chain_ids::sui_custom()); + + let mut scenario = test_scenario::begin(@0x1); + let ctx = test_scenario::ctx(&mut scenario); + let mut treasury = treasury::mock_for_test(ctx); + + // Global transfer limit is 1M USD + limiter.transfer_limits_mut().insert(route, 1_000_000 * usd_value_multiplier()); + limiter.transfer_limits_mut().insert(route2, 500_000 * usd_value_multiplier()); + // Notional price for ETH is 5 USD + let id = treasury::token_id(&treasury); + treasury.update_asset_notional_price(id, 5 * usd_value_multiplier()); + + let mut clock = clock::create_for_testing(ctx); + clock.set_for_testing(1706288001377); + + // Transfer 10000 ETH on route 1 + assert!( + limiter.check_and_record_sending_transfer( + &treasury, + &clock, + route, + 10_000 * treasury::decimal_multiplier(&treasury), + ), + 0, + ); + // Transfer 50000 ETH on route 2 + assert!( + limiter.check_and_record_sending_transfer( + &treasury, + &clock, + route2, + 50_000 * treasury::decimal_multiplier(&treasury), + ), + 0, + ); + + let record = limiter.transfer_records().get(&route); + assert!(record.total_amount() == 10000 * 5 * usd_value_multiplier(), 0); + + let record = limiter.transfer_records().get(&route2); + assert!(record.total_amount() == 50000 * 5 * usd_value_multiplier(), 0); + + destroy(limiter); + destroy(treasury); + clock::destroy_for_testing(clock); + test_scenario::end(scenario); + } + + #[test] + fun test_exceed_limit() { + let mut limiter = make_transfer_limiter(); + + let mut scenario = test_scenario::begin(@0x1); + let ctx = test_scenario::ctx(&mut scenario); + let mut treasury = treasury::mock_for_test(ctx); + + let route = chain_ids::get_route(chain_ids::sui_custom(), chain_ids::eth_sepolia()); + // Global transfer limit is 1M USD + limiter.transfer_limits_mut().insert(route, 1_000_000 * usd_value_multiplier()); + // Notional price for ETH is 10 USD + let id = treasury::token_id(&treasury); + treasury.update_asset_notional_price(id, 10 * usd_value_multiplier()); + + let mut clock = clock::create_for_testing(ctx); + clock.set_for_testing(1706288001377); + + assert!( + limiter.check_and_record_sending_transfer( + &treasury, + &clock, + route, + 90_000 * treasury::decimal_multiplier(&treasury), + ), + 0, + ); + + let record = limiter.transfer_records().get(&route); + assert_eq(record.total_amount(), 90000 * 10 * usd_value_multiplier()); + + clock.increment_for_testing(60 * 60 * 1000); + assert!( + limiter.check_and_record_sending_transfer( + &treasury, + &clock, + route, + 10_000 * treasury::decimal_multiplier(&treasury), + ), + 0, + ); + let record = limiter.transfer_records().get(&route); + assert_eq(record.total_amount(), 100000 * 10 * usd_value_multiplier()); + + // Tx should fail with a tiny amount because the limit is hit + assert!( + !limiter.check_and_record_sending_transfer(&treasury, &clock, route, 1), + 0, + ); + assert!( + !limiter.check_and_record_sending_transfer( + &treasury, + &clock, + route, + 90_000 * treasury::decimal_multiplier(&treasury), + ), + 0, + ); + + // Fast forward 23 hours, now the first 90k should be discarded + clock.increment_for_testing(60 * 60 * 1000 * 23); + assert!( + limiter.check_and_record_sending_transfer( + &treasury, + &clock, + route, + 90_000 * treasury::decimal_multiplier(&treasury), + ), + 0, + ); + let record = limiter.transfer_records().get(&route); + assert_eq(record.total_amount(), 100000 * 10 * usd_value_multiplier()); + + // But now limit is hit again + assert!( + !limiter.check_and_record_sending_transfer(&treasury, &clock, route, 1), + 0, + ); + let record = limiter.transfer_records().get(&route); + assert_eq(record.total_amount(), 100000 * 10 * usd_value_multiplier()); + + destroy(limiter); + destroy(treasury); + clock::destroy_for_testing(clock); + test_scenario::end(scenario); + } + + #[test] + #[expected_failure(abort_code = bridge::limiter::ELimitNotFoundForRoute)] + fun test_limiter_does_not_limit_receiving_transfers() { + let mut limiter = new(); + + let route = chain_ids::get_route(chain_ids::sui_mainnet(), chain_ids::eth_mainnet()); + let mut scenario = test_scenario::begin(@0x1); + let ctx = scenario.ctx(); + let treasury = treasury::mock_for_test(ctx); + let mut clock = clock::create_for_testing(ctx); + clock.set_for_testing(1706288001377); + // We don't limit sui -> eth transfers. This aborts with `ELimitNotFoundForRoute` + limiter.check_and_record_sending_transfer( + &treasury, + &clock, + route, 1 * treasury::decimal_multiplier(&treasury), + ); + destroy(limiter); + destroy(treasury); + clock::destroy_for_testing(clock); + test_scenario::end(scenario); + } + + #[test] + fun test_limiter_basic_op() { + // In this test we use very simple number for easier calculation. + let mut limiter = make_transfer_limiter(); + + let mut scenario = test_scenario::begin(@0x1); + let ctx = test_scenario::ctx(&mut scenario); + let mut treasury = treasury::mock_for_test(ctx); + + let route = chain_ids::get_route(chain_ids::eth_sepolia(), chain_ids::sui_testnet()); + // Global transfer limit is 100 USD + limiter.transfer_limits_mut().insert(route, 100 * usd_value_multiplier()); + // BTC: $10, ETH: $2.5, USDC: $1, USDT: $0.5 + let id = treasury::token_id(&treasury); + treasury.update_asset_notional_price(id, 10 * usd_value_multiplier()); + let id = treasury::token_id(&treasury); + let eth_price = 250000000; + treasury.update_asset_notional_price(id, eth_price); + let id = treasury::token_id(&treasury); + treasury.update_asset_notional_price(id, 1 * usd_value_multiplier()); + let id = treasury::token_id(&treasury); + treasury.update_asset_notional_price(id, 50000000); + + let mut clock = clock::create_for_testing(ctx); + clock.set_for_testing(36082800000); // hour 10023 + + // hour 0 (10023): $15 * 2.5 = $37.5 + // 15 eth = $37.5 + assert!( + limiter.check_and_record_sending_transfer( + &treasury, + &clock, + route, + 15 * treasury::decimal_multiplier(&treasury), + ), + 0, + ); + let record = limiter.transfer_records().get(&route); + assert_eq(record.hour_head(), 10023); + assert_eq(record.hour_tail(), 10000); + assert!( + record.per_hour_amounts() == + &vector[ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 15 * eth_price, + ], + 0, + ); + assert_eq(record.total_amount(), 15 * eth_price); + + // hour 0 (10023): $37.5 + $10 = $47.5 + // 10 uddc = $10 + assert!( + limiter.check_and_record_sending_transfer( + &treasury, + &clock, + route, 10 * treasury::decimal_multiplier(&treasury), + ), + 0, + ); + let record = limiter.transfer_records().get(&route); + assert_eq(record.hour_head(), 10023); + assert_eq(record.hour_tail(), 10000); + let expected_notion_amount_10023 = 15 * eth_price + 10 * usd_value_multiplier(); + assert!( + record.per_hour_amounts() == + &vector[ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + expected_notion_amount_10023, + ], + 0, + ); + assert_eq(record.total_amount(), expected_notion_amount_10023); + + // hour 1 (10024): $20 + clock.increment_for_testing(60 * 60 * 1000); + // 2 btc = $20 + assert!( + limiter.check_and_record_sending_transfer( + &treasury, + &clock, + route, 2 * treasury::decimal_multiplier(&treasury), + ), + 0, + ); + let record = limiter.transfer_records().get(&route); + assert_eq(record.hour_head(), 10024); + assert_eq(record.hour_tail(), 10001); + let expected_notion_amount_10024 = 20 * usd_value_multiplier(); + assert!( + record.per_hour_amounts() == + &vector[ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + expected_notion_amount_10023, + expected_notion_amount_10024, + ], + 0, + ); + assert_eq(record.total_amount(), expected_notion_amount_10023 + expected_notion_amount_10024); + + // Fast forward 22 hours, now hour 23 (10046): try to transfer $33 willf fail + clock.increment_for_testing(60 * 60 * 1000 * 22); + // fail + // 65 usdt = $33 + assert!( + !limiter.check_and_record_sending_transfer( + &treasury, &clock, route, 66 * 1_000_000, + ), + 0, + ); + // but window slid + let record = limiter.transfer_records().get(&route); + assert_eq(record.hour_head(), 10046); + assert_eq(record.hour_tail(), 10023); + assert!( + record.per_hour_amounts() == + &vector[ + expected_notion_amount_10023, expected_notion_amount_10024, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + 0, + ); + assert_eq(record.total_amount(), expected_notion_amount_10023 + expected_notion_amount_10024); + + // hour 23 (10046): $32.5 deposit will succeed + // 65 usdt = $32.5 + assert!( + limiter.check_and_record_sending_transfer( + &treasury, &clock, route, 65 * 1_000_000, + ), + 0, + ); + let record = limiter.transfer_records().get(&route); + let expected_notion_amount_10046 = 325 * usd_value_multiplier() / 10; + assert_eq(record.hour_head(), 10046); + assert_eq(record.hour_tail(), 10023); + assert!( + record.per_hour_amounts() == + &vector[ + expected_notion_amount_10023, + expected_notion_amount_10024, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + expected_notion_amount_10046, + ], + 0, + ); + assert_eq(record.total_amount(), expected_notion_amount_10023 + expected_notion_amount_10024 + expected_notion_amount_10046); + + // Hour 24 (10047), we can deposit $0.5 now + clock.increment_for_testing(60 * 60 * 1000); + // 1 usdt = $0.5 + assert!( + limiter.check_and_record_sending_transfer(&treasury, &clock, route, 1_000_000), + 0, + ); + let record = limiter.transfer_records().get(&route); + let expected_notion_amount_10047 = 5 * usd_value_multiplier() / 10; + assert_eq(record.hour_head(), 10047); + assert_eq(record.hour_tail(), 10024); + assert!( + record.per_hour_amounts() == + &vector[ + expected_notion_amount_10024, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + expected_notion_amount_10046, + expected_notion_amount_10047, + ], + 0, + ); + assert_eq(record.total_amount(), expected_notion_amount_10024 + expected_notion_amount_10046 + expected_notion_amount_10047); + + // Fast forward to Hour 30 (10053) + clock.increment_for_testing(60 * 60 * 1000 * 6); + // 1 usdc = $1 + assert!( + limiter.check_and_record_sending_transfer( + &treasury, + &clock, + route, + 1 * treasury::decimal_multiplier(&treasury), + ), + 0, + ); + let record = limiter.transfer_records().get(&route); + let expected_notion_amount_10053 = 1 * usd_value_multiplier(); + assert_eq(record.hour_head(), 10053); + assert_eq(record.hour_tail(), 10030); + assert!( + record.per_hour_amounts() == + &vector[ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + expected_notion_amount_10046, + expected_notion_amount_10047, + 0, 0, 0, 0, 0, + expected_notion_amount_10053, + ], + 0, + ); + assert_eq(record.total_amount(), expected_notion_amount_10046 + expected_notion_amount_10047 + expected_notion_amount_10053); + + // Fast forward to hour 130 (10153) + clock.increment_for_testing(60 * 60 * 1000 * 100); + // 1 usdc = $1 + assert!( + limiter.check_and_record_sending_transfer( + &treasury, + &clock, + route, + treasury::decimal_multiplier(&treasury), + ), + 0, + ); + let record = limiter.transfer_records().get(&route); + let expected_notion_amount_10153 = 1 * usd_value_multiplier(); + assert_eq(record.hour_head(), 10153); + assert_eq(record.hour_tail(), 10130); + assert!( + record.per_hour_amounts() == + &vector[ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + expected_notion_amount_10153, + ], + 0, + ); + assert_eq(record.total_amount(), expected_notion_amount_10153); + + destroy(limiter); + destroy(treasury); + clock::destroy_for_testing(clock); + test_scenario::end(scenario); + } + + #[test] + fun test_update_route_limit() { + // default routes, default notion values + let mut limiter = new(); + assert_eq( + limiter.transfer_limits()[ + &chain_ids::get_route(chain_ids::eth_mainnet(), chain_ids::sui_mainnet()) + ], + 5_000_000 * usd_value_multiplier(), + ); + + assert_eq( + limiter.transfer_limits()[ + &chain_ids::get_route(chain_ids::eth_sepolia(), chain_ids::sui_testnet()) + ], + max_transfer_limit(), + ); + + // shrink testnet limit + update_route_limit(&mut limiter, &chain_ids::get_route(chain_ids::eth_sepolia(), chain_ids::sui_testnet()), 1_000 * usd_value_multiplier()); + assert_eq( + limiter.transfer_limits()[ + &chain_ids::get_route(chain_ids::eth_sepolia(), chain_ids::sui_testnet()) + ], + 1_000 * usd_value_multiplier(), + ); + // mainnet route does not change + assert_eq( + limiter.transfer_limits()[ + &chain_ids::get_route(chain_ids::eth_mainnet(), chain_ids::sui_mainnet()) + ], + 5_000_000 * usd_value_multiplier(), + ); + destroy(limiter); + } + + #[test] + fun test_update_route_limit_all_paths() { + let mut limiter = new(); + // pick an existing route limit + assert_eq( + limiter.transfer_limits()[ + &chain_ids::get_route(chain_ids::eth_sepolia(), chain_ids::sui_testnet()) + ], + max_transfer_limit(), + ); + let new_limit = 1_000 * usd_value_multiplier(); + update_route_limit(&mut limiter, &chain_ids::get_route(chain_ids::eth_sepolia(), chain_ids::sui_testnet()), new_limit); + assert_eq( + limiter.transfer_limits()[ + &chain_ids::get_route(chain_ids::eth_sepolia(), chain_ids::sui_testnet()) + ], + new_limit, + ); + + // pick a new route limit + update_route_limit(&mut limiter, &chain_ids::get_route(chain_ids::sui_testnet(), chain_ids::eth_sepolia()), new_limit); + assert_eq( + limiter.transfer_limits()[ + &chain_ids::get_route(chain_ids::eth_sepolia(), chain_ids::sui_testnet()) + ], + new_limit, + ); + + + destroy(limiter); + } + + #[test] + fun test_update_asset_price() { + // default routes, default notion values + let mut scenario = test_scenario::begin(@0x1); + let ctx = test_scenario::ctx(&mut scenario); + let mut treasury = treasury::mock_for_test(ctx); + + assert_eq(treasury.notional_value(), (50_000 * usd_value_multiplier())); + assert_eq(treasury.notional_value(), (3_000 * usd_value_multiplier())); + assert_eq(treasury.notional_value(), (1 * usd_value_multiplier())); + assert_eq(treasury.notional_value(), (1 * usd_value_multiplier())); + // change usdt price + let id = treasury.token_id(); + treasury.update_asset_notional_price(id, 11 * usd_value_multiplier() / 10); + assert_eq(treasury.notional_value(), (11 * usd_value_multiplier() / 10)); + // other prices do not change + assert_eq(treasury.notional_value(), (50_000 * usd_value_multiplier())); + assert_eq(treasury.notional_value(), (3_000 * usd_value_multiplier())); + assert_eq(treasury.notional_value(), (1 * usd_value_multiplier())); + scenario.end(); + destroy(treasury); + } +} diff --git a/crates/sui-framework/packages/bridge/tests/message_tests.move b/crates/sui-framework/packages/bridge/tests/message_tests.move new file mode 100644 index 0000000000000..c7783bfb32052 --- /dev/null +++ b/crates/sui-framework/packages/bridge/tests/message_tests.move @@ -0,0 +1,462 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[test_only] +module bridge::message_tests { + use bridge::{ + chain_ids, + message::{ + blocklist_validator_addresses, + create_add_tokens_on_sui_message, create_blocklist_message, + create_emergency_op_message, create_token_bridge_message, + create_update_asset_price_message, create_update_bridge_limit_message, + deserialize_message_test_only, extract_add_tokens_on_sui, + extract_blocklist_payload, extract_token_bridge_payload, + extract_update_asset_price, extract_update_bridge_limit, make_add_token_on_sui, + make_payload, peel_u64_be_for_testing, reverse_bytes_test, + serialize_message, + update_asset_price_payload_token_id, + update_bridge_limit_payload_limit, + update_bridge_limit_payload_receiving_chain, + update_bridge_limit_payload_sending_chain, + }, + treasury::{Self, BTC, ETH, USDC}, + }; + use std::ascii; + use sui::{address, balance, coin, hex, test_scenario, test_utils::{assert_eq, destroy}}; + use sui::bcs; + + #[test] + fun test_message_serialization_sui_to_eth() { + let sender_address = address::from_u256(100); + let mut scenario = test_scenario::begin(sender_address); + let ctx = test_scenario::ctx(&mut scenario); + + let coin = coin::mint_for_testing(12345, ctx); + + let token_bridge_message = create_token_bridge_message( + chain_ids::sui_testnet(), // source chain + 10, // seq_num + address::to_bytes(sender_address), // sender address + chain_ids::eth_sepolia(), // target_chain + // Eth address is 20 bytes long + hex::decode(b"00000000000000000000000000000000000000c8"), // target_address + 3u8, // token_type + balance::value(coin::balance(&coin)) // amount: u64 + ); + + // Test payload extraction + let token_payload = make_payload( + address::to_bytes(sender_address), + chain_ids::eth_sepolia(), + hex::decode(b"00000000000000000000000000000000000000c8"), + 3u8, + balance::value(coin::balance(&coin)) + ); + assert!(token_bridge_message.extract_token_bridge_payload() == token_payload, 0); + + // Test message serialization + let message = serialize_message(token_bridge_message); + let expected_msg = hex::decode( + b"0001000000000000000a012000000000000000000000000000000000000000000000000000000000000000640b1400000000000000000000000000000000000000c8030000000000003039", + ); + + assert!(message == expected_msg, 0); + assert!(token_bridge_message == deserialize_message_test_only(message), 0); + + coin::burn_for_testing(coin); + test_scenario::end(scenario); + } + + #[test] + fun test_message_serialization_eth_to_sui() { + let address_1 = address::from_u256(100); + let mut scenario = test_scenario::begin(address_1); + let ctx = test_scenario::ctx(&mut scenario); + + let coin = coin::mint_for_testing(12345, ctx); + + let token_bridge_message = create_token_bridge_message( + chain_ids::eth_sepolia(), // source chain + 10, // seq_num + // Eth address is 20 bytes long + hex::decode(b"00000000000000000000000000000000000000c8"), // eth sender address + chain_ids::sui_testnet(), // target_chain + address::to_bytes(address_1), // target address + 3u8, // token_type + balance::value(coin::balance(&coin)) // amount: u64 + ); + + // Test payload extraction + let token_payload = make_payload( + hex::decode(b"00000000000000000000000000000000000000c8"), + chain_ids::sui_testnet(), + address::to_bytes(address_1), + 3u8, + balance::value(coin::balance(&coin)), + ); + assert!(token_bridge_message.extract_token_bridge_payload() == token_payload, 0); + + + // Test message serialization + let message = serialize_message(token_bridge_message); + let expected_msg = hex::decode( + b"0001000000000000000a0b1400000000000000000000000000000000000000c801200000000000000000000000000000000000000000000000000000000000000064030000000000003039", + ); + assert!(message == expected_msg, 0); + assert!(token_bridge_message == deserialize_message_test_only(message), 0); + + coin::burn_for_testing(coin); + test_scenario::end(scenario); + } + + #[test] + fun test_emergency_op_message_serialization() { + let emergency_op_message = create_emergency_op_message( + chain_ids::sui_testnet(), // source chain + 10, // seq_num + 0, + ); + + // Test message serialization + let message = serialize_message(emergency_op_message); + let expected_msg = hex::decode( + b"0201000000000000000a0100", + ); + + assert!(message == expected_msg, 0); + assert!(emergency_op_message == deserialize_message_test_only(message), 0); + } + + // Do not change/remove this test, it uses move bytes generated by Rust + #[test] + fun test_emergency_op_message_serialization_regression() { + let emergency_op_message = create_emergency_op_message( + chain_ids::sui_custom(), + 55, // seq_num + 0, // pause + ); + + // Test message serialization + let message = serialize_message(emergency_op_message); + let expected_msg = hex::decode( + b"020100000000000000370200", + ); + + assert_eq(expected_msg, message); + assert!(emergency_op_message == deserialize_message_test_only(message), 0); + } + + #[test] + fun test_blocklist_message_serialization() { + let validator_pub_key1 = hex::decode(b"b14d3c4f5fbfbcfb98af2d330000d49c95b93aa7"); + let validator_pub_key2 = hex::decode(b"f7e93cc543d97af6632c9b8864417379dba4bf15"); + + let validator_eth_addresses = vector[validator_pub_key1, validator_pub_key2]; + let blocklist_message = create_blocklist_message( + chain_ids::sui_testnet(), // source chain + 10, // seq_num + 0, + validator_eth_addresses + ); + // Test message serialization + let message = serialize_message(blocklist_message); + + let expected_msg = hex::decode( + b"0101000000000000000a010002b14d3c4f5fbfbcfb98af2d330000d49c95b93aa7f7e93cc543d97af6632c9b8864417379dba4bf15", + ); + + assert!(message == expected_msg, 0); + assert!(blocklist_message == deserialize_message_test_only(message), 0); + + let blocklist = blocklist_message.extract_blocklist_payload(); + assert!(blocklist.blocklist_validator_addresses() == validator_eth_addresses, 0) + } + + // Do not change/remove this test, it uses move bytes generated by Rust + #[test] + fun test_blocklist_message_serialization_regression() { + let validator_eth_addr_1 = hex::decode(b"68b43fd906c0b8f024a18c56e06744f7c6157c65"); + let validator_eth_addr_2 = hex::decode(b"acaef39832cb995c4e049437a3e2ec6a7bad1ab5"); + // Test 1 + let validator_eth_addresses = vector[validator_eth_addr_1]; + let blocklist_message = create_blocklist_message( + chain_ids::sui_custom(), // source chain + 129, // seq_num + 0, // blocklist + validator_eth_addresses + ); + // Test message serialization + let message = serialize_message(blocklist_message); + + let expected_msg = hex::decode( + b"0101000000000000008102000168b43fd906c0b8f024a18c56e06744f7c6157c65", + ); + + assert_eq(expected_msg, message); + assert!(blocklist_message == deserialize_message_test_only(message), 0); + + let blocklist = blocklist_message.extract_blocklist_payload(); + assert!(blocklist.blocklist_validator_addresses() == validator_eth_addresses, 0); + + // Test 2 + let validator_eth_addresses = vector[validator_eth_addr_1, validator_eth_addr_2]; + let blocklist_message = create_blocklist_message( + chain_ids::sui_custom(), // source chain + 68, // seq_num + 1, // unblocklist + validator_eth_addresses + ); + // Test message serialization + let message = serialize_message(blocklist_message); + + let expected_msg = hex::decode( + b"0101000000000000004402010268b43fd906c0b8f024a18c56e06744f7c6157c65acaef39832cb995c4e049437a3e2ec6a7bad1ab5", + ); + + assert_eq(expected_msg, message); + assert!(blocklist_message == deserialize_message_test_only(message), 0); + + let blocklist = blocklist_message.extract_blocklist_payload(); + assert!(blocklist.blocklist_validator_addresses() == validator_eth_addresses, 0) + } + + #[test] + fun test_update_bridge_limit_message_serialization() { + let update_bridge_limit = create_update_bridge_limit_message( + chain_ids::sui_testnet(), // source chain + 10, // seq_num + chain_ids::eth_sepolia(), + 1000000000 + ); + + // Test message serialization + let message = serialize_message(update_bridge_limit); + let expected_msg = hex::decode( + b"0301000000000000000a010b000000003b9aca00", + ); + + assert!(message == expected_msg, 0); + assert!(update_bridge_limit == deserialize_message_test_only(message), 0); + + let bridge_limit = extract_update_bridge_limit(&update_bridge_limit); + assert!( + bridge_limit.update_bridge_limit_payload_receiving_chain() + == chain_ids::sui_testnet(), + 0, + ); + assert!( + bridge_limit.update_bridge_limit_payload_sending_chain() + == chain_ids::eth_sepolia(), + 0, + ); + assert!(bridge_limit.update_bridge_limit_payload_limit() == 1000000000, 0); + } + + // Do not change/remove this test, it uses move bytes generated by Rust + #[test] + fun test_update_bridge_limit_message_serialization_regression() { + let update_bridge_limit = create_update_bridge_limit_message( + chain_ids::sui_custom(), // source chain + 15, // seq_num + chain_ids::eth_custom(), + 10_000_000_000 // 1M USD + ); + + // Test message serialization + let message = serialize_message(update_bridge_limit); + let expected_msg = hex::decode( + b"0301000000000000000f020c00000002540be400", + ); + + assert_eq(message, expected_msg); + assert!(update_bridge_limit == deserialize_message_test_only(message), 0); + + let bridge_limit = extract_update_bridge_limit(&update_bridge_limit); + assert!( + bridge_limit.update_bridge_limit_payload_receiving_chain() + == chain_ids::sui_custom(), + 0, + ); + assert!( + bridge_limit.update_bridge_limit_payload_sending_chain() + == chain_ids::eth_custom(), + 0, + ); + assert!(bridge_limit.update_bridge_limit_payload_limit() == 10_000_000_000, 0); + } + + #[test] + fun test_update_asset_price_message_serialization() { + let asset_price_message = create_update_asset_price_message( + 2, + chain_ids::sui_testnet(), // source chain + 10, // seq_num + 12345 + ); + + // Test message serialization + let message = serialize_message(asset_price_message); + let expected_msg = hex::decode( + b"0401000000000000000a01020000000000003039", + ); + assert!(message == expected_msg, 0); + assert!(asset_price_message == deserialize_message_test_only(message), 0); + + let asset_price = extract_update_asset_price(&asset_price_message); + + let mut scenario = test_scenario::begin(@0x1); + let ctx = test_scenario::ctx(&mut scenario); + let treasury = treasury::mock_for_test(ctx); + + assert!( + asset_price.update_asset_price_payload_token_id() + == treasury::token_id(&treasury), + 0, + ); + assert!(asset_price.update_asset_price_payload_new_price() == 12345, 0); + + destroy(treasury); + test_scenario::end(scenario); + } + + // Do not change/remove this test, it uses move bytes generated by Rust + #[test] + fun test_update_asset_price_message_serialization_regression() { + let mut scenario = test_scenario::begin(@0x1); + let ctx = test_scenario::ctx(&mut scenario); + let treasury = treasury::mock_for_test(ctx); + + let asset_price_message = create_update_asset_price_message( + treasury.token_id(), + chain_ids::sui_custom(), // source chain + 266, // seq_num + 1_000_000_000 // $100k USD + ); + + // Test message serialization + let message = serialize_message(asset_price_message); + let expected_msg = hex::decode( + b"0401000000000000010a0201000000003b9aca00", + ); + assert_eq(expected_msg, message); + assert!(asset_price_message == deserialize_message_test_only(message), 0); + + let asset_price = extract_update_asset_price(&asset_price_message); + + assert!( + asset_price.update_asset_price_payload_token_id() + == treasury::token_id(&treasury), + 0, + ); + assert!(asset_price.update_asset_price_payload_new_price() == 1_000_000_000, 0); + + destroy(treasury); + test_scenario::end(scenario); + } + + #[test] + fun test_add_tokens_on_sui_message_serialization() { + let mut scenario = test_scenario::begin(@0x1); + let ctx = test_scenario::ctx(&mut scenario); + let treasury = treasury::mock_for_test(ctx); + + let add_tokens_on_sui_message = create_add_tokens_on_sui_message( + chain_ids::sui_custom(), + 1, // seq_num + false, // native_token + vector[treasury.token_id(), treasury.token_id()], + vector[ascii::string(b"28ac483b6f2b62dd58abdf0bbc3f86900d86bbdc710c704ba0b33b7f1c4b43c8::btc::BTC"), ascii::string(b"0xbd69a54e7c754a332804f325307c6627c06631dc41037239707e3242bc542e99::eth::ETH")], + vector[100, 100] + ); + let payload = add_tokens_on_sui_message.extract_add_tokens_on_sui(); + assert!( + payload == make_add_token_on_sui( + false, + vector[treasury.token_id(), treasury.token_id()], + vector[ascii::string(b"28ac483b6f2b62dd58abdf0bbc3f86900d86bbdc710c704ba0b33b7f1c4b43c8::btc::BTC"), ascii::string(b"0xbd69a54e7c754a332804f325307c6627c06631dc41037239707e3242bc542e99::eth::ETH")], + vector[100, 100], + ), + 0, + ); + // Test message serialization + let message = serialize_message(add_tokens_on_sui_message); + let expected_msg = hex::decode( + b"060100000000000000010200020102024a323861633438336236663262363264643538616264663062626333663836393030643836626264633731306337303462613062333362376631633462343363383a3a6274633a3a4254434c3078626436396135346537633735346133333238303466333235333037633636323763303636333164633431303337323339373037653332343262633534326539393a3a6574683a3a4554480264000000000000006400000000000000", + ); + assert_eq(message, expected_msg); + assert!(add_tokens_on_sui_message == deserialize_message_test_only(message), 0); + + destroy(treasury); + test_scenario::end(scenario); + } + + #[test] + fun test_add_tokens_on_sui_message_serialization_2() { + let mut scenario = test_scenario::begin(@0x1); + let ctx = test_scenario::ctx(&mut scenario); + let treasury = treasury::mock_for_test(ctx); + + let add_tokens_on_sui_message = create_add_tokens_on_sui_message( + chain_ids::sui_custom(), + 0, // seq_num + false, // native_token + vector[1, 2, 3, 4], + vector[ + ascii::string(b"9b5e13bcd0cb23ff25c07698e89d48056c745338d8c9dbd033a4172b87027073::btc::BTC"), + ascii::string(b"7970d71c03573f540a7157f0d3970e117effa6ae16cefd50b45c749670b24e6a::eth::ETH"), + ascii::string(b"500e429a24478405d5130222b20f8570a746b6bc22423f14b4d4e6a8ea580736::usdc::USDC"), + ascii::string(b"46bfe51da1bd9511919a92eb1154149b36c0f4212121808e13e3e5857d607a9c::usdt::USDT") + ], + vector[500_000_000, 30_000_000, 1_000, 1_000] + ); + let payload = add_tokens_on_sui_message.extract_add_tokens_on_sui(); + assert!( + payload == make_add_token_on_sui( + false, + vector[1, 2, 3, 4], + vector[ + ascii::string(b"9b5e13bcd0cb23ff25c07698e89d48056c745338d8c9dbd033a4172b87027073::btc::BTC"), + ascii::string(b"7970d71c03573f540a7157f0d3970e117effa6ae16cefd50b45c749670b24e6a::eth::ETH"), + ascii::string(b"500e429a24478405d5130222b20f8570a746b6bc22423f14b4d4e6a8ea580736::usdc::USDC"), + ascii::string(b"46bfe51da1bd9511919a92eb1154149b36c0f4212121808e13e3e5857d607a9c::usdt::USDT") + ], + vector[500_000_000, 30_000_000, 1_000, 1_000], + ), + 0, + ); + // Test message serialization + let message = serialize_message(add_tokens_on_sui_message); + let expected_msg = hex::decode( + b"0601000000000000000002000401020304044a396235653133626364306362323366663235633037363938653839643438303536633734353333386438633964626430333361343137326238373032373037333a3a6274633a3a4254434a373937306437316330333537336635343061373135376630643339373065313137656666613661653136636566643530623435633734393637306232346536613a3a6574683a3a4554484c353030653432396132343437383430356435313330323232623230663835373061373436623662633232343233663134623464346536613865613538303733363a3a757364633a3a555344434c343662666535316461316264393531313931396139326562313135343134396233366330663432313231323138303865313365336535383537643630376139633a3a757364743a3a55534454040065cd1d0000000080c3c90100000000e803000000000000e803000000000000", + ); + assert_eq(message, expected_msg); + assert!(add_tokens_on_sui_message == deserialize_message_test_only(message), 0); + + let mut message_bytes = b"SUI_BRIDGE_MESSAGE"; + message_bytes.append(message); + + let pubkey = sui::ecdsa_k1::secp256k1_ecrecover( + &x"b75e64b040eef6fa510e4b9be853f0d35183de635c6456c190714f9546b163ba12583e615a2e9944ec2d21b520aebd9b14e181dcae0fcc6cdaefc0aa235b3abe00" + , &message_bytes, 0); + + assert_eq(pubkey, x"025a8c385af9a76aa506c395e240735839cb06531301f9b396e5f9ef8eeb0d8879"); + destroy(treasury); + test_scenario::end(scenario); + } + + #[test] + fun test_be_to_le_conversion() { + let input = hex::decode(b"78563412"); + let expected = hex::decode(b"12345678"); + assert!(reverse_bytes_test(input) == expected, 0) + } + + #[test] + public(package) fun test_peel_u64_be() { + let input = hex::decode(b"0000000000003039"); + let expected = 12345u64; + let mut bcs = bcs::new(input); + assert!(peel_u64_be_for_testing(&mut bcs) == expected, 0) + } +} diff --git a/crates/sui-framework/packages/sui-framework/sources/object.move b/crates/sui-framework/packages/sui-framework/sources/object.move index fe8f69efa0f1d..5b1a388bb2450 100644 --- a/crates/sui-framework/packages/sui-framework/sources/object.move +++ b/crates/sui-framework/packages/sui-framework/sources/object.move @@ -39,6 +39,9 @@ module sui::object { /// The hardcoded ID for the singleton DenyList. const SUI_DENY_LIST_OBJECT_ID: address = @0x403; + /// The hardcoded ID for the Bridge Object. + const SUI_BRIDGE_ID: address = @0x9; + /// Sender is not @0x0 the system address. const ENotSystemAddress: u64 = 0; @@ -132,6 +135,15 @@ module sui::object { } } + #[allow(unused_function)] + /// Create the `UID` for the singleton `Bridge` object. + /// This should only be called once from `bridge`. + fun bridge(): UID { + UID { + id: ID { bytes: SUI_BRIDGE_ID } + } + } + /// Get the inner `ID` of `uid` public fun uid_as_inner(uid: &UID): &ID { &uid.id diff --git a/crates/sui-framework/packages/sui-system/sources/sui_system.move b/crates/sui-framework/packages/sui-system/sources/sui_system.move index e3a347a96f6ab..abc6a96196837 100644 --- a/crates/sui-framework/packages/sui-system/sources/sui_system.move +++ b/crates/sui-framework/packages/sui-system/sources/sui_system.move @@ -51,6 +51,7 @@ module sui_system::sui_system { use sui_system::stake_subsidy::StakeSubsidy; use sui_system::staking_pool::PoolTokenExchangeRate; use sui::dynamic_field; + use sui::vec_map::VecMap; #[test_only] use sui::balance; #[test_only] use sui_system::validator_set::ValidatorSet; @@ -584,6 +585,18 @@ module sui_system::sui_system { inner } + #[allow(unused_function)] + /// Returns the voting power of the active validators, values are voting power in the scale of 10000. + fun validator_voting_powers(wrapper: &mut SuiSystemState): VecMap { + let self = load_system_state(wrapper); + sui_system_state_inner::active_validator_voting_powers(self) + } + + #[test_only] + public fun validator_voting_powers_for_testing(wrapper: &mut SuiSystemState): VecMap { + validator_voting_powers(wrapper) + } + #[test_only] /// Return the current epoch number. Useful for applications that need a coarse-grained concept of time, /// since epochs are ever-increasing and epoch changes are intended to happen every 24 hours. diff --git a/crates/sui-framework/packages/sui-system/sources/sui_system_state_inner.move b/crates/sui-framework/packages/sui-system/sources/sui_system_state_inner.move index 8ae6c7cac3fbe..2b0385d7d7bc3 100644 --- a/crates/sui-framework/packages/sui-system/sources/sui_system_state_inner.move +++ b/crates/sui-framework/packages/sui-system/sources/sui_system_state_inner.move @@ -984,6 +984,19 @@ module sui_system::sui_system_state_inner { self.validators.validator_total_stake_amount(validator_addr) } + /// Returns the voting power for `validator_addr`. + /// Aborts if `validator_addr` is not an active validator. + public(package) fun active_validator_voting_powers(self: &SuiSystemStateInnerV2): VecMap { + let mut active_validators = active_validator_addresses(self); + let mut voting_powers = vec_map::empty(); + while (!vector::is_empty(&active_validators)) { + let validator = vector::pop_back(&mut active_validators); + let voting_power = validator_set::validator_voting_power(&self.validators, validator); + vec_map::insert(&mut voting_powers, validator, voting_power); + }; + voting_powers + } + /// Returns the staking pool id of a given validator. /// Aborts if `validator_addr` is not an active validator. public(package) fun validator_staking_pool_id(self: &SuiSystemStateInnerV2, validator_addr: address): ID { diff --git a/crates/sui-framework/packages/sui-system/sources/validator_set.move b/crates/sui-framework/packages/sui-system/sources/validator_set.move index 838aca452c899..83281b1d80f3e 100644 --- a/crates/sui-framework/packages/sui-system/sources/validator_set.move +++ b/crates/sui-framework/packages/sui-system/sources/validator_set.move @@ -533,6 +533,11 @@ module sui_system::validator_set { validator.stake_amount() } + public fun validator_voting_power(self: &ValidatorSet, validator_address: address): u64 { + let validator = get_validator_ref(&self.active_validators, validator_address); + validator.voting_power() + } + public fun validator_staking_pool_id(self: &ValidatorSet, validator_address: address): ID { let validator = get_validator_ref(&self.active_validators, validator_address); validator.staking_pool_id() diff --git a/crates/sui-framework/published_api.txt b/crates/sui-framework/published_api.txt index ba12810f3a3f6..8bdaa4b3d799a 100644 --- a/crates/sui-framework/published_api.txt +++ b/crates/sui-framework/published_api.txt @@ -481,6 +481,9 @@ validator_total_stake_amount validator_stake_amount public fun 0x3::validator_set +validator_voting_power + public fun + 0x3::validator_set validator_staking_pool_id public fun 0x3::validator_set @@ -784,6 +787,9 @@ epoch_start_timestamp_ms validator_stake_amount public(package) fun 0x3::sui_system_state_inner +active_validator_voting_powers + public(package) fun + 0x3::sui_system_state_inner validator_staking_pool_id public(package) fun 0x3::sui_system_state_inner @@ -934,6 +940,9 @@ load_system_state_mut load_inner_maybe_upgrade fun 0x3::sui_system +validator_voting_powers + fun + 0x3::sui_system GenesisValidatorMetadata public struct 0x3::genesis @@ -1120,6 +1129,9 @@ randomness_state sui_deny_list_object_id public(package) fun 0x2::object +bridge + fun + 0x2::object uid_as_inner public fun 0x2::object @@ -3553,6 +3565,411 @@ order_id tick_level public fun 0xdee9::order_query +BridgeTreasury + public struct + 0xb::treasury +BridgeTokenMetadata + public struct + 0xb::treasury +ForeignTokenRegistration + public struct + 0xb::treasury +UpdateTokenPriceEvent + public struct + 0xb::treasury +NewTokenEvent + public struct + 0xb::treasury +TokenRegistrationEvent + public struct + 0xb::treasury +token_id + public fun + 0xb::treasury +decimal_multiplier + public fun + 0xb::treasury +notional_value + public fun + 0xb::treasury +register_foreign_token + public(package) fun + 0xb::treasury +add_new_token + public(package) fun + 0xb::treasury +create + public(package) fun + 0xb::treasury +burn + public(package) fun + 0xb::treasury +mint + public(package) fun + 0xb::treasury +update_asset_notional_price + public(package) fun + 0xb::treasury +get_token_metadata + fun + 0xb::treasury +token + public fun + 0xb::message_types +committee_blocklist + public fun + 0xb::message_types +emergency_op + public fun + 0xb::message_types +update_bridge_limit + public fun + 0xb::message_types +update_asset_price + public fun + 0xb::message_types +add_tokens_on_sui + public fun + 0xb::message_types +BridgeRoute + public struct + 0xb::chain_ids +sui_mainnet + public fun + 0xb::chain_ids +sui_testnet + public fun + 0xb::chain_ids +sui_custom + public fun + 0xb::chain_ids +eth_mainnet + public fun + 0xb::chain_ids +eth_sepolia + public fun + 0xb::chain_ids +eth_custom + public fun + 0xb::chain_ids +route_source + public fun + 0xb::chain_ids +route_destination + public fun + 0xb::chain_ids +assert_valid_chain_id + public fun + 0xb::chain_ids +valid_routes + public fun + 0xb::chain_ids +is_valid_route + public fun + 0xb::chain_ids +get_route + public fun + 0xb::chain_ids +BridgeMessage + public struct + 0xb::message +BridgeMessageKey + public struct + 0xb::message +TokenPayload + public struct + 0xb::message +EmergencyOp + public struct + 0xb::message +Blocklist + public struct + 0xb::message +UpdateBridgeLimit + public struct + 0xb::message +UpdateAssetPrice + public struct + 0xb::message +AddTokenOnSui + public struct + 0xb::message +extract_token_bridge_payload + public fun + 0xb::message +extract_emergency_op_payload + public fun + 0xb::message +extract_blocklist_payload + public fun + 0xb::message +extract_update_bridge_limit + public fun + 0xb::message +extract_update_asset_price + public fun + 0xb::message +extract_add_tokens_on_sui + public fun + 0xb::message +serialize_message + public fun + 0xb::message +create_token_bridge_message + public fun + 0xb::message +create_emergency_op_message + public fun + 0xb::message +create_blocklist_message + public fun + 0xb::message +create_update_bridge_limit_message + public fun + 0xb::message +create_update_asset_price_message + public fun + 0xb::message +create_add_tokens_on_sui_message + public fun + 0xb::message +create_key + public fun + 0xb::message +key + public fun + 0xb::message +message_version + public fun + 0xb::message +message_type + public fun + 0xb::message +seq_num + public fun + 0xb::message +source_chain + public fun + 0xb::message +token_target_chain + public fun + 0xb::message +token_target_address + public fun + 0xb::message +token_type + public fun + 0xb::message +token_amount + public fun + 0xb::message +emergency_op_type + public fun + 0xb::message +blocklist_type + public fun + 0xb::message +blocklist_validator_addresses + public fun + 0xb::message +update_bridge_limit_payload_sending_chain + public fun + 0xb::message +update_bridge_limit_payload_receiving_chain + public fun + 0xb::message +update_bridge_limit_payload_limit + public fun + 0xb::message +update_asset_price_payload_token_id + public fun + 0xb::message +update_asset_price_payload_new_price + public fun + 0xb::message +is_native + public fun + 0xb::message +token_ids + public fun + 0xb::message +token_type_names + public fun + 0xb::message +token_prices + public fun + 0xb::message +emergency_op_pause + public fun + 0xb::message +emergency_op_unpause + public fun + 0xb::message +required_voting_power + public fun + 0xb::message +reverse_bytes + fun + 0xb::message +peel_u64_be + fun + 0xb::message +TransferLimiter + public struct + 0xb::limiter +TransferRecord + public struct + 0xb::limiter +UpdateRouteLimitEvent + public struct + 0xb::limiter +get_route_limit + public fun + 0xb::limiter +new + public(package) fun + 0xb::limiter +check_and_record_sending_transfer + public(package) fun + 0xb::limiter +update_route_limit + public(package) fun + 0xb::limiter +current_hour_since_epoch + fun + 0xb::limiter +adjust_transfer_records + fun + 0xb::limiter +initial_transfer_limits + fun + 0xb::limiter +ecdsa_pub_key_to_eth_address + public(package) fun + 0xb::crypto +BlocklistValidatorEvent + public struct + 0xb::committee +BridgeCommittee + public struct + 0xb::committee +CommitteeUpdateEvent + public struct + 0xb::committee +CommitteeMember + public struct + 0xb::committee +CommitteeMemberRegistration + public struct + 0xb::committee +verify_signatures + public fun + 0xb::committee +create + public(package) fun + 0xb::committee +register + public(package) fun + 0xb::committee +try_create_next_committee + public(package) fun + 0xb::committee +execute_blocklist + public(package) fun + 0xb::committee +committee_members + public(package) fun + 0xb::committee +check_uniqueness_bridge_keys + fun + 0xb::committee +Bridge + public struct + 0xb::bridge +BridgeInner + public struct + 0xb::bridge +TokenDepositedEvent + public struct + 0xb::bridge +EmergencyOpEvent + public struct + 0xb::bridge +BridgeRecord + public struct + 0xb::bridge +TokenTransferApproved + public struct + 0xb::bridge +TokenTransferClaimed + public struct + 0xb::bridge +TokenTransferAlreadyApproved + public struct + 0xb::bridge +TokenTransferAlreadyClaimed + public struct + 0xb::bridge +TokenTransferLimitExceed + public struct + 0xb::bridge +create + fun + 0xb::bridge +init_bridge_committee + fun + 0xb::bridge +committee_registration + public fun + 0xb::bridge +register_foreign_token + public fun + 0xb::bridge +send_token + public fun + 0xb::bridge +approve_token_transfer + public fun + 0xb::bridge +claim_token + public fun + 0xb::bridge +claim_and_transfer_token + public fun + 0xb::bridge +execute_system_message + public fun + 0xb::bridge +get_token_transfer_action_status + public fun + 0xb::bridge +load_inner + fun + 0xb::bridge +load_inner_mut + fun + 0xb::bridge +claim_token_internal + fun + 0xb::bridge +execute_emergency_op + fun + 0xb::bridge +execute_update_bridge_limit + fun + 0xb::bridge +execute_update_asset_price + fun + 0xb::bridge +execute_add_tokens_on_sui + fun + 0xb::bridge +get_current_seq_num_and_increment + fun + 0xb::bridge +get_token_transfer_action_signatures + fun + 0xb::bridge print public fun 0x1::debug diff --git a/crates/sui-framework/src/lib.rs b/crates/sui-framework/src/lib.rs index 6a686a04a19a1..e72c74c34bda0 100644 --- a/crates/sui-framework/src/lib.rs +++ b/crates/sui-framework/src/lib.rs @@ -11,7 +11,6 @@ use serde::{Deserialize, Serialize}; use std::fmt::Formatter; use sui_types::base_types::ObjectRef; use sui_types::storage::ObjectStore; -use sui_types::DEEPBOOK_PACKAGE_ID; use sui_types::{ base_types::ObjectID, digests::TransactionDigest, @@ -19,6 +18,7 @@ use sui_types::{ object::{Object, OBJECT_START_VERSION}, MOVE_STDLIB_PACKAGE_ID, SUI_FRAMEWORK_PACKAGE_ID, SUI_SYSTEM_PACKAGE_ID, }; +use sui_types::{BRIDGE_PACKAGE_ID, DEEPBOOK_PACKAGE_ID}; use tracing::error; /// Represents a system package in the framework, that's built from the source code inside @@ -123,6 +123,15 @@ impl BuiltInFramework { DEEPBOOK_PACKAGE_ID, "deepbook", [MOVE_STDLIB_PACKAGE_ID, SUI_FRAMEWORK_PACKAGE_ID] + ), + ( + BRIDGE_PACKAGE_ID, + "bridge", + [ + MOVE_STDLIB_PACKAGE_ID, + SUI_FRAMEWORK_PACKAGE_ID, + SUI_SYSTEM_PACKAGE_ID + ] ) ]) .iter() diff --git a/crates/sui-genesis-builder/src/lib.rs b/crates/sui-genesis-builder/src/lib.rs index f52916e12a973..64d9e15aac911 100644 --- a/crates/sui-genesis-builder/src/lib.rs +++ b/crates/sui-genesis-builder/src/lib.rs @@ -22,6 +22,7 @@ use sui_protocol_config::{Chain, ProtocolConfig, ProtocolVersion}; use sui_types::base_types::{ ExecutionDigests, ObjectID, SequenceNumber, SuiAddress, TransactionDigest, TxContext, }; +use sui_types::bridge::{BridgeChainId, BRIDGE_CREATE_FUNCTION_NAME, BRIDGE_MODULE_NAME}; use sui_types::committee::Committee; use sui_types::crypto::{ AuthorityKeyPair, AuthorityPublicKeyBytes, AuthoritySignInfo, AuthoritySignInfoTrait, @@ -34,8 +35,10 @@ use sui_types::epoch_data::EpochData; use sui_types::gas::SuiGasStatus; use sui_types::gas_coin::GasCoin; use sui_types::governance::StakedSui; +use sui_types::id::UID; use sui_types::in_memory_storage::InMemoryStorage; use sui_types::inner_temporary_store::InnerTemporaryStore; +use sui_types::is_system_package; use sui_types::message_envelope::Message; use sui_types::messages_checkpoint::{ CertifiedCheckpointSummary, CheckpointContents, CheckpointSummary, @@ -47,7 +50,7 @@ use sui_types::sui_system_state::{get_sui_system_state, SuiSystemState, SuiSyste use sui_types::transaction::{ CallArg, CheckedInputObjects, Command, InputObjectKind, ObjectReadResult, Transaction, }; -use sui_types::{SUI_FRAMEWORK_ADDRESS, SUI_SYSTEM_ADDRESS}; +use sui_types::{BRIDGE_ADDRESS, SUI_BRIDGE_OBJECT_ID, SUI_FRAMEWORK_ADDRESS, SUI_SYSTEM_ADDRESS}; use tracing::trace; use validator_info::{GenesisValidatorInfo, GenesisValidatorMetadata, ValidatorInfo}; @@ -321,6 +324,11 @@ impl Builder { unsigned_genesis.has_randomness_state_object() ); + assert_eq!( + protocol_config.enable_bridge(), + unsigned_genesis.has_bridge_object() + ); + assert_eq!( protocol_config.enable_coin_deny_list(), unsigned_genesis.coin_deny_list_state().is_some(), @@ -735,10 +743,14 @@ fn build_unsigned_genesis_data( // Get the correct system packages for our protocol version. If we cannot find the snapshot // that means that we must be at the latest version and we should use the latest version of the // framework. - let system_packages = + let mut system_packages = sui_framework_snapshot::load_bytecode_snapshot(parameters.protocol_version.as_u64()) .unwrap_or_else(|_| BuiltInFramework::iter_system_packages().cloned().collect()); + // if system packages are provided in `objects`, update them with the provided bytes. + // This is a no-op under normal conditions and only an issue with certain tests. + update_system_packages_from_objects(&mut system_packages, objects); + let mut genesis_ctx = create_genesis_context( &epoch_data, &genesis_chain_parameters, @@ -778,6 +790,45 @@ fn build_unsigned_genesis_data( } } +// Some tests provide an override of the system packages via objects to the genesis builder. +// When that happens we need to update the system packages with the new bytes provided. +// Mock system packages in protocol config tests are an example of that (today the only +// example). +// The problem here arises from the fact that if regular system packages are pushed first +// *AND* if any of them is loaded in the loader cache, there is no way to override them +// with the provided object (no way to mock properly). +// System packages are loaded only from internal dependencies (a system package depending on +// some other), and in that case they would be loaded in the VM/loader cache. +// The Bridge is an example of that and what led to this code. The bridge depends +// on `sui_system` which is mocked in some tests, but would be in the loader +// cache courtesy of the Bridge, thus causing the problem. +fn update_system_packages_from_objects( + system_packages: &mut Vec, + objects: &[Object], +) { + // Filter `objects` for system packages, and make `SystemPackage`s out of them. + let system_package_overrides: BTreeMap>> = objects + .iter() + .filter_map(|obj| { + let pkg = obj.data.try_as_package()?; + is_system_package(pkg.id()).then(|| { + ( + pkg.id(), + pkg.serialized_module_map().values().cloned().collect(), + ) + }) + }) + .collect(); + + // Replace packages in `system_packages` that are present in `objects` with their counterparts + // from the previous step. + for package in system_packages { + if let Some(overrides) = system_package_overrides.get(&package.id).cloned() { + package.bytes = overrides; + } + } +} + fn create_genesis_checkpoint( parameters: &GenesisCeremonyParameters, transaction: &Transaction, @@ -1075,6 +1126,22 @@ pub fn generate_genesis_system_object( )?; } + if protocol_config.enable_bridge() { + let bridge_uid = builder + .input(CallArg::Pure(UID::new(SUI_BRIDGE_OBJECT_ID).to_bcs_bytes())) + .unwrap(); + // TODO(bridge): this needs to be passed in as a parameter for next testnet regenesis + // Hardcoding chain id to SuiCustom + let bridge_chain_id = builder.pure(BridgeChainId::SuiCustom).unwrap(); + builder.programmable_move_call( + BRIDGE_ADDRESS.into(), + BRIDGE_MODULE_NAME.to_owned(), + BRIDGE_CREATE_FUNCTION_NAME.to_owned(), + vec![], + vec![bridge_uid, bridge_chain_id], + ); + } + // Step 4: Mint the supply of SUI. let sui_supply = builder.programmable_move_call( SUI_FRAMEWORK_ADDRESS.into(), diff --git a/crates/sui-graphql-rpc/schema/current_progress_schema.graphql b/crates/sui-graphql-rpc/schema/current_progress_schema.graphql index 6224a32e3be5c..a4f13dd556cd5 100644 --- a/crates/sui-graphql-rpc/schema/current_progress_schema.graphql +++ b/crates/sui-graphql-rpc/schema/current_progress_schema.graphql @@ -310,6 +310,14 @@ String representation of an arbitrary width, possibly signed integer. scalar BigInt +type BridgeCommitteeInitTransaction { + bridgeObjInitialSharedVersion: Int! +} + +type BridgeStateCreateTransaction { + chainId: String! +} + """ A system transaction that updates epoch information on-chain (increments the current epoch). Executed by the system once per epoch, without using gas. Epoch change transactions cannot be @@ -943,7 +951,7 @@ type EndOfEpochTransaction { transactions(first: Int, before: String, last: Int, after: String): EndOfEpochTransactionKindConnection! } -union EndOfEpochTransactionKind = ChangeEpochTransaction | AuthenticatorStateCreateTransaction | AuthenticatorStateExpireTransaction | RandomnessStateCreateTransaction | CoinDenyListStateCreateTransaction +union EndOfEpochTransactionKind = ChangeEpochTransaction | AuthenticatorStateCreateTransaction | AuthenticatorStateExpireTransaction | RandomnessStateCreateTransaction | CoinDenyListStateCreateTransaction | BridgeStateCreateTransaction | BridgeCommitteeInitTransaction type EndOfEpochTransactionKindConnection { """ diff --git a/crates/sui-graphql-rpc/src/types/transaction_block_kind/end_of_epoch.rs b/crates/sui-graphql-rpc/src/types/transaction_block_kind/end_of_epoch.rs index a03c8a9f26ef0..f6defbea0febc 100644 --- a/crates/sui-graphql-rpc/src/types/transaction_block_kind/end_of_epoch.rs +++ b/crates/sui-graphql-rpc/src/types/transaction_block_kind/end_of_epoch.rs @@ -5,6 +5,8 @@ use async_graphql::connection::{Connection, CursorType, Edge}; use async_graphql::*; use move_binary_format::errors::PartialVMResult; use move_binary_format::CompiledModule; +use sui_types::base_types::SequenceNumber; +use sui_types::digests::ChainIdentifier as SuiChainIdentifier; use sui_types::{ digests::TransactionDigest, object::Object as NativeObject, @@ -40,6 +42,8 @@ pub(crate) enum EndOfEpochTransactionKind { AuthenticatorStateExpire(AuthenticatorStateExpireTransaction), RandomnessStateCreate(RandomnessStateCreateTransaction), CoinDenyListStateCreate(CoinDenyListStateCreateTransaction), + BridgeStateCreate(BridgeStateCreateTransaction), + BridgeCommitteeInit(BridgeCommitteeInitTransaction), } #[derive(Clone, PartialEq, Eq)] @@ -78,6 +82,20 @@ pub(crate) struct CoinDenyListStateCreateTransaction { dummy: Option, } +#[derive(Clone, PartialEq, Eq)] +pub(crate) struct BridgeStateCreateTransaction { + pub native: SuiChainIdentifier, + /// The checkpoint sequence number this was viewed at. + pub checkpoint_viewed_at: u64, +} + +#[derive(Clone, PartialEq, Eq)] +pub(crate) struct BridgeCommitteeInitTransaction { + pub native: SequenceNumber, + /// The checkpoint sequence number this was viewed at. + pub checkpoint_viewed_at: u64, +} + pub(crate) type CTxn = JsonCursor; pub(crate) type CPackage = JsonCursor; @@ -230,6 +248,20 @@ impl AuthenticatorStateExpireTransaction { } } +#[Object] +impl BridgeStateCreateTransaction { + async fn chain_id(&self) -> String { + self.native.to_string() + } +} + +#[Object] +impl BridgeCommitteeInitTransaction { + async fn bridge_obj_initial_shared_version(&self) -> u64 { + self.native.value() + } +} + impl EndOfEpochTransactionKind { fn from(kind: NativeEndOfEpochTransactionKind, checkpoint_viewed_at: u64) -> Self { use EndOfEpochTransactionKind as K; @@ -255,6 +287,16 @@ impl EndOfEpochTransactionKind { N::DenyListStateCreate => { K::CoinDenyListStateCreate(CoinDenyListStateCreateTransaction { dummy: None }) } + N::BridgeStateCreate(chain_id) => K::BridgeStateCreate(BridgeStateCreateTransaction { + native: chain_id, + checkpoint_viewed_at, + }), + N::BridgeCommitteeInit(bridge_shared_version) => { + K::BridgeCommitteeInit(BridgeCommitteeInitTransaction { + native: bridge_shared_version, + checkpoint_viewed_at, + }) + } } } } diff --git a/crates/sui-graphql-rpc/tests/snapshots/snapshot_tests__schema_sdl_export.snap b/crates/sui-graphql-rpc/tests/snapshots/snapshot_tests__schema_sdl_export.snap index a799576a36a86..7f5f1dfbc7d16 100644 --- a/crates/sui-graphql-rpc/tests/snapshots/snapshot_tests__schema_sdl_export.snap +++ b/crates/sui-graphql-rpc/tests/snapshots/snapshot_tests__schema_sdl_export.snap @@ -314,6 +314,14 @@ String representation of an arbitrary width, possibly signed integer. scalar BigInt +type BridgeCommitteeInitTransaction { + bridgeObjInitialSharedVersion: Int! +} + +type BridgeStateCreateTransaction { + chainId: String! +} + """ A system transaction that updates epoch information on-chain (increments the current epoch). Executed by the system once per epoch, without using gas. Epoch change transactions cannot be @@ -947,7 +955,7 @@ type EndOfEpochTransaction { transactions(first: Int, before: String, last: Int, after: String): EndOfEpochTransactionKindConnection! } -union EndOfEpochTransactionKind = ChangeEpochTransaction | AuthenticatorStateCreateTransaction | AuthenticatorStateExpireTransaction | RandomnessStateCreateTransaction | CoinDenyListStateCreateTransaction +union EndOfEpochTransactionKind = ChangeEpochTransaction | AuthenticatorStateCreateTransaction | AuthenticatorStateExpireTransaction | RandomnessStateCreateTransaction | CoinDenyListStateCreateTransaction | BridgeStateCreateTransaction | BridgeCommitteeInitTransaction type EndOfEpochTransactionKindConnection { """ diff --git a/crates/sui-json-rpc-api/src/bridge.rs b/crates/sui-json-rpc-api/src/bridge.rs new file mode 100644 index 0000000000000..858953ed22ed6 --- /dev/null +++ b/crates/sui-json-rpc-api/src/bridge.rs @@ -0,0 +1,21 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use jsonrpsee::core::RpcResult; +use jsonrpsee::proc_macros::rpc; + +use sui_open_rpc_macros::open_rpc; +use sui_types::bridge::BridgeSummary; + +#[open_rpc(namespace = "suix", tag = "Bridge Read API")] +#[rpc(server, client, namespace = "suix")] +pub trait BridgeReadApi { + /// Returns the latest BridgeSummary + #[method(name = "getLatestBridge")] + async fn get_latest_bridge(&self) -> RpcResult; + + /// Returns the initial shared version of the bridge object, usually + /// for the purpose of constructing an ObjectArg in a transaction. + #[method(name = "getBridgeObjectInitialSharedVersion")] + async fn get_bridge_object_initial_shared_version(&self) -> RpcResult; +} diff --git a/crates/sui-json-rpc-api/src/lib.rs b/crates/sui-json-rpc-api/src/lib.rs index 053d69c22299a..104963812e31f 100644 --- a/crates/sui-json-rpc-api/src/lib.rs +++ b/crates/sui-json-rpc-api/src/lib.rs @@ -4,6 +4,9 @@ use anyhow::anyhow; use mysten_metrics::histogram::Histogram; +pub use bridge::BridgeReadApiClient; +pub use bridge::BridgeReadApiOpenRpc; +pub use bridge::BridgeReadApiServer; pub use coin::CoinReadApiClient; pub use coin::CoinReadApiOpenRpc; pub use coin::CoinReadApiServer; @@ -33,6 +36,7 @@ pub use write::WriteApiClient; pub use write::WriteApiOpenRpc; pub use write::WriteApiServer; +mod bridge; mod coin; mod extended; mod governance; diff --git a/crates/sui-json-rpc-types/src/sui_transaction.rs b/crates/sui-json-rpc-types/src/sui_transaction.rs index af61e95f2ff51..ef60affd2af8b 100644 --- a/crates/sui-json-rpc-types/src/sui_transaction.rs +++ b/crates/sui-json-rpc-types/src/sui_transaction.rs @@ -27,7 +27,9 @@ use sui_types::base_types::{ EpochId, ObjectID, ObjectRef, SequenceNumber, SuiAddress, TransactionDigest, }; use sui_types::crypto::SuiSignature; -use sui_types::digests::{ConsensusCommitDigest, ObjectDigest, TransactionEventsDigest}; +use sui_types::digests::{ + CheckpointDigest, ConsensusCommitDigest, ObjectDigest, TransactionEventsDigest, +}; use sui_types::effects::{TransactionEffects, TransactionEffectsAPI, TransactionEvents}; use sui_types::error::{ExecutionError, SuiError, SuiResult}; use sui_types::execution_status::ExecutionStatus; @@ -519,6 +521,16 @@ impl SuiTransactionBlockKind { EndOfEpochTransactionKind::DenyListStateCreate => { SuiEndOfEpochTransactionKind::CoinDenyListStateCreate } + EndOfEpochTransactionKind::BridgeStateCreate(chain_id) => { + SuiEndOfEpochTransactionKind::BridgeStateCreate( + (*chain_id.as_bytes()).into(), + ) + } + EndOfEpochTransactionKind::BridgeCommitteeInit( + bridge_shared_version, + ) => SuiEndOfEpochTransactionKind::BridgeCommitteeUpdate( + bridge_shared_version, + ), }) .collect(), }) @@ -599,6 +611,14 @@ impl SuiTransactionBlockKind { EndOfEpochTransactionKind::DenyListStateCreate => { SuiEndOfEpochTransactionKind::CoinDenyListStateCreate } + EndOfEpochTransactionKind::BridgeStateCreate(id) => { + SuiEndOfEpochTransactionKind::BridgeStateCreate( + (*id.as_bytes()).into(), + ) + } + EndOfEpochTransactionKind::BridgeCommitteeInit(seq) => { + SuiEndOfEpochTransactionKind::BridgeCommitteeUpdate(seq) + } }) .collect(), }) @@ -1598,6 +1618,8 @@ pub enum SuiEndOfEpochTransactionKind { AuthenticatorStateExpire(SuiAuthenticatorStateExpire), RandomnessStateCreate, CoinDenyListStateCreate, + BridgeStateCreate(CheckpointDigest), + BridgeCommitteeUpdate(SequenceNumber), } #[serde_as] diff --git a/crates/sui-json-rpc/src/authority_state.rs b/crates/sui-json-rpc/src/authority_state.rs index 60c66b71ea19b..146769054d696 100644 --- a/crates/sui-json-rpc/src/authority_state.rs +++ b/crates/sui-json-rpc/src/authority_state.rs @@ -23,6 +23,7 @@ use sui_storage::key_value_store::{ use sui_types::base_types::{ MoveObjectType, ObjectID, ObjectInfo, ObjectRef, SequenceNumber, SuiAddress, }; +use sui_types::bridge::Bridge; use sui_types::committee::{Committee, EpochId}; use sui_types::digests::{ChainIdentifier, TransactionDigest, TransactionEventsDigest}; use sui_types::dynamic_field::DynamicFieldInfo; @@ -167,6 +168,9 @@ pub trait StateRead: Send + Sync { fn get_system_state(&self) -> StateReadResult; fn get_or_latest_committee(&self, epoch: Option>) -> StateReadResult; + // bridge_api + fn get_bridge(&self) -> StateReadResult; + // coin_api fn find_publish_txn_digest(&self, package_id: ObjectID) -> StateReadResult; fn get_owned_coins( @@ -437,6 +441,12 @@ impl StateRead for AuthorityState { .get_or_latest_committee(epoch.map(|e| *e))?) } + fn get_bridge(&self) -> StateReadResult { + self.get_cache_reader() + .get_bridge_object_unsafe() + .map_err(|err| err.into()) + } + fn find_publish_txn_digest(&self, package_id: ObjectID) -> StateReadResult { Ok(self.find_publish_txn_digest(package_id)?) } diff --git a/crates/sui-json-rpc/src/bridge_api.rs b/crates/sui-json-rpc/src/bridge_api.rs new file mode 100644 index 0000000000000..58100c7f844fd --- /dev/null +++ b/crates/sui-json-rpc/src/bridge_api.rs @@ -0,0 +1,66 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use std::sync::Arc; + +use async_trait::async_trait; +use jsonrpsee::core::RpcResult; +use jsonrpsee::RpcModule; +use sui_core::authority::AuthorityState; +use sui_json_rpc_api::{BridgeReadApiOpenRpc, BridgeReadApiServer, JsonRpcMetrics}; +use sui_open_rpc::Module; +use sui_types::bridge::{get_bridge_obj_initial_shared_version, BridgeSummary, BridgeTrait}; +use tracing::{info, instrument}; + +use crate::authority_state::StateRead; +use crate::error::Error; +use crate::{with_tracing, SuiRpcModule}; + +#[derive(Clone)] +pub struct BridgeReadApi { + state: Arc, + pub metrics: Arc, +} + +impl BridgeReadApi { + pub fn new(state: Arc, metrics: Arc) -> Self { + Self { state, metrics } + } +} + +#[async_trait] +impl BridgeReadApiServer for BridgeReadApi { + #[instrument(skip(self))] + async fn get_latest_bridge(&self) -> RpcResult { + with_tracing!(async move { + self.state + .get_bridge() + .map_err(Error::from)? + .try_into_bridge_summary() + .map_err(Error::from) + }) + } + + #[instrument(skip(self))] + async fn get_bridge_object_initial_shared_version(&self) -> RpcResult { + with_tracing!(async move { + Ok( + get_bridge_obj_initial_shared_version(self.state.get_object_store())? + .ok_or(Error::UnexpectedError( + "Failed to find Bridge object initial version".to_string(), + ))? + .into(), + ) + }) + } +} + +impl SuiRpcModule for BridgeReadApi { + fn rpc(self) -> RpcModule { + self.into_rpc() + } + + fn rpc_doc_module() -> Module { + BridgeReadApiOpenRpc::module_doc() + } +} diff --git a/crates/sui-json-rpc/src/lib.rs b/crates/sui-json-rpc/src/lib.rs index ad3484b9ba760..a8eda2e73f14e 100644 --- a/crates/sui-json-rpc/src/lib.rs +++ b/crates/sui-json-rpc/src/lib.rs @@ -35,6 +35,7 @@ use crate::routing_layer::RpcRouter; pub mod authority_state; pub mod axum_router; mod balance_changes; +pub mod bridge_api; pub mod coin_api; pub mod error; pub mod governance_api; diff --git a/crates/sui-move-build/src/lib.rs b/crates/sui-move-build/src/lib.rs index 3e1bb4cc23084..295dba14a9652 100644 --- a/crates/sui-move-build/src/lib.rs +++ b/crates/sui-move-build/src/lib.rs @@ -46,7 +46,8 @@ use sui_types::{ error::{SuiError, SuiResult}, is_system_package, move_package::{FnInfo, FnInfoKey, FnInfoMap, MovePackage}, - DEEPBOOK_ADDRESS, MOVE_STDLIB_ADDRESS, SUI_FRAMEWORK_ADDRESS, SUI_SYSTEM_ADDRESS, + BRIDGE_ADDRESS, DEEPBOOK_ADDRESS, MOVE_STDLIB_ADDRESS, SUI_FRAMEWORK_ADDRESS, + SUI_SYSTEM_ADDRESS, }; use sui_verifier::verifier as sui_bytecode_verifier; @@ -396,6 +397,12 @@ impl CompiledPackage { .filter(|m| *m.self_id().address() == DEEPBOOK_ADDRESS) } + /// Get bytecode modules from DeepBook that are used by this package + pub fn get_bridge_modules(&self) -> impl Iterator { + self.get_modules_and_deps() + .filter(|m| *m.self_id().address() == BRIDGE_ADDRESS) + } + /// Get bytecode modules from the Sui System that are used by this package pub fn get_sui_system_modules(&self) -> impl Iterator { self.get_modules_and_deps() diff --git a/crates/sui-node/src/lib.rs b/crates/sui-node/src/lib.rs index 836b4ede56f59..133884fd427cd 100644 --- a/crates/sui-node/src/lib.rs +++ b/crates/sui-node/src/lib.rs @@ -30,6 +30,7 @@ use sui_core::epoch::randomness::RandomnessManager; use sui_core::execution_cache::ExecutionCacheMetrics; use sui_core::execution_cache::NotifyReadWrapper; use sui_core::traffic_controller::metrics::TrafficControllerMetrics; +use sui_json_rpc::bridge_api::BridgeReadApi; use sui_json_rpc::ServerType; use sui_json_rpc_api::JsonRpcMetrics; use sui_network::randomness; @@ -1852,6 +1853,7 @@ pub async fn build_http_server( server.register_module(TransactionBuilderApi::new(state.clone()))?; } server.register_module(GovernanceReadApi::new(state.clone(), metrics.clone()))?; + server.register_module(BridgeReadApi::new(state.clone(), metrics.clone()))?; if let Some(transaction_orchestrator) = transaction_orchestrator { server.register_module(TransactionExecutionApi::new( diff --git a/crates/sui-open-rpc/spec/openrpc.json b/crates/sui-open-rpc/spec/openrpc.json index 1169d95300cc6..296d8d1b2128f 100644 --- a/crates/sui-open-rpc/spec/openrpc.json +++ b/crates/sui-open-rpc/spec/openrpc.json @@ -1367,6 +1367,7 @@ "advance_to_highest_supported_protocol_version": false, "allow_receiving_object_id": false, "ban_entry_init": false, + "bridge": false, "commit_root_state_digest": false, "consensus_order_end_of_epoch_last": true, "disable_invariant_violation_check_in_swap_loc": false, @@ -8551,6 +8552,30 @@ } }, "additionalProperties": false + }, + { + "type": "object", + "required": [ + "BridgeStateCreate" + ], + "properties": { + "BridgeStateCreate": { + "$ref": "#/components/schemas/CheckpointDigest" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "BridgeCommitteeUpdate" + ], + "properties": { + "BridgeCommitteeUpdate": { + "$ref": "#/components/schemas/SequenceNumber" + } + }, + "additionalProperties": false } ] }, diff --git a/crates/sui-protocol-config/src/lib.rs b/crates/sui-protocol-config/src/lib.rs index 69784cba3a7ec..20f5607a1650e 100644 --- a/crates/sui-protocol-config/src/lib.rs +++ b/crates/sui-protocol-config/src/lib.rs @@ -125,6 +125,8 @@ const MAX_PROTOCOL_VERSION: u64 = 45; // Enable random beacon protocol on testnet. // Set min Move binary format version to 6. // Enable transactions to be signed with zkLogin inside multisig signature. +// Add native bridge. +// Enable native bridge in devnet #[derive(Copy, Clone, Debug, Hash, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub struct ProtocolVersion(u64); @@ -349,6 +351,10 @@ struct FeatureFlags { #[serde(skip_serializing_if = "is_false")] random_beacon: bool, + // Enable bridge protocol + #[serde(skip_serializing_if = "is_false")] + bridge: bool, + #[serde(skip_serializing_if = "is_false")] enable_effects_v2: bool, @@ -1225,6 +1231,15 @@ impl ProtocolConfig { self.feature_flags.random_beacon } + pub fn enable_bridge(&self) -> bool { + let ret = self.feature_flags.bridge; + if ret { + // bridge required end-of-epoch transactions + assert!(self.feature_flags.end_of_epoch_transaction_supported); + } + ret + } + pub fn enable_effects_v2(&self) -> bool { self.feature_flags.enable_effects_v2 } @@ -2156,6 +2171,11 @@ impl ProtocolConfig { cfg.min_move_binary_format_version = Some(6); cfg.feature_flags.accept_zklogin_in_multisig = true; // Also bumps framework snapshot to fix binop issue. + + // enable bridge in devnet + if chain != Chain::Mainnet && chain != Chain::Testnet { + cfg.feature_flags.bridge = true; + } } // Use this template when making changes: // @@ -2318,6 +2338,9 @@ impl ProtocolConfig { pub fn set_zklogin_max_epoch_upper_bound_delta(&mut self, val: Option) { self.feature_flags.zklogin_max_epoch_upper_bound_delta = val } + pub fn set_disable_bridge_for_testing(&mut self) { + self.feature_flags.bridge = false + } } type OverrideFn = dyn Fn(ProtocolVersion, ProtocolConfig) -> ProtocolConfig + Send; diff --git a/crates/sui-protocol-config/src/snapshots/sui_protocol_config__test__version_45.snap b/crates/sui-protocol-config/src/snapshots/sui_protocol_config__test__version_45.snap index 81bc6416c5cbf..4736e218d7c42 100644 --- a/crates/sui-protocol-config/src/snapshots/sui_protocol_config__test__version_45.snap +++ b/crates/sui-protocol-config/src/snapshots/sui_protocol_config__test__version_45.snap @@ -33,6 +33,7 @@ feature_flags: loaded_child_object_format_type: true receive_objects: true random_beacon: true + bridge: true enable_effects_v2: true narwhal_certificate_v2: true verify_legacy_zklogin_address: true diff --git a/crates/sui-sdk/src/wallet_context.rs b/crates/sui-sdk/src/wallet_context.rs index 8e392b747a0a0..a14ce9564c203 100644 --- a/crates/sui-sdk/src/wallet_context.rs +++ b/crates/sui-sdk/src/wallet_context.rs @@ -16,6 +16,7 @@ use sui_json_rpc_types::{ }; use sui_keys::keystore::AccountKeystore; use sui_types::base_types::{ObjectID, ObjectRef, SuiAddress}; +use sui_types::crypto::SuiKeyPair; use sui_types::gas_coin::GasCoin; use sui_types::transaction::{Transaction, TransactionData, TransactionDataAPI}; use tokio::sync::RwLock; @@ -278,6 +279,11 @@ impl WalletContext { Ok(gas_price) } + /// Add an account + pub fn add_account(&mut self, alias: Option, keypair: SuiKeyPair) { + self.config.keystore.add_key(alias, keypair).unwrap(); + } + /// Sign a transaction with a key currently managed by the WalletContext pub fn sign_transaction(&self, data: &TransactionData) -> Transaction { let sig = self diff --git a/crates/sui-swarm-config/tests/snapshots/snapshot_tests__genesis_config_snapshot_matches.snap b/crates/sui-swarm-config/tests/snapshots/snapshot_tests__genesis_config_snapshot_matches.snap index fa761cbecd628..74df7ab4fa262 100644 --- a/crates/sui-swarm-config/tests/snapshots/snapshot_tests__genesis_config_snapshot_matches.snap +++ b/crates/sui-swarm-config/tests/snapshots/snapshot_tests__genesis_config_snapshot_matches.snap @@ -49,3 +49,4 @@ accounts: - 30000000000000000 - 30000000000000000 - 30000000000000000 + diff --git a/crates/sui-swarm-config/tests/snapshots/snapshot_tests__populated_genesis_snapshot_matches-2.snap b/crates/sui-swarm-config/tests/snapshots/snapshot_tests__populated_genesis_snapshot_matches-2.snap index f7e82687dc1c5..931fe555b8583 100644 --- a/crates/sui-swarm-config/tests/snapshots/snapshot_tests__populated_genesis_snapshot_matches-2.snap +++ b/crates/sui-swarm-config/tests/snapshots/snapshot_tests__populated_genesis_snapshot_matches-2.snap @@ -240,13 +240,13 @@ validators: next_epoch_worker_address: ~ extra_fields: id: - id: "0x93d24898fb529aef7433a35b7a4de05036b087aac96f33aa6ebea11bca918873" + id: "0xac9fdccb1113a5ead94e4673576e6a760db21d991a22cf2b0d24ac484a05bddf" size: 0 voting_power: 10000 - operation_cap_id: "0xad75a4f0dafa36f6c37a08501501279213d930189bdbdedf0f9c98ff5f718322" + operation_cap_id: "0x60e827aab20f6d00deeb5c2b6905b69691e348779b3bbef9934241028fad9cac" gas_price: 1000 staking_pool: - id: "0xb580917d26a325db5527c08cab34dbdb767e56b5840e2e4fa599222f86977d2b" + id: "0x92c825f5f682227d3d6b9bc4250ee58b871457e71c25c8cb35188318f3beeb75" activation_epoch: 0 deactivation_epoch: ~ sui_balance: 20000000000000000 @@ -254,14 +254,14 @@ validators: value: 0 pool_token_balance: 20000000000000000 exchange_rates: - id: "0xee81da2200b48ee373e8d9d14340461459c713aaf7c4eae88f916420ceeb1236" + id: "0x755c1a3a47fd39c01a5f0254f9cddfda49d5e3c5fdc0e8cdfb8c9e09ea4ab159" size: 1 pending_stake: 0 pending_total_sui_withdraw: 0 pending_pool_token_withdraw: 0 extra_fields: id: - id: "0x5b3cbb226c5ab748b3e62cec6b7446f85768214806256c5eb605bddd2287a374" + id: "0x1e62cc42d9d0f988f2ca64ba977265fc927f8902b6a9c4463ce1403dcfb53481" size: 0 commission_rate: 200 next_epoch_stake: 20000000000000000 @@ -269,27 +269,27 @@ validators: next_epoch_commission_rate: 200 extra_fields: id: - id: "0x74ae2314dbd1cd1ddc0497d9616b58d117253754175fb99136b4d7c70a1e17b1" + id: "0xda16f4a6cf350652385ee9f70a75f75bf5fc4db3881f551ff4667590dec6b168" size: 0 pending_active_validators: contents: - id: "0xbdacc1af1f90dc7c36e5c5ec67a115caf5f366761279fd4647ff6417c7efb791" + id: "0x3e54263b253e0a34c4d7cc3411c1d1e71c04d439296063f4abd59d2d36361476" size: 0 pending_removals: [] staking_pool_mappings: - id: "0x47040d1659ed168e3ead94614b16680604d7983d17b25ab1351de356a30c7488" + id: "0x481b46e1234711601011a3509fc058f5412acf2c45a6df50a3efbcef1fdbea2c" size: 1 inactive_validators: - id: "0x3f371596e9faed0adfa565d83749e12dbd3d6bb97f97e3863dd2dfdb24ee7cf3" + id: "0x25813ecb5889bb69bf99c7ef124c41ab5ee402a70d8e8f8b8292be0164bdb102" size: 0 validator_candidates: - id: "0xf07d7fd23b6ab04c1634b2bfc89cd15f11771e7bfc4675a0cee9f5cd1f36fd04" + id: "0xa553c4986f10db731d26ce7681a5f12f36c155e968ddf8aece4adb000fffe675" size: 0 at_risk_validators: contents: [] extra_fields: id: - id: "0x9788679b8a4eb68ebe565041337b029d1192b2ae3883c531b665dd53b54dedcf" + id: "0x9a36528247ca768e9978a0f6261262eb9a1ee1c36171d62a34274a5585154766" size: 0 storage_fund: total_object_storage_rebates: @@ -306,7 +306,7 @@ parameters: validator_low_stake_grace_period: 7 extra_fields: id: - id: "0x0e39e3baeb1a652db096a6510bcd8b3eeac0ea729729251692c1d545d158443e" + id: "0x0a1a9f6c24196754c0fbc53ee19825c9b4dc08c48235ccd24300cba232c5b230" size: 0 reference_gas_price: 1000 validator_report_records: @@ -320,7 +320,7 @@ stake_subsidy: stake_subsidy_decrease_rate: 1000 extra_fields: id: - id: "0x1a73c2836d39587c63685aeac9dd86e618bd4651055c17a03f986020110df0d9" + id: "0x7d66ca292c50b9c41a42b1b6c8e0e364e216ed855ebe2501d635e0460095a681" size: 0 safe_mode: false safe_mode_storage_rewards: @@ -332,6 +332,6 @@ safe_mode_non_refundable_storage_fee: 0 epoch_start_timestamp_ms: 10 extra_fields: id: - id: "0x589a4beca826b64ddfaaa96b69b0e0eb009259b31a825f4d2a9b47d57211a5f4" + id: "0x8f29d92087ed1cc18eb60278915388fb7276b96e0ed4eee38b811366cec36ec9" size: 0 diff --git a/crates/sui-transactional-test-runner/src/test_adapter.rs b/crates/sui-transactional-test-runner/src/test_adapter.rs index fe756f4de8507..6612a7c92b6aa 100644 --- a/crates/sui-transactional-test-runner/src/test_adapter.rs +++ b/crates/sui-transactional-test-runner/src/test_adapter.rs @@ -73,7 +73,6 @@ use sui_types::storage::ObjectStore; use sui_types::storage::ReadStore; use sui_types::transaction::Command; use sui_types::transaction::ProgrammableTransaction; -use sui_types::MOVE_STDLIB_PACKAGE_ID; use sui_types::SUI_SYSTEM_ADDRESS; use sui_types::{ base_types::{ObjectID, ObjectRef, SuiAddress, SUI_ADDRESS_LENGTH}, @@ -93,6 +92,7 @@ use sui_types::{ programmable_transaction_builder::ProgrammableTransactionBuilder, SUI_FRAMEWORK_PACKAGE_ID, }; use sui_types::{utils::to_sender_signed_transaction, SUI_SYSTEM_PACKAGE_ID}; +use sui_types::{BRIDGE_ADDRESS, MOVE_STDLIB_PACKAGE_ID}; use sui_types::{DEEPBOOK_ADDRESS, SUI_DENY_LIST_OBJECT_ID}; use sui_types::{DEEPBOOK_PACKAGE_ID, SUI_RANDOMNESS_STATE_OBJECT_ID}; use tempfile::{tempdir, NamedTempFile}; @@ -1849,6 +1849,13 @@ static NAMED_ADDRESSES: Lazy> = Lazy::new(|| move_compiler::shared::NumberFormat::Hex, ), ); + map.insert( + "bridge".to_string(), + NumericalAddress::new( + BRIDGE_ADDRESS.into_bytes(), + move_compiler::shared::NumberFormat::Hex, + ), + ); map }); @@ -1881,10 +1888,21 @@ pub static PRE_COMPILED: Lazy = Lazy::new(|| { flavor: Flavor::Sui, ..Default::default() }; + let bridge_sources = { + let mut buf = sui_files.to_path_buf(); + buf.extend(["packages", "bridge", "sources"]); + buf.to_string_lossy().to_string() + }; let fully_compiled_res = move_compiler::construct_pre_compiled_lib( vec![PackagePaths { name: Some(("sui-framework".into(), config)), - paths: vec![sui_system_sources, sui_sources, sui_deps, deepbook_sources], + paths: vec![ + sui_system_sources, + sui_sources, + sui_deps, + deepbook_sources, + bridge_sources, + ], named_address_map: NAMED_ADDRESSES.clone(), }], None, diff --git a/crates/sui-types/Cargo.toml b/crates/sui-types/Cargo.toml index 41dd5f93e1ac1..eb01502c274f5 100644 --- a/crates/sui-types/Cargo.toml +++ b/crates/sui-types/Cargo.toml @@ -14,6 +14,7 @@ bcs.workspace = true byteorder.workspace = true chrono.workspace = true consensus-config.workspace = true +num_enum.workspace = true im.workspace = true itertools.workspace = true nonempty.workspace = true diff --git a/crates/sui-types/src/bridge.rs b/crates/sui-types/src/bridge.rs index 79471f69d806d..dacf58410f12a 100644 --- a/crates/sui-types/src/bridge.rs +++ b/crates/sui-types/src/bridge.rs @@ -1,19 +1,96 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 +use enum_dispatch::enum_dispatch; +use move_core_types::ident_str; +use move_core_types::identifier::IdentStr; +use num_enum::TryFromPrimitive; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; + +use crate::base_types::ObjectID; use crate::base_types::SequenceNumber; +use crate::collection_types::LinkedTableNode; +use crate::dynamic_field::{get_dynamic_field_from_store, Field}; use crate::error::SuiResult; use crate::object::Owner; use crate::storage::ObjectStore; +use crate::sui_serde::BigInt; +use crate::sui_serde::Readable; +use crate::versioned::Versioned; use crate::SUI_BRIDGE_OBJECT_ID; -use move_core_types::ident_str; -use move_core_types::identifier::IdentStr; +use crate::{ + base_types::SuiAddress, + collection_types::{Bag, LinkedTable, VecMap}, + error::SuiError, + id::UID, +}; + +pub type BridgeInnerDynamicField = Field; +pub type BridgeRecordDyanmicField = Field< + MoveTypeBridgeMessageKey, + LinkedTableNode, +>; pub const BRIDGE_MODULE_NAME: &IdentStr = ident_str!("bridge"); +pub const BRIDGE_COMMITTEE_MODULE_NAME: &IdentStr = ident_str!("committee"); +pub const BRIDGE_MESSAGE_MODULE_NAME: &IdentStr = ident_str!("message"); pub const BRIDGE_CREATE_FUNCTION_NAME: &IdentStr = ident_str!("create"); +pub const BRIDGE_INIT_COMMITTEE_FUNCTION_NAME: &IdentStr = ident_str!("init_bridge_committee"); +pub const BRIDGE_REGISTER_FOREIGN_TOKEN_FUNCTION_NAME: &IdentStr = + ident_str!("register_foreign_token"); +pub const BRIDGE_CREATE_ADD_TOKEN_ON_SUI_MESSAGE_FUNCTION_NAME: &IdentStr = + ident_str!("create_add_tokens_on_sui_message"); +pub const BRIDGE_EXECUTE_SYSTEM_MESSAGE_FUNCTION_NAME: &IdentStr = + ident_str!("execute_system_message"); pub const BRIDGE_SUPPORTED_ASSET: &[&str] = &["btc", "eth", "usdc", "usdt"]; +pub const BRIDGE_COMMITTEE_MINIMAL_VOTING_POWER: u64 = 7500; // out of 10000 (75%) +pub const BRIDGE_COMMITTEE_MAXIMAL_VOTING_POWER: u64 = 10000; // (100%) + +// Threshold for action to be approved by the committee (our of 10000) +pub const APPROVAL_THRESHOLD_TOKEN_TRANSFER: u64 = 3334; +pub const APPROVAL_THRESHOLD_EMERGENCY_PAUSE: u64 = 450; +pub const APPROVAL_THRESHOLD_EMERGENCY_UNPAUSE: u64 = 5001; +pub const APPROVAL_THRESHOLD_COMMITTEE_BLOCKLIST: u64 = 5001; +pub const APPROVAL_THRESHOLD_LIMIT_UPDATE: u64 = 5001; +pub const APPROVAL_THRESHOLD_ASSET_PRICE_UPDATE: u64 = 5001; +pub const APPROVAL_THRESHOLD_EVM_CONTRACT_UPGRADE: u64 = 5001; +pub const APPROVAL_THRESHOLD_ADD_TOKENS_ON_SUI: u64 = 5001; +pub const APPROVAL_THRESHOLD_ADD_TOKENS_ON_EVM: u64 = 5001; + +// const for initial token ids for convenience +pub const TOKEN_ID_SUI: u8 = 0; +pub const TOKEN_ID_BTC: u8 = 1; +pub const TOKEN_ID_ETH: u8 = 2; +pub const TOKEN_ID_USDC: u8 = 3; +pub const TOKEN_ID_USDT: u8 = 4; + +#[derive( + Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, TryFromPrimitive, JsonSchema, Hash, +)] +#[repr(u8)] +pub enum BridgeChainId { + SuiMainnet = 0, + SuiTestnet = 1, + SuiCustom = 2, + + EthMainnet = 10, + EthSepolia = 11, + EthCustom = 12, +} + +impl BridgeChainId { + pub fn is_sui_chain(&self) -> bool { + matches!( + self, + BridgeChainId::SuiMainnet | BridgeChainId::SuiTestnet | BridgeChainId::SuiCustom + ) + } +} + pub fn get_bridge_obj_initial_shared_version( object_store: &dyn ObjectStore, ) -> SuiResult> { @@ -26,3 +103,375 @@ pub fn get_bridge_obj_initial_shared_version( _ => unreachable!("Bridge object must be shared"), })) } + +/// Bridge provides an abstraction over multiple versions of the inner BridgeInner object. +/// This should be the primary interface to the bridge object in Rust. +/// We use enum dispatch to dispatch all methods defined in BridgeTrait to the actual +/// implementation in the inner types. +#[derive(Debug, Serialize, Deserialize, Clone)] +#[enum_dispatch(BridgeTrait)] +pub enum Bridge { + V1(BridgeInnerV1), +} + +/// Rust version of the Move sui::bridge::Bridge type +/// This repreents the object with 0x9 ID. +/// In Rust, this type should be rarely used since it's just a thin +/// wrapper used to access the inner object. +/// Within this module, we use it to determine the current version of the bridge inner object type, +/// so that we could deserialize the inner object correctly. +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct BridgeWrapper { + pub id: UID, + pub version: Versioned, +} + +/// This is the standard API that all bridge inner object type should implement. +#[enum_dispatch] +pub trait BridgeTrait { + fn bridge_version(&self) -> u64; + fn message_version(&self) -> u8; + fn chain_id(&self) -> u8; + fn sequence_nums(&self) -> &VecMap; + fn committee(&self) -> &MoveTypeBridgeCommittee; + fn treasury(&self) -> &MoveTypeBridgeTreasury; + fn bridge_records(&self) -> &LinkedTable; + fn frozen(&self) -> bool; + fn try_into_bridge_summary(self) -> SuiResult; +} + +#[serde_as] +#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct BridgeSummary { + #[schemars(with = "BigInt")] + #[serde_as(as = "Readable, _>")] + pub bridge_version: u64, + // Message version + pub message_version: u8, + /// Self Chain ID + pub chain_id: u8, + /// Sequence numbers of all message types + #[schemars(with = "Vec<(u8, BigInt)>")] + #[serde_as(as = "Vec<(_, Readable, _>)>")] + pub sequence_nums: Vec<(u8, u64)>, + pub committee: BridgeCommitteeSummary, + /// Summary of the treasury + pub treasury: BridgeTreasurySummary, + /// Object ID of bridge Records (dynamic field) + pub bridge_records_id: ObjectID, + /// Summary of the limiter + pub limiter: BridgeLimiterSummary, + /// Whether the bridge is currently frozen or not + pub is_frozen: bool, + // TODO: add treasury +} + +pub fn get_bridge_wrapper(object_store: &dyn ObjectStore) -> Result { + let wrapper = object_store + .get_object(&SUI_BRIDGE_OBJECT_ID)? + // Don't panic here on None because object_store is a generic store. + .ok_or_else(|| SuiError::SuiBridgeReadError("BridgeWrapper object not found".to_owned()))?; + let move_object = wrapper.data.try_as_move().ok_or_else(|| { + SuiError::SuiBridgeReadError("BridgeWrapper object must be a Move object".to_owned()) + })?; + let result = bcs::from_bytes::(move_object.contents()) + .map_err(|err| SuiError::SuiBridgeReadError(err.to_string()))?; + Ok(result) +} + +pub fn get_bridge(object_store: &dyn ObjectStore) -> Result { + let wrapper = get_bridge_wrapper(object_store)?; + let id = wrapper.version.id.id.bytes; + let version = wrapper.version.version; + match version { + 1 => { + let result: BridgeInnerV1 = get_dynamic_field_from_store(object_store, id, &version) + .map_err(|err| { + SuiError::SuiBridgeReadError(format!( + "Failed to load bridge inner object with ID {:?} and version {:?}: {:?}", + id, version, err + )) + })?; + Ok(Bridge::V1(result)) + } + _ => Err(SuiError::SuiBridgeReadError(format!( + "Unsupported SuiBridge version: {}", + version + ))), + } +} + +/// Rust version of the Move bridge::BridgeInner type. +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct BridgeInnerV1 { + pub bridge_version: u64, + pub message_version: u8, + pub chain_id: u8, + pub sequence_nums: VecMap, + pub committee: MoveTypeBridgeCommittee, + pub treasury: MoveTypeBridgeTreasury, + pub bridge_records: LinkedTable, + pub limiter: MoveTypeBridgeTransferLimiter, + pub frozen: bool, +} + +impl BridgeTrait for BridgeInnerV1 { + fn bridge_version(&self) -> u64 { + self.bridge_version + } + + fn message_version(&self) -> u8 { + self.message_version + } + + fn chain_id(&self) -> u8 { + self.chain_id + } + + fn sequence_nums(&self) -> &VecMap { + &self.sequence_nums + } + + fn committee(&self) -> &MoveTypeBridgeCommittee { + &self.committee + } + + fn treasury(&self) -> &MoveTypeBridgeTreasury { + &self.treasury + } + + fn bridge_records(&self) -> &LinkedTable { + &self.bridge_records + } + + fn frozen(&self) -> bool { + self.frozen + } + + fn try_into_bridge_summary(self) -> SuiResult { + let transfer_limit = self + .limiter + .transfer_limit + .contents + .into_iter() + .map(|e| { + let source = BridgeChainId::try_from(e.key.source).map_err(|_e| { + SuiError::GenericBridgeError { + error: format!("Unrecognized chain id: {}", e.key.source), + } + })?; + let destination = BridgeChainId::try_from(e.key.destination).map_err(|_e| { + SuiError::GenericBridgeError { + error: format!("Unrecognized chain id: {}", e.key.destination), + } + })?; + Ok((source, destination, e.value)) + }) + .collect::>>()?; + let supported_tokens = self + .treasury + .supported_tokens + .contents + .into_iter() + .map(|e| (e.key, e.value)) + .collect::>(); + let id_token_type_map = self + .treasury + .id_token_type_map + .contents + .into_iter() + .map(|e| (e.key, e.value)) + .collect::>(); + let transfer_records = self + .limiter + .transfer_records + .contents + .into_iter() + .map(|e| { + let source = BridgeChainId::try_from(e.key.source).map_err(|_e| { + SuiError::GenericBridgeError { + error: format!("Unrecognized chain id: {}", e.key.source), + } + })?; + let destination = BridgeChainId::try_from(e.key.destination).map_err(|_e| { + SuiError::GenericBridgeError { + error: format!("Unrecognized chain id: {}", e.key.destination), + } + })?; + Ok((source, destination, e.value)) + }) + .collect::>>()?; + let limiter = BridgeLimiterSummary { + transfer_limit, + transfer_records, + }; + Ok(BridgeSummary { + bridge_version: self.bridge_version, + message_version: self.message_version, + chain_id: self.chain_id, + sequence_nums: self + .sequence_nums + .contents + .into_iter() + .map(|e| (e.key, e.value)) + .collect(), + committee: BridgeCommitteeSummary { + members: self + .committee + .members + .contents + .into_iter() + .map(|e| (e.key, e.value)) + .collect(), + member_registration: self + .committee + .member_registrations + .contents + .into_iter() + .map(|e| (e.key, e.value)) + .collect(), + last_committee_update_epoch: self.committee.last_committee_update_epoch, + }, + bridge_records_id: self.bridge_records.id, + limiter, + treasury: BridgeTreasurySummary { + supported_tokens, + id_token_type_map, + }, + is_frozen: self.frozen, + }) + } +} + +/// Rust version of the Move treasury::BridgeTreasury type. +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct MoveTypeBridgeTreasury { + pub treasuries: Bag, + pub supported_tokens: VecMap, + // Mapping token id to type name + pub id_token_type_map: VecMap, + // Bag for storing potential new token waiting to be approved + pub waiting_room: Bag, +} + +#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema, Default, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct BridgeTokenMetadata { + pub id: u8, + pub decimal_multiplier: u64, + pub notional_value: u64, + pub native_token: bool, +} + +/// Rust version of the Move committee::BridgeCommittee type. +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct MoveTypeBridgeCommittee { + pub members: VecMap, MoveTypeCommitteeMember>, + pub member_registrations: VecMap, + pub last_committee_update_epoch: u64, +} + +/// Rust version of the Move committee::CommitteeMemberRegistration type. +#[serde_as] +#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema, Default, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct MoveTypeCommitteeMemberRegistration { + pub sui_address: SuiAddress, + pub bridge_pubkey_bytes: Vec, + pub http_rest_url: Vec, +} + +#[serde_as] +#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema, Default)] +#[serde(rename_all = "camelCase")] +pub struct BridgeCommitteeSummary { + pub members: Vec<(Vec, MoveTypeCommitteeMember)>, + pub member_registration: Vec<(SuiAddress, MoveTypeCommitteeMemberRegistration)>, + pub last_committee_update_epoch: u64, +} + +#[serde_as] +#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema, Default)] +#[serde(rename_all = "camelCase")] +pub struct BridgeLimiterSummary { + pub transfer_limit: Vec<(BridgeChainId, BridgeChainId, u64)>, + pub transfer_records: Vec<(BridgeChainId, BridgeChainId, MoveTypeBridgeTransferRecord)>, +} + +#[serde_as] +#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema, Default)] +#[serde(rename_all = "camelCase")] +pub struct BridgeTreasurySummary { + pub supported_tokens: Vec<(String, BridgeTokenMetadata)>, + pub id_token_type_map: Vec<(u8, String)>, +} + +/// Rust version of the Move committee::CommitteeMember type. +#[serde_as] +#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema, Default, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct MoveTypeCommitteeMember { + pub sui_address: SuiAddress, + pub bridge_pubkey_bytes: Vec, + pub voting_power: u64, + pub http_rest_url: Vec, + pub blocklisted: bool, +} + +/// Rust version of the Move message::BridgeMessageKey type. +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] +pub struct MoveTypeBridgeMessageKey { + pub source_chain: u8, + pub message_type: u8, + pub bridge_seq_num: u64, +} + +/// Rust version of the Move limiter::TransferLimiter type. +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct MoveTypeBridgeTransferLimiter { + pub transfer_limit: VecMap, + pub transfer_records: VecMap, +} + +/// Rust version of the Move chain_ids::BridgeRoute type. +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct MoveTypeBridgeRoute { + pub source: u8, + pub destination: u8, +} + +/// Rust version of the Move limiter::TransferRecord type. +#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)] +pub struct MoveTypeBridgeTransferRecord { + hour_head: u64, + hour_tail: u64, + per_hour_amounts: Vec, + total_amount: u64, +} + +/// Rust version of the Move message::BridgeMessage type. +#[derive(Debug, Serialize, Deserialize)] +pub struct MoveTypeBridgeMessage { + pub message_type: u8, + pub message_version: u8, + pub seq_num: u64, + pub source_chain: u8, + pub payload: Vec, +} + +/// Rust version of the Move message::BridgeMessage type. +#[derive(Debug, Serialize, Deserialize)] +pub struct MoveTypeBridgeRecord { + pub message: MoveTypeBridgeMessage, + pub verified_signatures: Option>>, + pub claimed: bool, +} + +pub fn is_bridge_committee_initiated(object_store: &dyn ObjectStore) -> SuiResult { + match get_bridge(object_store) { + Ok(bridge) => Ok(!bridge.committee().members.contents.is_empty()), + Err(SuiError::SuiBridgeReadError(..)) => Ok(false), + Err(other) => Err(other), + } +} diff --git a/crates/sui-types/src/crypto.rs b/crates/sui-types/src/crypto.rs index 5455fc13b690e..b4732bc05a80f 100644 --- a/crates/sui-types/src/crypto.rs +++ b/crates/sui-types/src/crypto.rs @@ -148,6 +148,14 @@ impl SuiKeyPair { SuiKeyPair::Secp256r1(kp) => PublicKey::Secp256r1(kp.public().into()), } } + + pub fn copy(&self) -> Self { + match self { + SuiKeyPair::Ed25519(kp) => kp.copy().into(), + SuiKeyPair::Secp256k1(kp) => kp.copy().into(), + SuiKeyPair::Secp256r1(kp) => kp.copy().into(), + } + } } impl Signer for SuiKeyPair { @@ -211,6 +219,15 @@ impl SuiKeyPair { _ => Err(eyre!("Invalid bytes")), } } + + pub fn to_bytes_no_flag(&self) -> Vec { + match self { + SuiKeyPair::Ed25519(kp) => kp.as_bytes().to_vec(), + SuiKeyPair::Secp256k1(kp) => kp.as_bytes().to_vec(), + SuiKeyPair::Secp256r1(kp) => kp.as_bytes().to_vec(), + } + } + /// Encode a SuiKeyPair as `flag || privkey` in Bech32 starting with "suiprivkey" to a string. Note that the pubkey is not encoded. pub fn encode(&self) -> Result { Bech32::encode(self.to_bytes(), SUI_PRIV_KEY_PREFIX).map_err(|e| eyre!(e)) diff --git a/crates/sui-types/src/error.rs b/crates/sui-types/src/error.rs index 52def41456b8a..5fe67e0b8c2e6 100644 --- a/crates/sui-types/src/error.rs +++ b/crates/sui-types/src/error.rs @@ -498,6 +498,9 @@ pub enum SuiError { #[error("Authority Error: {error:?}")] GenericAuthorityError { error: String }, + #[error("Generic Bridge Error: {error:?}")] + GenericBridgeError { error: String }, + #[error("Failed to dispatch subscription: {error:?}")] FailedToDispatchSubscription { error: String }, @@ -595,6 +598,9 @@ pub enum SuiError { #[error("Failed to read or deserialize system state related data structures on-chain: {0}")] SuiSystemStateReadError(String), + #[error("Failed to read or deserialize bridge related data structures on-chain: {0}")] + SuiBridgeReadError(String), + #[error("Unexpected version error: {0}")] UnexpectedVersion(String), diff --git a/crates/sui-types/src/transaction.rs b/crates/sui-types/src/transaction.rs index fb0b4b983e7e7..f0f420772951c 100644 --- a/crates/sui-types/src/transaction.rs +++ b/crates/sui-types/src/transaction.rs @@ -2,7 +2,7 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -use super::{base_types::*, error::*}; +use super::{base_types::*, error::*, SUI_BRIDGE_OBJECT_ID}; use crate::authenticator_state::ActiveJwk; use crate::committee::{Committee, EpochId, ProtocolVersion}; use crate::crypto::{ @@ -11,7 +11,7 @@ use crate::crypto::{ RandomnessRound, Signature, Signer, SuiSignatureInner, ToFromBytes, }; use crate::digests::{CertificateDigest, SenderSignedDataDigest}; -use crate::digests::{ConsensusCommitDigest, ZKLoginInputsDigest}; +use crate::digests::{ChainIdentifier, ConsensusCommitDigest, ZKLoginInputsDigest}; use crate::execution::SharedInput; use crate::message_envelope::{Envelope, Message, TrustedEnvelope, VerifiedEnvelope}; use crate::messages_checkpoint::CheckpointTimestamp; @@ -297,6 +297,8 @@ pub enum EndOfEpochTransactionKind { AuthenticatorStateExpire(AuthenticatorStateExpire), RandomnessStateCreate, DenyListStateCreate, + BridgeStateCreate(ChainIdentifier), + BridgeCommitteeInit(SequenceNumber), } impl EndOfEpochTransactionKind { @@ -344,6 +346,14 @@ impl EndOfEpochTransactionKind { Self::DenyListStateCreate } + pub fn new_bridge_create(chain_identifier: ChainIdentifier) -> Self { + Self::BridgeStateCreate(chain_identifier) + } + + pub fn init_bridge_committee(bridge_shared_version: SequenceNumber) -> Self { + Self::BridgeCommitteeInit(bridge_shared_version) + } + fn input_objects(&self) -> Vec { match self { Self::ChangeEpoch(_) => { @@ -363,20 +373,50 @@ impl EndOfEpochTransactionKind { } Self::RandomnessStateCreate => vec![], Self::DenyListStateCreate => vec![], + Self::BridgeStateCreate(_) => vec![], + Self::BridgeCommitteeInit(bridge_version) => vec![ + InputObjectKind::SharedMoveObject { + id: SUI_BRIDGE_OBJECT_ID, + initial_shared_version: *bridge_version, + mutable: true, + }, + InputObjectKind::SharedMoveObject { + id: SUI_SYSTEM_STATE_OBJECT_ID, + initial_shared_version: SUI_SYSTEM_STATE_OBJECT_SHARED_VERSION, + mutable: true, + }, + ], } } fn shared_input_objects(&self) -> impl Iterator + '_ { match self { - Self::ChangeEpoch(_) => Either::Left(iter::once(SharedInputObject::SUI_SYSTEM_OBJ)), - Self::AuthenticatorStateExpire(expire) => Either::Left(iter::once(SharedInputObject { - id: SUI_AUTHENTICATOR_STATE_OBJECT_ID, - initial_shared_version: expire.authenticator_obj_initial_shared_version(), - mutable: true, - })), + Self::ChangeEpoch(_) => { + Either::Left(vec![SharedInputObject::SUI_SYSTEM_OBJ].into_iter()) + } + Self::AuthenticatorStateExpire(expire) => Either::Left( + vec![SharedInputObject { + id: SUI_AUTHENTICATOR_STATE_OBJECT_ID, + initial_shared_version: expire.authenticator_obj_initial_shared_version(), + mutable: true, + }] + .into_iter(), + ), Self::AuthenticatorStateCreate => Either::Right(iter::empty()), Self::RandomnessStateCreate => Either::Right(iter::empty()), Self::DenyListStateCreate => Either::Right(iter::empty()), + Self::BridgeStateCreate(_) => Either::Right(iter::empty()), + Self::BridgeCommitteeInit(bridge_version) => Either::Left( + vec![ + SharedInputObject { + id: SUI_BRIDGE_OBJECT_ID, + initial_shared_version: *bridge_version, + mutable: true, + }, + SharedInputObject::SUI_SYSTEM_OBJ, + ] + .into_iter(), + ), } } @@ -395,6 +435,10 @@ impl EndOfEpochTransactionKind { // Transaction should have been rejected earlier (or never formed). assert!(config.enable_coin_deny_list()); } + Self::BridgeStateCreate(_) | Self::BridgeCommitteeInit(_) => { + // Transaction should have been rejected earlier (or never formed). + assert!(config.enable_bridge()); + } } Ok(()) } @@ -477,6 +521,20 @@ impl VersionedProtocolMessage for TransactionKind { }); } } + EndOfEpochTransactionKind::BridgeStateCreate(_) => { + if !protocol_config.enable_bridge() { + return Err(SuiError::UnsupportedFeatureError { + error: "bridge not enabled".to_string(), + }); + } + } + EndOfEpochTransactionKind::BridgeCommitteeInit(_) => { + if !protocol_config.enable_bridge() { + return Err(SuiError::UnsupportedFeatureError { + error: "bridge not enabled".to_string(), + }); + } + } } } @@ -1315,7 +1373,18 @@ impl TransactionKind { }] } Self::EndOfEpochTransaction(txns) => { - txns.iter().flat_map(|txn| txn.input_objects()).collect() + // Dedup since transactions may have a overlap in input objects. + // Note: it's critical to ensure the order of inputs are deterministic. + let before_dedup: Vec<_> = + txns.iter().flat_map(|txn| txn.input_objects()).collect(); + let mut has_seen = HashSet::new(); + let mut after_dedup = vec![]; + for obj in before_dedup { + if has_seen.insert(obj) { + after_dedup.push(obj); + } + } + after_dedup } Self::ProgrammableTransaction(p) => return p.input_objects(), }; @@ -2751,7 +2820,7 @@ pub trait VersionedProtocolMessage { fn check_version_supported(&self, protocol_config: &ProtocolConfig) -> SuiResult; } -#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, PartialOrd, Ord)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, PartialOrd, Ord, Hash)] pub enum InputObjectKind { // A Move package, must be immutable. MovePackage(ObjectID), diff --git a/crates/sui/Cargo.toml b/crates/sui/Cargo.toml index fc8dd93c9066b..3f40302927e7c 100644 --- a/crates/sui/Cargo.toml +++ b/crates/sui/Cargo.toml @@ -39,8 +39,10 @@ miette.workspace = true datatest-stable.workspace = true insta.workspace = true shlex.workspace = true +futures.workspace = true sui-config.workspace = true +sui-bridge.workspace = true sui-execution = { path = "../../sui-execution" } sui-swarm-config.workspace = true sui-genesis-builder.workspace = true diff --git a/crates/sui/src/sui_commands.rs b/crates/sui/src/sui_commands.rs index c4c3dfef5aa10..ec2b6e26d54cc 100644 --- a/crates/sui/src/sui_commands.rs +++ b/crates/sui/src/sui_commands.rs @@ -16,6 +16,9 @@ use std::io::{stderr, stdout, Write}; use std::num::NonZeroUsize; use std::path::{Path, PathBuf}; use std::{fs, io}; +use sui_bridge::config::{read_bridge_authority_key, BridgeCommitteeConfig}; +use sui_bridge::sui_client::SuiBridgeClient; +use sui_bridge::sui_transaction_builder::build_committee_register_transaction; use sui_config::node::Genesis; use sui_config::p2p::SeedPeer; use sui_config::{ @@ -35,6 +38,7 @@ use sui_swarm_config::genesis_config::{GenesisConfig, DEFAULT_NUMBER_OF_AUTHORIT use sui_swarm_config::network_config::NetworkConfig; use sui_swarm_config::network_config_builder::ConfigBuilder; use sui_swarm_config::node_config_builder::FullnodeConfigBuilder; +use sui_types::base_types::SuiAddress; use sui_types::crypto::{SignatureScheme, SuiKeyPair}; use tracing::info; @@ -150,6 +154,18 @@ pub enum SuiCommand { cmd: sui_move::Command, }, + /// Command to initialize the bridge committee, usually used when + /// running local bridge cluster. + #[clap(name = "bridge-committee-init")] + BridgeInitialize { + #[clap(long = "network.config")] + network_config: Option, + #[clap(long = "client.config")] + client_config: Option, + #[clap(long = "bridge_committee.config")] + bridge_committee_config_path: PathBuf, + }, + /// Tool for Fire Drill FireDrill { #[clap(subcommand)] @@ -319,6 +335,81 @@ impl SuiCommand { build_config, cmd, } => execute_move_command(package_path, build_config, cmd), + SuiCommand::BridgeInitialize { + network_config, + client_config, + bridge_committee_config_path, + } => { + // Load the config of the Sui authority. + let network_config_path = network_config + .clone() + .unwrap_or(sui_config_dir()?.join(SUI_NETWORK_CONFIG)); + let network_config: NetworkConfig = PersistedConfig::read(&network_config_path) + .map_err(|err| { + err.context(format!( + "Cannot open Sui network config file at {:?}", + network_config_path + )) + })?; + let bridge_committee_config: BridgeCommitteeConfig = + PersistedConfig::read(&bridge_committee_config_path).map_err(|err| { + err.context(format!( + "Cannot open Bridge Committee config file at {:?}", + network_config_path + )) + })?; + + let config_path = + client_config.unwrap_or(sui_config_dir()?.join(SUI_CLIENT_CONFIG)); + let mut context = WalletContext::new(&config_path, None, None)?; + let rgp = context.get_reference_gas_price().await?; + let rpc_url = &context.config.get_active_env()?.rpc; + println!("rpc_url: {}", rpc_url); + let sui_bridge_client = SuiBridgeClient::new(rpc_url).await?; + let bridge_arg = sui_bridge_client + .get_mutable_bridge_object_arg_must_succeed() + .await; + assert_eq!( + network_config.validator_configs().len(), + bridge_committee_config + .bridge_authority_port_and_key_path + .len() + ); + for node_config in network_config.validator_configs() { + let account_kp = node_config.account_key_pair.keypair(); + context.add_account(None, account_kp.copy()); + } + + let context = context; + let mut tasks = vec![]; + for (node_config, (port, key_path)) in network_config + .validator_configs() + .iter() + .zip(bridge_committee_config.bridge_authority_port_and_key_path) + { + let account_kp = node_config.account_key_pair.keypair(); + let sui_address = SuiAddress::from(&account_kp.public()); + let gas_obj_ref = context + .get_one_gas_object_owned_by_address(sui_address) + .await? + .expect("Validator does not own any gas objects"); + let kp = read_bridge_authority_key(&key_path)?; + // build registration tx + let tx = build_committee_register_transaction( + sui_address, + &gas_obj_ref, + bridge_arg, + kp, + &format!("http://127.0.0.1:{port}"), + rgp, + ) + .unwrap(); + let signed_tx = context.sign_transaction(&tx); + tasks.push(context.execute_transaction_must_succeed(signed_tx)); + } + futures::future::join_all(tasks).await; + Ok(()) + } SuiCommand::FireDrill { fire_drill } => run_fire_drill(fire_drill).await, } } diff --git a/crates/sui/src/validator_commands.rs b/crates/sui/src/validator_commands.rs index 624bba3bb17f4..f18629189860d 100644 --- a/crates/sui/src/validator_commands.rs +++ b/crates/sui/src/validator_commands.rs @@ -34,6 +34,10 @@ use fastcrypto::{ }; use serde::Serialize; use shared_crypto::intent::{Intent, IntentMessage, IntentScope}; +use sui_bridge::config::{read_bridge_authority_key, BridgeNodeConfig}; +use sui_bridge::sui_client::SuiClient as SuiBridgeClient; +use sui_bridge::sui_transaction_builder::build_committee_register_transaction; +use sui_config::Config; use sui_json_rpc_types::{ SuiObjectDataOptions, SuiTransactionBlockResponse, SuiTransactionBlockResponseOptions, }; @@ -163,6 +167,15 @@ pub enum SuiValidatorCommand { #[clap(name = "gas-budget", long)] gas_budget: Option, }, + /// Sui native bridge committee member registration + BridgeCommitteeRegistration { + /// Path to bridge node config + #[clap(long)] + bridge_node_config_path: PathBuf, + /// Bridge authority URL which clients collects action signatures from + #[clap(long)] + bridge_authority_url: String, + }, } #[derive(Serialize)] @@ -181,6 +194,7 @@ pub enum SuiValidatorCommandResponse { data: TransactionData, serialized_data: String, }, + BridgeCommitteeRegistration(SuiTransactionBlockResponse), } fn make_key_files( @@ -454,6 +468,50 @@ impl SuiValidatorCommand { serialized_data, } } + SuiValidatorCommand::BridgeCommitteeRegistration { + bridge_node_config_path, + bridge_authority_url, + } => { + let bridge_config = match BridgeNodeConfig::load(bridge_node_config_path) { + Ok(config) => config, + Err(e) => panic!("Couldn't load BridgeNodeConfig, caused by: {e}"), + }; + // Read bridge keypair + let ecdsa_keypair = + read_bridge_authority_key(&bridge_config.bridge_authority_key_path_base64_raw)?; + + let address = context.active_address()?; + println!("Starting bridge committee registration for Sui validator: {address}, with bridge public key: {}", ecdsa_keypair.public); + + let bridge_client = SuiBridgeClient::new(&bridge_config.sui.sui_rpc_url).await?; + let bridge = bridge_client + .get_mutable_bridge_object_arg_must_succeed() + .await; + + let gas = context + .get_one_gas_object_owned_by_address(address) + .await? + .unwrap_or_else(|| panic!("Cannot find gas object from address : {address}")); + + let gas_price = context.get_reference_gas_price().await?; + let tx_data = build_committee_register_transaction( + address, + &gas, + bridge, + ecdsa_keypair, + &bridge_authority_url, + gas_price, + ) + .map_err(|e| anyhow!("{e:?}"))?; + + let tx = context.sign_transaction(&tx_data); + let response = context.execute_transaction_must_succeed(tx).await; + println!( + "Committee registration successful. Transaction digest: {}", + response.digest + ); + SuiValidatorCommandResponse::BridgeCommitteeRegistration(response) + } }); ret } @@ -689,6 +747,9 @@ impl Display for SuiValidatorCommandResponse { data, serialized_data )?; } + SuiValidatorCommandResponse::BridgeCommitteeRegistration(response) => { + write!(writer, "{}", response)?; + } } write!(f, "{}", writer.trim_end_matches('\n')) } diff --git a/crates/test-cluster/Cargo.toml b/crates/test-cluster/Cargo.toml index cbb98e51e22f3..826433cb330f2 100644 --- a/crates/test-cluster/Cargo.toml +++ b/crates/test-cluster/Cargo.toml @@ -8,6 +8,8 @@ edition = "2021" [dependencies] anyhow.workspace = true +bcs.workspace = true +fastcrypto.workspace = true futures.workspace = true tracing.workspace = true jsonrpsee.workspace = true @@ -19,12 +21,14 @@ sui-framework.workspace = true sui-swarm-config.workspace = true sui-json-rpc.workspace = true sui-json-rpc-types.workspace = true +sui-json-rpc-api.workspace = true sui-node.workspace = true sui-protocol-config.workspace = true sui-swarm.workspace = true sui-types = { workspace = true, features = ["test-utils"] } prometheus.workspace = true sui-keys.workspace = true +sui-bridge.workspace = true sui-sdk.workspace = true sui-test-transaction-builder.workspace = true diff --git a/crates/test-cluster/src/lib.rs b/crates/test-cluster/src/lib.rs index 3c861eca32c85..b9e4d8254cb94 100644 --- a/crates/test-cluster/src/lib.rs +++ b/crates/test-cluster/src/lib.rs @@ -2,27 +2,42 @@ // SPDX-License-Identifier: Apache-2.0 use futures::{future::join_all, StreamExt}; +use jsonrpsee::core::RpcResult; use jsonrpsee::http_client::{HttpClient, HttpClientBuilder}; use jsonrpsee::ws_client::WsClient; use jsonrpsee::ws_client::WsClientBuilder; use rand::{distributions::*, rngs::OsRng, seq::SliceRandom}; +use std::collections::BTreeMap; use std::collections::HashMap; use std::net::SocketAddr; use std::num::NonZeroUsize; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::time::Duration; +use sui_bridge::crypto::{BridgeAuthorityKeyPair, BridgeAuthoritySignInfo}; +use sui_bridge::sui_transaction_builder::build_add_tokens_on_sui_transaction; +use sui_bridge::sui_transaction_builder::build_committee_register_transaction; +use sui_bridge::types::BridgeCommitteeValiditySignInfo; +use sui_bridge::types::CertifiedBridgeAction; +use sui_bridge::types::VerifiedCertifiedBridgeAction; +use sui_bridge::utils::publish_coins_return_add_coins_on_sui_action; +use sui_bridge::utils::wait_for_server_to_be_up; +use sui_config::local_ip_utils::get_available_port; use sui_config::node::{AuthorityOverloadConfig, DBCheckpointConfig, RunWithRange}; use sui_config::{Config, SUI_CLIENT_CONFIG, SUI_NETWORK_CONFIG}; use sui_config::{NodeConfig, PersistedConfig, SUI_KEYSTORE_FILENAME}; use sui_core::authority_aggregator::AuthorityAggregator; use sui_core::authority_client::NetworkAuthorityClient; +use sui_json_rpc_api::BridgeReadApiClient; +use sui_json_rpc_types::SuiTransactionBlockResponseOptions; use sui_json_rpc_types::{ - SuiTransactionBlockEffectsAPI, SuiTransactionBlockResponse, TransactionFilter, + SuiExecutionStatus, SuiTransactionBlockEffectsAPI, SuiTransactionBlockResponse, + TransactionFilter, }; use sui_keys::keystore::{AccountKeystore, FileBasedKeystore, Keystore}; use sui_node::SuiNodeHandle; use sui_protocol_config::{ProtocolVersion, SupportedProtocolVersions}; +use sui_sdk::apis::QuorumDriverApi; use sui_sdk::sui_client_config::{SuiClientConfig, SuiEnv}; use sui_sdk::wallet_context::WalletContext; use sui_sdk::{SuiClient, SuiClientBuilder}; @@ -38,6 +53,8 @@ use sui_swarm_config::node_config_builder::{FullnodeConfigBuilder, ValidatorConf use sui_test_transaction_builder::TestTransactionBuilder; use sui_types::base_types::ConciseableName; use sui_types::base_types::{AuthorityName, ObjectID, ObjectRef, SuiAddress}; +use sui_types::bridge::{get_bridge, TOKEN_ID_BTC, TOKEN_ID_ETH, TOKEN_ID_USDC, TOKEN_ID_USDT}; +use sui_types::bridge::{get_bridge_obj_initial_shared_version, BridgeSummary, BridgeTrait}; use sui_types::committee::CommitteeTrait; use sui_types::committee::{Committee, EpochId}; use sui_types::crypto::KeypairTraits; @@ -52,8 +69,10 @@ use sui_types::sui_system_state::SuiSystemState; use sui_types::sui_system_state::SuiSystemStateTrait; use sui_types::traffic_control::{PolicyConfig, RemoteFirewallConfig}; use sui_types::transaction::{ - CertifiedTransaction, Transaction, TransactionData, TransactionDataAPI, TransactionKind, + CertifiedTransaction, ObjectArg, Transaction, TransactionData, TransactionDataAPI, + TransactionKind, }; +use sui_types::SUI_BRIDGE_OBJECT_ID; use tokio::time::{timeout, Instant}; use tokio::{task::JoinHandle, time::sleep}; use tracing::{error, info}; @@ -97,6 +116,9 @@ pub struct TestCluster { pub swarm: Swarm, pub wallet: WalletContext, pub fullnode_handle: FullNodeHandle, + + pub bridge_authority_keys: Option>, + pub bridge_server_ports: Option>, } impl TestCluster { @@ -108,6 +130,10 @@ impl TestCluster { &self.fullnode_handle.sui_client } + pub fn quorum_driver_api(&self) -> &QuorumDriverApi { + self.sui_client().quorum_driver_api() + } + pub fn rpc_url(&self) -> &str { &self.fullnode_handle.rpc_url } @@ -242,6 +268,10 @@ impl TestCluster { .compute_object_reference() } + pub async fn get_bridge_summary(&self) -> RpcResult { + self.sui_client().http().get_latest_bridge().await + } + pub async fn get_object_or_tombstone_from_fullnode_store( &self, object_id: ObjectID, @@ -473,6 +503,54 @@ impl TestCluster { } } + pub async fn trigger_reconfiguration_if_not_yet_and_assert_bridge_committee_initialized(&self) { + let mut bridge = + get_bridge(self.fullnode_handle.sui_node.state().get_object_store()).unwrap(); + if !bridge.committee().members.contents.is_empty() { + assert_eq!( + self.swarm.active_validators().count(), + bridge.committee().members.contents.len() + ); + return; + } + // wait for next epoch + self.trigger_reconfiguration().await; + bridge = get_bridge(self.fullnode_handle.sui_node.state().get_object_store()).unwrap(); + // Committee should be initiated + assert!(bridge.committee().member_registrations.contents.is_empty()); + assert_eq!( + self.swarm.active_validators().count(), + bridge.committee().members.contents.len() + ); + } + + // Wait for bridge node in the cluster to be up and running. + pub async fn wait_for_bridge_cluster_to_be_up(&self, timeout_sec: u64) { + let bridge_ports = self.bridge_server_ports.as_ref().unwrap(); + let mut tasks = vec![]; + for port in bridge_ports.iter() { + let server_url = format!("http://127.0.0.1:{}", port); + tasks.push(wait_for_server_to_be_up(server_url, timeout_sec)); + } + join_all(tasks) + .await + .into_iter() + .collect::>>() + .unwrap(); + } + + pub async fn get_mut_bridge_arg(&self) -> Option { + get_bridge_obj_initial_shared_version( + self.fullnode_handle.sui_node.state().get_object_store(), + ) + .unwrap() + .map(|seq| ObjectArg::SharedObject { + id: SUI_BRIDGE_OBJECT_ID, + initial_shared_version: seq, + mutable: true, + }) + } + pub async fn wait_for_authenticator_state_update(&self) { timeout( Duration::from_secs(60), @@ -691,6 +769,27 @@ impl TestCluster { .unwrap() } + pub async fn transfer_sui_must_exceed( + &self, + sender: SuiAddress, + receiver: SuiAddress, + amount: u64, + ) -> ObjectID { + // let sender = self.get_address_0(); + let tx = self + .test_transaction_builder_with_sender(sender) + .await + .transfer_sui(Some(amount), receiver) + .build(); + let effects = self + .sign_and_execute_transaction(&tx) + .await + .effects + .unwrap(); + assert_eq!(&SuiExecutionStatus::Success, effects.status()); + effects.created().first().unwrap().object_id() + } + #[cfg(msim)] pub fn set_safe_mode_expected(&self, value: bool) { for n in self.all_node_handles() { @@ -1059,7 +1158,219 @@ impl TestClusterBuilder { swarm, wallet, fullnode_handle, + bridge_authority_keys: None, + bridge_server_ports: None, + } + } + + pub async fn build_with_bridge( + self, + bridge_authority_keys: Vec, + deploy_tokens: bool, + ) -> TestCluster { + let timer = Instant::now(); + let mut test_cluster = self.build().await; + info!( + "TestCluster build took {:?} secs", + timer.elapsed().as_secs() + ); + let ref_gas_price = test_cluster.get_reference_gas_price().await; + let bridge_arg = test_cluster.get_mut_bridge_arg().await.unwrap(); + assert_eq!( + bridge_authority_keys.len(), + test_cluster.swarm.active_validators().count() + ); + + let publish_tokens_tasks = if deploy_tokens { + let quorum_driver_api = Arc::new(test_cluster.quorum_driver_api().clone()); + // Register tokens + let token_packages_dir = [ + Path::new("../../bridge/move/tokens/btc"), + Path::new("../../bridge/move/tokens/eth"), + Path::new("../../bridge/move/tokens/usdc"), + Path::new("../../bridge/move/tokens/usdt"), + ]; + + // publish coin packages + let mut publish_tokens_tasks = vec![]; + let sender = test_cluster.get_address_0(); + let gases = test_cluster + .wallet + .get_gas_objects_owned_by_address(sender, None) + .await + .unwrap(); + assert!(gases.len() >= token_packages_dir.len()); + for (token_package_dir, gas) in token_packages_dir.iter().zip(gases) { + let tx = test_cluster + .test_transaction_builder_with_gas_object(sender, gas) + .await + .publish(token_package_dir.to_path_buf()) + .build(); + let tx = test_cluster.wallet.sign_transaction(&tx); + let api_clone = quorum_driver_api.clone(); + publish_tokens_tasks.push(tokio::spawn(async move { + api_clone.execute_transaction_block( + tx, + SuiTransactionBlockResponseOptions::new() + .with_effects() + .with_input() + .with_events() + .with_object_changes() + .with_balance_changes(), + Some(sui_types::quorum_driver_types::ExecuteTransactionRequestType::WaitForLocalExecution), + ).await + })); + } + Some(publish_tokens_tasks) + } else { + None + }; + + let mut server_ports = vec![]; + let mut tasks = vec![]; + // use a different sender address than the coin publish to avoid object locks + let sender_address = test_cluster.get_address_1(); + for (node, kp) in test_cluster + .swarm + .active_validators() + .zip(bridge_authority_keys.iter()) + { + let validator_address = node.config().sui_address(); + // 1, send some gas to validator + test_cluster + .transfer_sui_must_exceed(sender_address, validator_address, 1000000000) + .await; + // 2, create committee registration tx + let gas = test_cluster + .wallet + .get_one_gas_object_owned_by_address(validator_address) + .await + .unwrap() + .unwrap(); + + let server_port = get_available_port("127.0.0.1"); + let server_url = format!("http://127.0.0.1:{}", server_port); + server_ports.push(server_port); + let data = build_committee_register_transaction( + validator_address, + &gas, + bridge_arg, + kp.copy(), + &server_url, + ref_gas_price, + ) + .unwrap(); + + let tx = Transaction::from_data_and_signer( + data, + vec![node.config().account_key_pair.keypair()], + ); + tasks.push( + test_cluster + .sui_client() + .quorum_driver_api() + .execute_transaction_block( + tx, + SuiTransactionBlockResponseOptions::new().with_effects(), + None, + ), + ); + } + // The tx may fail if a member tries to register when the committee is already finalized. + // In that case, we just need to check the committee members is not empty since once + // the committee is finalized, it should not be empty. + let responses = join_all(tasks).await; + let mut has_failure = false; + for response in responses { + if response.unwrap().effects.unwrap().status() != &SuiExecutionStatus::Success { + has_failure = true; + } } + if has_failure { + let bridge_summary = test_cluster.get_bridge_summary().await.unwrap(); + assert_ne!(bridge_summary.committee.members.len(), 0); + } + + if deploy_tokens { + let timer = Instant::now(); + let publish_tokens_responses = join_all(publish_tokens_tasks.unwrap()) + .await + .into_iter() + .collect::, _>>() + .unwrap() + .into_iter() + .collect::, _>>() + .unwrap(); + for resp in &publish_tokens_responses { + assert_eq!( + resp.effects.as_ref().unwrap().status(), + &SuiExecutionStatus::Success + ); + } + let token_ids = vec![TOKEN_ID_BTC, TOKEN_ID_ETH, TOKEN_ID_USDC, TOKEN_ID_USDT]; + let token_prices = vec![500_000_000u64, 30_000_000u64, 1_000u64, 1_000u64]; + + let action = publish_coins_return_add_coins_on_sui_action( + test_cluster.wallet_mut(), + bridge_arg, + publish_tokens_responses, + token_ids, + token_prices, + 0, + ) + .await; + info!("register tokens took {:?} secs", timer.elapsed().as_secs()); + let sig_map = bridge_authority_keys + .iter() + .map(|key| { + ( + key.public().into(), + BridgeAuthoritySignInfo::new(&action, key).signature, + ) + }) + .collect::>(); + let certified_action = CertifiedBridgeAction::new_from_data_and_sig( + action, + BridgeCommitteeValiditySignInfo { + signatures: sig_map.clone(), + }, + ); + let verifired_action_cert = + VerifiedCertifiedBridgeAction::new_from_verified(certified_action); + let sender_address = test_cluster.get_address_0(); + + // Wait until committee is set up + test_cluster + .trigger_reconfiguration_if_not_yet_and_assert_bridge_committee_initialized() + .await; + + let tx = build_add_tokens_on_sui_transaction( + sender_address, + &test_cluster + .wallet + .get_one_gas_object_owned_by_address(sender_address) + .await + .unwrap() + .unwrap(), + verifired_action_cert, + bridge_arg, + ) + .unwrap(); + + let response = test_cluster.sign_and_execute_transaction(&tx).await; + assert_eq!( + response.effects.unwrap().status(), + &SuiExecutionStatus::Success + ); + info!("Deploy tokens took {:?} secs", timer.elapsed().as_secs()); + } + info!( + "TestCluster build_with_bridge took {:?} secs", + timer.elapsed().as_secs() + ); + test_cluster.bridge_authority_keys = Some(bridge_authority_keys); + test_cluster.bridge_server_ports = Some(server_ports); + test_cluster } /// Start a Swarm and set up WalletConfig diff --git a/sui-execution/latest/sui-adapter/src/execution_engine.rs b/sui-execution/latest/sui-adapter/src/execution_engine.rs index 46612432f1c53..cb6b698464863 100644 --- a/sui-execution/latest/sui-adapter/src/execution_engine.rs +++ b/sui-execution/latest/sui-adapter/src/execution_engine.rs @@ -23,20 +23,30 @@ mod checked { RANDOMNESS_MODULE_NAME, RANDOMNESS_STATE_CREATE_FUNCTION_NAME, RANDOMNESS_STATE_UPDATE_FUNCTION_NAME, }; - use sui_types::SUI_RANDOMNESS_STATE_OBJECT_ID; + use sui_types::{BRIDGE_ADDRESS, SUI_BRIDGE_OBJECT_ID, SUI_RANDOMNESS_STATE_OBJECT_ID}; use tracing::{info, instrument, trace, warn}; use crate::programmable_transactions; use crate::type_layout_resolver::TypeLayoutResolver; use crate::{gas_charger::GasCharger, temporary_store::TemporaryStore}; + use move_core_types::ident_str; use sui_protocol_config::{check_limit_by_meter, LimitThresholdCrossed, ProtocolConfig}; use sui_types::authenticator_state::{ AUTHENTICATOR_STATE_CREATE_FUNCTION_NAME, AUTHENTICATOR_STATE_EXPIRE_JWKS_FUNCTION_NAME, AUTHENTICATOR_STATE_MODULE_NAME, AUTHENTICATOR_STATE_UPDATE_FUNCTION_NAME, }; + use sui_types::base_types::SequenceNumber; + use sui_types::bridge::BRIDGE_COMMITTEE_MINIMAL_VOTING_POWER; + use sui_types::bridge::{ + BridgeChainId, BRIDGE_CREATE_FUNCTION_NAME, BRIDGE_INIT_COMMITTEE_FUNCTION_NAME, + BRIDGE_MODULE_NAME, + }; use sui_types::clock::{CLOCK_MODULE_NAME, CONSENSUS_COMMIT_PROLOGUE_FUNCTION_NAME}; use sui_types::committee::EpochId; use sui_types::deny_list::{DENY_LIST_CREATE_FUNC, DENY_LIST_MODULE}; + use sui_types::digests::{ + get_mainnet_chain_identifier, get_testnet_chain_identifier, ChainIdentifier, + }; use sui_types::effects::TransactionEffects; use sui_types::error::{ExecutionError, ExecutionErrorKind}; use sui_types::execution::is_certificate_denied; @@ -44,6 +54,7 @@ mod checked { use sui_types::execution_status::{CongestedObjects, ExecutionStatus}; use sui_types::gas::GasCostSummary; use sui_types::gas::SuiGasStatus; + use sui_types::id::UID; use sui_types::inner_temporary_store::InnerTemporaryStore; use sui_types::storage::BackingStore; #[cfg(msim)] @@ -639,6 +650,14 @@ mod checked { assert!(protocol_config.enable_coin_deny_list()); builder = setup_coin_deny_list_state_create(builder); } + EndOfEpochTransactionKind::BridgeStateCreate(chain_id) => { + assert!(protocol_config.enable_bridge()); + builder = setup_bridge_create(builder, chain_id) + } + EndOfEpochTransactionKind::BridgeCommitteeInit(bridge_shared_version) => { + assert!(protocol_config.enable_bridge()); + builder = setup_bridge_committee_update(builder, bridge_shared_version) + } } } unreachable!("EndOfEpochTransactionKind::ChangeEpoch should be the last transaction in the list") @@ -999,6 +1018,75 @@ mod checked { builder } + fn setup_bridge_create( + mut builder: ProgrammableTransactionBuilder, + chain_id: ChainIdentifier, + ) -> ProgrammableTransactionBuilder { + let bridge_uid = builder + .input(CallArg::Pure(UID::new(SUI_BRIDGE_OBJECT_ID).to_bcs_bytes())) + .expect("Unable to create Bridge object UID!"); + + let bridge_chain_id = if chain_id == get_mainnet_chain_identifier() { + BridgeChainId::SuiMainnet as u8 + } else if chain_id == get_testnet_chain_identifier() { + BridgeChainId::SuiTestnet as u8 + } else { + // How do we distinguish devnet from other test envs? + BridgeChainId::SuiCustom as u8 + }; + + let bridge_chain_id = builder.pure(bridge_chain_id).unwrap(); + builder.programmable_move_call( + BRIDGE_ADDRESS.into(), + BRIDGE_MODULE_NAME.to_owned(), + BRIDGE_CREATE_FUNCTION_NAME.to_owned(), + vec![], + vec![bridge_uid, bridge_chain_id], + ); + builder + } + + fn setup_bridge_committee_update( + mut builder: ProgrammableTransactionBuilder, + bridge_shared_version: SequenceNumber, + ) -> ProgrammableTransactionBuilder { + let bridge = builder + .obj(ObjectArg::SharedObject { + id: SUI_BRIDGE_OBJECT_ID, + initial_shared_version: bridge_shared_version, + mutable: true, + }) + .expect("Unable to create Bridge object arg!"); + let system_state = builder + .obj(ObjectArg::SUI_SYSTEM_MUT) + .expect("Unable to create System State object arg!"); + + let voting_power = builder.programmable_move_call( + SUI_SYSTEM_PACKAGE_ID, + SUI_SYSTEM_MODULE_NAME.to_owned(), + ident_str!("validator_voting_powers").to_owned(), + vec![], + vec![system_state], + ); + + // Hardcoding min stake participation to 75.00% + // TODO: We need to set a correct value or make this configurable. + let min_stake_participation_percentage = builder + .input(CallArg::Pure( + bcs::to_bytes(&BRIDGE_COMMITTEE_MINIMAL_VOTING_POWER).unwrap(), + )) + .unwrap(); + + builder.programmable_move_call( + BRIDGE_ADDRESS.into(), + BRIDGE_MODULE_NAME.to_owned(), + BRIDGE_INIT_COMMITTEE_FUNCTION_NAME.to_owned(), + vec![], + vec![bridge, voting_power, min_stake_participation_percentage], + ); + builder + } + fn setup_authenticator_state_update( update: AuthenticatorStateUpdate, temporary_store: &mut TemporaryStore<'_>, diff --git a/sui-execution/latest/sui-move-natives/src/object_runtime/mod.rs b/sui-execution/latest/sui-move-natives/src/object_runtime/mod.rs index 31c6e5cfb9d5b..9f8e7d86ce078 100644 --- a/sui-execution/latest/sui-move-natives/src/object_runtime/mod.rs +++ b/sui-execution/latest/sui-move-natives/src/object_runtime/mod.rs @@ -37,8 +37,8 @@ use sui_types::{ metrics::LimitsMetrics, object::{MoveObject, Owner}, storage::ChildObjectResolver, - SUI_AUTHENTICATOR_STATE_OBJECT_ID, SUI_CLOCK_OBJECT_ID, SUI_DENY_LIST_OBJECT_ID, - SUI_RANDOMNESS_STATE_OBJECT_ID, SUI_SYSTEM_STATE_OBJECT_ID, + SUI_AUTHENTICATOR_STATE_OBJECT_ID, SUI_BRIDGE_OBJECT_ID, SUI_CLOCK_OBJECT_ID, + SUI_DENY_LIST_OBJECT_ID, SUI_RANDOMNESS_STATE_OBJECT_ID, SUI_SYSTEM_STATE_OBJECT_ID, }; pub enum ObjectEvent { @@ -256,6 +256,7 @@ impl<'a> ObjectRuntime<'a> { SUI_AUTHENTICATOR_STATE_OBJECT_ID, SUI_RANDOMNESS_STATE_OBJECT_ID, SUI_DENY_LIST_OBJECT_ID, + SUI_BRIDGE_OBJECT_ID, ] .contains(&id); let transfer_result = if self.state.new_ids.contains(&id) { diff --git a/sui-execution/v1/sui-adapter/src/execution_engine.rs b/sui-execution/v1/sui-adapter/src/execution_engine.rs index e10c0e442c933..e468aa9c31066 100644 --- a/sui-execution/v1/sui-adapter/src/execution_engine.rs +++ b/sui-execution/v1/sui-adapter/src/execution_engine.rs @@ -618,6 +618,14 @@ mod checked { EndOfEpochTransactionKind::DenyListStateCreate => { panic!("EndOfEpochTransactionKind::CoinDenyListStateCreate should not exist in v1"); } + EndOfEpochTransactionKind::BridgeStateCreate(_) => { + panic!( + "EndOfEpochTransactionKind::BridgeStateCreate should not exist in v1" + ); + } + EndOfEpochTransactionKind::BridgeCommitteeInit(_) => { + panic!("EndOfEpochTransactionKind::BridgeCommitteeInit should not exist in v1"); + } } } unreachable!("EndOfEpochTransactionKind::ChangeEpoch should be the last transaction in the list") diff --git a/sui-execution/v1/sui-verifier/src/id_leak_verifier.rs b/sui-execution/v1/sui-verifier/src/id_leak_verifier.rs index fd6385d587166..a9a24539d3541 100644 --- a/sui-execution/v1/sui-verifier/src/id_leak_verifier.rs +++ b/sui-execution/v1/sui-verifier/src/id_leak_verifier.rs @@ -28,13 +28,14 @@ use move_core_types::{ account_address::AccountAddress, ident_str, identifier::IdentStr, vm_status::StatusCode, }; use std::{collections::BTreeMap, error::Error, num::NonZeroU64}; +use sui_types::bridge::BRIDGE_MODULE_NAME; use sui_types::{ authenticator_state::AUTHENTICATOR_STATE_MODULE_NAME, clock::CLOCK_MODULE_NAME, error::{ExecutionError, VMMVerifierErrorSubStatusCode}, id::OBJECT_MODULE_NAME, sui_system_state::SUI_SYSTEM_MODULE_NAME, - SUI_FRAMEWORK_ADDRESS, SUI_SYSTEM_ADDRESS, + BRIDGE_ADDRESS, SUI_FRAMEWORK_ADDRESS, SUI_SYSTEM_ADDRESS, }; use crate::{ @@ -82,11 +83,14 @@ const SUI_AUTHENTICATOR_STATE_CREATE: FunctionIdent = ( AUTHENTICATOR_STATE_MODULE_NAME, ident_str!("create"), ); +const SUI_BRIDGE_CREATE: FunctionIdent = + (&BRIDGE_ADDRESS, BRIDGE_MODULE_NAME, ident_str!("create")); const FRESH_ID_FUNCTIONS: &[FunctionIdent] = &[OBJECT_NEW, OBJECT_NEW_UID_FROM_HASH, TS_NEW_OBJECT]; const FUNCTIONS_TO_SKIP: &[FunctionIdent] = &[ SUI_SYSTEM_CREATE, SUI_CLOCK_CREATE, SUI_AUTHENTICATOR_STATE_CREATE, + SUI_BRIDGE_CREATE, ]; impl AbstractValue { diff --git a/sui-execution/v1/sui-verifier/src/one_time_witness_verifier.rs b/sui-execution/v1/sui-verifier/src/one_time_witness_verifier.rs index 6c0bcf9de4c35..7285cd44192a2 100644 --- a/sui-execution/v1/sui-verifier/src/one_time_witness_verifier.rs +++ b/sui-execution/v1/sui-verifier/src/one_time_witness_verifier.rs @@ -20,11 +20,12 @@ use move_binary_format::file_format::{ SignatureToken, StructDefinition, StructHandle, }; use move_core_types::{ident_str, language_storage::ModuleId}; +use sui_types::bridge::BRIDGE_SUPPORTED_ASSET; use sui_types::{ base_types::{TX_CONTEXT_MODULE_NAME, TX_CONTEXT_STRUCT_NAME}, error::ExecutionError, move_package::{is_test_fun, FnInfoMap}, - SUI_FRAMEWORK_ADDRESS, + BRIDGE_ADDRESS, SUI_FRAMEWORK_ADDRESS, }; use crate::{verification_failure, INIT_FN_NAME}; @@ -41,7 +42,16 @@ pub fn verify_module( // the module has no initializer). The reason for it is that the SUI coin is only instantiated // during genesis. It is easiest to simply special-case this module particularly that this is // framework code and thus deemed correct. - if ModuleId::new(SUI_FRAMEWORK_ADDRESS, ident_str!("sui").to_owned()) == module.self_id() { + let self_id = module.self_id(); + + if ModuleId::new(SUI_FRAMEWORK_ADDRESS, ident_str!("sui").to_owned()) == self_id { + return Ok(()); + } + + if BRIDGE_SUPPORTED_ASSET + .iter() + .any(|token| ModuleId::new(BRIDGE_ADDRESS, ident_str!(token).to_owned()) == self_id) + { return Ok(()); } diff --git a/sui-execution/v2/sui-adapter/src/execution_engine.rs b/sui-execution/v2/sui-adapter/src/execution_engine.rs index 46612432f1c53..07e2a2f4971a0 100644 --- a/sui-execution/v2/sui-adapter/src/execution_engine.rs +++ b/sui-execution/v2/sui-adapter/src/execution_engine.rs @@ -639,6 +639,14 @@ mod checked { assert!(protocol_config.enable_coin_deny_list()); builder = setup_coin_deny_list_state_create(builder); } + EndOfEpochTransactionKind::BridgeStateCreate(_) => { + panic!( + "EndOfEpochTransactionKind::BridgeStateCreate should not exist in v2" + ); + } + EndOfEpochTransactionKind::BridgeCommitteeInit(_) => { + panic!("EndOfEpochTransactionKind::BridgeCommitteeInit should not exist in v2"); + } } } unreachable!("EndOfEpochTransactionKind::ChangeEpoch should be the last transaction in the list")