From 37ec72882bad56184a257c12f9b5f4877c17a0cf Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Sat, 31 Aug 2024 15:24:08 +0000 Subject: [PATCH 01/10] feat(exo-gateway): add oracleInfo ExocoreNetwork/exocore#159 adds a new `oracleInfo` parameter to the `IAssets.sol` precompile when registering tokens. This PR follows that change. Previously, multiple tokens could be registered with a single call to ExocoreGateway. However, that resulted in too many variables (and too much gas) for Solidity to handle within a single function. To that end, and keeping in mind that addition of new tokens isn't likely to be a frequent occurrence, the function's capabilities have been tempered to support only a single token within the transaction. As a side effect, this fixes #80 --- script/3_Setup.s.sol | 40 ++- src/core/Bootstrap.sol | 8 +- src/core/ClientChainGateway.sol | 4 +- src/core/ClientGatewayLzReceiver.sol | 40 +-- src/core/ExocoreGateway.sol | 103 +++--- src/interfaces/IExocoreGateway.sol | 30 +- src/interfaces/precompiles/IAssets.sol | 4 +- src/libraries/Errors.sol | 7 +- src/storage/ExocoreGatewayStorage.sol | 8 +- src/storage/GatewayStorage.sol | 2 +- test/foundry/Delegation.t.sol | 4 +- test/foundry/DepositThenDelegateTo.t.sol | 4 +- test/foundry/DepositWithdrawPrinciple.t.sol | 8 +- test/foundry/ExocoreDeployer.t.sol | 118 ++++--- test/foundry/WithdrawReward.t.sol | 2 +- test/foundry/unit/ExocoreGateway.t.sol | 311 +++++++------------ test/mocks/AssetsMock.sol | 3 +- test/mocks/ExocoreGatewayMock.sol | 98 +++--- test/mocks/NonShortCircuitEndpointV2Mock.sol | 4 +- 19 files changed, 356 insertions(+), 442 deletions(-) diff --git a/script/3_Setup.s.sol b/script/3_Setup.s.sol index 966a35b4..234937e9 100644 --- a/script/3_Setup.s.sol +++ b/script/3_Setup.s.sol @@ -1,9 +1,11 @@ pragma solidity ^0.8.19; -import "../src/interfaces/IClientChainGateway.sol"; +import {GatewayStorage} from "../src/storage/GatewayStorage.sol"; +import "../src/interfaces/IClientChainGateway.sol"; import "../src/interfaces/IExocoreGateway.sol"; import "../src/interfaces/IVault.sol"; + import {NonShortCircuitEndpointV2Mock} from "../test/mocks/NonShortCircuitEndpointV2Mock.sol"; import {BaseScript} from "./BaseScript.sol"; @@ -86,41 +88,53 @@ contract SetupScript is BaseScript { exocoreGateway.registerOrUpdateClientChain( clientChainId, address(clientGateway).toBytes32(), 20, "ClientChain", "EVM compatible network", "secp256k1" ); - vm.stopBroadcast(); // 3. adding tokens to the whtielist of both Exocore and client chain gateway to enable restaking // first we read decimals from client chain ERC20 token contract to prepare for token data - vm.selectFork(clientChain); bytes32[] memory whitelistTokensBytes32 = new bytes32[](2); uint8[] memory decimals = new uint8[](2); uint256[] memory tvlLimits = new uint256[](2); string[] memory names = new string[](2); - string[] memory metaData = new string[](2); + string[] memory metaDatas = new string[](2); + string[] memory oracleInfos = new string[](2); // this stands for LST restaking for restakeToken whitelistTokensBytes32[0] = bytes32(bytes20(address(restakeToken))); decimals[0] = restakeToken.decimals(); tvlLimits[0] = 1e10 ether; names[0] = "RestakeToken"; - metaData[0] = "ERC20 LST token"; + metaDatas[0] = "ERC20 LST token"; + oracleInfos[0] = "{'a': 'b'}"; // this stands for Native Restaking for ETH whitelistTokensBytes32[1] = bytes32(bytes20(VIRTUAL_STAKED_ETH_ADDRESS)); decimals[1] = 18; tvlLimits[1] = 1e8 ether; names[1] = "StakedETH"; - metaData[1] = "natively staked ETH on Ethereum"; - vm.stopBroadcast(); + metaDatas[1] = "natively staked ETH on Ethereum"; + oracleInfos[1] = "{'b': 'a'}"; // second add whitelist tokens and their meta data on Exocore side to enable LST Restaking and Native Restaking, // and this would also add token addresses to client chain gateway's whitelist - vm.selectFork(exocore); - uint256 messageLength = TOKEN_ADDRESS_BYTES_LENGTH * whitelistTokensBytes32.length + 2; - uint256 nativeFee = exocoreGateway.quote(clientChainId, new bytes(messageLength)); - exocoreGateway.addOrUpdateWhitelistTokens{value: nativeFee}( - clientChainId, whitelistTokensBytes32, decimals, tvlLimits, names, metaData - ); + uint256 nativeFee; + for (uint256 i = 0; i < whitelistTokensBytes32.length; i++) { + nativeFee = exocoreGateway.quote( + clientChainId, + abi.encodePacked( + GatewayStorage.Action.REQUEST_ADD_WHITELIST_TOKEN, abi.encodePacked(whitelistTokensBytes32[i]) + ) + ); + exocoreGateway.addOrUpdateWhitelistToken{value: nativeFee}( + clientChainId, + whitelistTokensBytes32[i], + decimals[i], + tvlLimits[i], + names[i], + metaDatas[i], + oracleInfos[i] + ); + } vm.stopBroadcast(); } diff --git a/src/core/Bootstrap.sol b/src/core/Bootstrap.sol index 3afce2ad..8d1a1164 100644 --- a/src/core/Bootstrap.sol +++ b/src/core/Bootstrap.sol @@ -195,10 +195,10 @@ contract Bootstrap is whitelistTokens.push(token); isWhitelistedToken[token] = true; - // deploy the corresponding vault if not deployed before - if (address(tokenToVault[token]) == address(0)) { - _deployVault(token); - } + // tokens cannot be removed from the whitelist. hence, if the token is not in the + // whitelist, it means that it is missing a vault. we do not need to check for a + // pre-existing vault. + _deployVault(token); emit WhitelistTokenAdded(token); } diff --git a/src/core/ClientChainGateway.sol b/src/core/ClientChainGateway.sol index c652bded..8144df1a 100644 --- a/src/core/ClientChainGateway.sol +++ b/src/core/ClientChainGateway.sol @@ -69,8 +69,8 @@ contract ClientChainGateway is revert Errors.ZeroAddress(); } - _whiteListFunctionSelectors[Action.REQUEST_ADD_WHITELIST_TOKENS] = - this.afterReceiveAddWhitelistTokensRequest.selector; + _whiteListFunctionSelectors[Action.REQUEST_ADD_WHITELIST_TOKEN] = + this.afterReceiveAddWhitelistTokenRequest.selector; bootstrapped = true; diff --git a/src/core/ClientGatewayLzReceiver.sol b/src/core/ClientGatewayLzReceiver.sol index 8fa361ed..3d7af67b 100644 --- a/src/core/ClientGatewayLzReceiver.sol +++ b/src/core/ClientGatewayLzReceiver.sol @@ -290,40 +290,34 @@ abstract contract ClientGatewayLzReceiver is PausableUpgradeable, OAppReceiverUp } } - /// @notice Called after an add-whitelist-tokens response is received. + /// @notice Called after an add-whitelist-token response is received. /// @param requestPayload The request payload. // Though `_deployVault` would make external call to newly created `Vault` contract and initialize it, // `Vault` contract belongs to Exocore and we could make sure its implementation does not have dangerous behavior // like reentrancy. // slither-disable-next-line reentrancy-no-eth - function afterReceiveAddWhitelistTokensRequest(bytes calldata requestPayload) + function afterReceiveAddWhitelistTokenRequest(bytes calldata requestPayload) public onlyCalledFromThis whenNotPaused { - uint8 count = uint8(requestPayload[0]); - uint256 expectedLength = count * TOKEN_ADDRESS_BYTES_LENGTH + 1; - if (requestPayload.length != expectedLength) { - revert InvalidAddWhitelistTokensRequest(expectedLength, requestPayload.length); + address token = address(bytes20(abi.decode(requestPayload, (bytes32)))); + if (token == address(0)) { + revert Errors.ZeroAddress(); } - - for (uint256 i = 0; i < count; ++i) { - uint256 start = i * TOKEN_ADDRESS_BYTES_LENGTH + 1; - uint256 end = start + TOKEN_ADDRESS_BYTES_LENGTH; - address token = address(bytes20(requestPayload[start:end])); - - if (!isWhitelistedToken[token]) { - isWhitelistedToken[token] = true; - whitelistTokens.push(token); - - // deploy the corresponding vault if not deployed before - if (token != VIRTUAL_STAKED_ETH_ADDRESS && address(tokenToVault[token]) == address(0)) { - _deployVault(token); - } - - emit WhitelistTokenAdded(token); - } + if (isWhitelistedToken[token]) { + // grave error, should never happen + revert Errors.ClientChainGatewayAlreadyWhitelisted(token); + } + isWhitelistedToken[token] = true; + whitelistTokens.push(token); + // deploy the corresponding vault if not deployed before + if (token != VIRTUAL_STAKED_ETH_ADDRESS) { + // since it is no longer possible to remove a token from the whitelist, the check for existing vault + // is not necessary. + _deployVault(token); } + emit WhitelistTokenAdded(token); } } diff --git a/src/core/ExocoreGateway.sol b/src/core/ExocoreGateway.sol index 5a0ea9f3..3cf5f9b4 100644 --- a/src/core/ExocoreGateway.sol +++ b/src/core/ExocoreGateway.sol @@ -177,51 +177,52 @@ contract ExocoreGateway is /// @notice If we want to activate client chain's native restaking, we should add the corresponding virtual /// token address to the whitelist, bytes32(bytes20(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE)) for Ethereum /// native restaking for example. - function addOrUpdateWhitelistTokens( + function addOrUpdateWhitelistToken( uint32 clientChainId, - bytes32[] calldata tokens, - uint8[] calldata decimals, - uint256[] calldata tvlLimits, - string[] calldata names, - string[] calldata metaData + bytes32 token, + uint8 decimals, + uint256 tvlLimit, + string calldata name, + string calldata metaData, + string calldata oracleInfo ) external payable onlyOwner whenNotPaused nonReentrant { - // The registration of the client chain is left for the precompile to validate. - _validateWhitelistTokensInput(tokens, decimals, tvlLimits, names, metaData); - - bool success; - bool updated; - for (uint256 i = 0; i < tokens.length; ++i) { - require(tokens[i] != bytes32(0), "ExocoreGateway: token cannot be zero address"); - require(tvlLimits[i] > 0, "ExocoreGateway: tvl limit should not be zero"); - require(bytes(names[i]).length != 0, "ExocoreGateway: name cannot be empty"); - require(bytes(metaData[i]).length != 0, "ExocoreGateway: meta data cannot be empty"); - - (success, updated) = ASSETS_CONTRACT.registerOrUpdateTokens( - clientChainId, abi.encodePacked(tokens[i]), decimals[i], tvlLimits[i], names[i], metaData[i] - ); + require(token != bytes32(0), "ExocoreGateway: token cannot be zero address"); + require(tvlLimit > 0, "ExocoreGateway: tvl limit should not be zero"); + require(bytes(name).length != 0, "ExocoreGateway: name cannot be empty"); + require(bytes(metaData).length != 0, "ExocoreGateway: meta data cannot be empty"); + require(bytes(oracleInfo).length != 0, "ExocoreGateway: oracleInfo cannot be empty"); + + (bool success, bool updated) = ASSETS_CONTRACT.registerOrUpdateTokens( + clientChainId, + abi.encodePacked(token), // convert to bytes from bytes32 + decimals, + tvlLimit, + name, + metaData, + oracleInfo + ); - if (success) { - if (!updated) { - emit WhitelistTokenAdded(clientChainId, tokens[i]); - } else { - emit WhitelistTokenUpdated(clientChainId, tokens[i]); + if (success) { + if (!updated) { + if (msg.value == 0) { + revert Errors.ZeroValue(); } + emit WhitelistTokenAdded(clientChainId, token); + _sendInterchainMsg( + clientChainId, + Action.REQUEST_ADD_WHITELIST_TOKEN, + abi.encodePacked(token), // convert for decoding it on the receiving end + false + ); } else { - if (!updated) { - revert AddWhitelistTokenFailed(tokens[i]); - } else { - revert UpdateWhitelistTokenFailed(tokens[i]); + if (msg.value != 0) { + revert Errors.NonZeroValue(); } + emit WhitelistTokenUpdated(clientChainId, token); } - } - - if (!updated) { - _sendInterchainMsg( - clientChainId, - Action.REQUEST_ADD_WHITELIST_TOKENS, - abi.encodePacked(uint8(tokens.length), tokens), - false - ); + } else { + // if the precompile call didn't succeed, we don't know if it is an update. + revert AddOrUpdateWhitelistTokenFailed(token); } } @@ -258,32 +259,6 @@ contract ExocoreGateway is } } - /// @dev Validates the input for whitelist tokens. - /// @param tokens The list of token addresses, length must be <= 255. - /// @param decimals The list of token decimals, length must be equal to that of @param tokens. - /// @param tvlLimits The list of token TVL limits, length must be equal to that of @param tokens. - /// @param names The list of token names, length must be equal to that of @param tokens. - /// @param metaData The list of token meta data, length must be equal to that of @param tokens. - function _validateWhitelistTokensInput( - bytes32[] calldata tokens, - uint8[] calldata decimals, - uint256[] calldata tvlLimits, - string[] calldata names, - string[] calldata metaData - ) internal pure { - uint256 expectedLength = tokens.length; - if (expectedLength > type(uint8).max) { - revert WhitelistTokensListTooLong(); - } - - if ( - decimals.length != expectedLength || tvlLimits.length != expectedLength || names.length != expectedLength - || metaData.length != expectedLength - ) { - revert InvalidWhitelistTokensInput(); - } - } - /// @dev Validates that the client chain id is registered. /// @dev This is designed to be called only in the cases wherein the precompile isn't used. /// @dev In all other situations, it is the responsibility of the precompile to perform such diff --git a/src/interfaces/IExocoreGateway.sol b/src/interfaces/IExocoreGateway.sol index c43e2dbe..661db622 100644 --- a/src/interfaces/IExocoreGateway.sol +++ b/src/interfaces/IExocoreGateway.sol @@ -41,21 +41,27 @@ interface IExocoreGateway is IOAppReceiver, IOAppCore { string calldata signatureType ) external; - /// @notice Adds a list of whitelisted tokens to the client chain. + /// @notice Add a single whitelisted token to the client chain. /// @param clientChainId The LayerZero chain id of the client chain. - /// @param tokens The list of token addresses to be whitelisted. - /// @param decimals The list of token decimals, in the same order as the tokens list. - /// @param tvlLimits The list of token TVL limits (typically max supply),in the same order as the tokens list. - /// @param names The names of the tokens, in the same order as the tokens list. - /// @param metaData The meta information of the tokens, in the same order as the tokens list. + /// @param token The token address to be whitelisted. + /// @param decimals The decimals of the token. + /// @param tvlLimit The TVL limit of the token. + /// @param name The name of the token. + /// @param metaData The meta information of the token. + /// @param oracleInfo The oracle information of the token. /// @dev The chain must be registered before adding tokens. - function addOrUpdateWhitelistTokens( + /// @dev This function is payable because it sends a message to the client chain if + /// the token is not already registered. + /// @dev Previously, we tried to use this function for multiple tokens, but that + /// results in too many local variables (stack too deep). + function addOrUpdateWhitelistToken( uint32 clientChainId, - bytes32[] calldata tokens, - uint8[] calldata decimals, - uint256[] calldata tvlLimits, - string[] calldata names, - string[] calldata metaData + bytes32 token, + uint8 decimals, + uint256 tvlLimit, + string calldata name, + string calldata metaData, + string calldata oracleInfo ) external payable; } diff --git a/src/interfaces/precompiles/IAssets.sol b/src/interfaces/precompiles/IAssets.sol index 65580341..2d8fa035 100644 --- a/src/interfaces/precompiles/IAssets.sol +++ b/src/interfaces/precompiles/IAssets.sol @@ -65,6 +65,7 @@ interface IAssets { /// maxSupply if there is no limit /// @param name is the name of the token /// @param metaData is the arbitrary metadata of the token + /// @param oracleInfo is the oracle information of the token /// @return success if the token registration is successful /// @return updated whether the token was added or updated function registerOrUpdateTokens( @@ -73,7 +74,8 @@ interface IAssets { uint8 decimals, uint256 tvlLimit, string calldata name, - string calldata metaData + string calldata metaData, + string calldata oracleInfo ) external returns (bool success, bool updated); /// QUERIES diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol index c6ad9afb..9e78b6bf 100644 --- a/src/libraries/Errors.sol +++ b/src/libraries/Errors.sol @@ -23,11 +23,10 @@ library Errors { /// @dev Thrown when passed-in amount is zero error ZeroAmount(); - /// @dev Thrown when the passed-in value is not zero + /// @dev Thrown when the passed-in msg.value is not zero but should be error NonZeroValue(); - /// @dev Thrown wehn the passed-in value is zero - /// @dev This is used when the value in question is not an amount + /// @dev Thrown when the passed-in msg.value is zero but should not be error ZeroValue(); /// @dev Index out of array bounds @@ -124,7 +123,7 @@ library Errors { error ClientChainGatewayAddWhitelistTooManyTokens(); /// @dev ClientChainGateway: token should not be whitelisted before - error ClientChainGatewayAlreadyWhitelisted(); + error ClientChainGatewayAlreadyWhitelisted(address token); ////////////////////////////////////// // ClientGatewayLzReceiver Errors // diff --git a/src/storage/ExocoreGatewayStorage.sol b/src/storage/ExocoreGatewayStorage.sol index 04560ffc..03e5c958 100644 --- a/src/storage/ExocoreGatewayStorage.sol +++ b/src/storage/ExocoreGatewayStorage.sol @@ -156,13 +156,9 @@ contract ExocoreGatewayStorage is GatewayStorage { /// @param clientChainId The LayerZero chain ID of the client chain. error RegisterClientChainToExocoreFailed(uint32 clientChainId); - /// @notice Thrown when a whitelist token addition fails + /// @notice Thrown when a whitelist token addition or update fails /// @param token The address of the token. - error AddWhitelistTokenFailed(bytes32 token); - - /// @notice Thrown when a whitelist token update fails - /// @param token The address of the token. - error UpdateWhitelistTokenFailed(bytes32 token); + error AddOrUpdateWhitelistTokenFailed(bytes32 token); /// @notice Thrown when the whitelist tokens input is invalid. error InvalidWhitelistTokensInput(); diff --git a/src/storage/GatewayStorage.sol b/src/storage/GatewayStorage.sol index b46e9cb7..d117acf4 100644 --- a/src/storage/GatewayStorage.sol +++ b/src/storage/GatewayStorage.sol @@ -15,7 +15,7 @@ contract GatewayStorage { REQUEST_UNDELEGATE_FROM, REQUEST_DEPOSIT_THEN_DELEGATE_TO, REQUEST_MARK_BOOTSTRAP, - REQUEST_ADD_WHITELIST_TOKENS, + REQUEST_ADD_WHITELIST_TOKEN, REQUEST_ASSOCIATE_OPERATOR, REQUEST_DISSOCIATE_OPERATOR, RESPOND diff --git a/test/foundry/Delegation.t.sol b/test/foundry/Delegation.t.sol index f1078e1d..ee1f92c3 100644 --- a/test/foundry/Delegation.t.sol +++ b/test/foundry/Delegation.t.sol @@ -120,7 +120,7 @@ contract DelegateTest is ExocoreDeployer { // 2. second layerzero relayers should watch the request message packet and relay the message to destination // endpoint - uint64 delegateResponseNonce = 2; + uint64 delegateResponseNonce = 3; bytes memory delegateResponsePayload = abi.encodePacked(GatewayStorage.Action.RESPOND, delegateRequestNonce, true); uint256 responseNativeFee = exocoreGateway.quote(clientChainId, delegateResponsePayload); @@ -243,7 +243,7 @@ contract DelegateTest is ExocoreDeployer { // 2. second layerzero relayers should watch the request message packet and relay the message to destination // endpoint - uint64 undelegateResponseNonce = 3; + uint64 undelegateResponseNonce = 4; bytes memory undelegateResponsePayload = abi.encodePacked(GatewayStorage.Action.RESPOND, undelegateRequestNonce, true); uint256 responseNativeFee = exocoreGateway.quote(clientChainId, undelegateResponsePayload); diff --git a/test/foundry/DepositThenDelegateTo.t.sol b/test/foundry/DepositThenDelegateTo.t.sol index a30494d4..e4d25e6f 100644 --- a/test/foundry/DepositThenDelegateTo.t.sol +++ b/test/foundry/DepositThenDelegateTo.t.sol @@ -46,7 +46,7 @@ contract DepositThenDelegateToTest is ExocoreDeployer { deal(address(exocoreGateway), 1e22); uint64 requestLzNonce = 1; - uint64 responseLzNonce = 2; + uint64 responseLzNonce = 3; // 2 tokens are whitelisted, 3 is response uint256 delegateAmount = 10_000; // before all operations we should add whitelist tokens @@ -85,7 +85,7 @@ contract DepositThenDelegateToTest is ExocoreDeployer { deal(address(exocoreGateway), 1e22); uint64 requestLzNonce = 1; - uint64 responseLzNonce = 2; + uint64 responseLzNonce = 3; uint256 delegateAmount = 10_000; // before all operations we should add whitelist tokens diff --git a/test/foundry/DepositWithdrawPrinciple.t.sol b/test/foundry/DepositWithdrawPrinciple.t.sol index dea6ad03..24bc39f7 100644 --- a/test/foundry/DepositWithdrawPrinciple.t.sol +++ b/test/foundry/DepositWithdrawPrinciple.t.sol @@ -111,7 +111,7 @@ contract DepositWithdrawPrincipalTest is ExocoreDeployer { // exocore gateway should return response message to exocore network layerzero endpoint vm.expectEmit(true, true, true, true, address(exocoreLzEndpoint)); lastlyUpdatedPrincipalBalance += depositAmount; - uint64 depositResponseNonce = 2; + uint64 depositResponseNonce = 3; bytes memory depositResponsePayload = abi.encodePacked(GatewayStorage.Action.RESPOND, depositRequestNonce, true, lastlyUpdatedPrincipalBalance); uint256 depositResponseNativeFee = exocoreGateway.quote(clientChainId, depositResponsePayload); @@ -196,7 +196,7 @@ contract DepositWithdrawPrincipalTest is ExocoreDeployer { // second layerzero relayers should watch the request message packet and relay the message to destination // endpoint - uint64 withdrawResponseNonce = 3; + uint64 withdrawResponseNonce = 4; lastlyUpdatedPrincipalBalance -= withdrawAmount; bytes memory withdrawResponsePayload = abi.encodePacked(GatewayStorage.Action.RESPOND, withdrawRequestNonce, true, lastlyUpdatedPrincipalBalance); @@ -344,7 +344,7 @@ contract DepositWithdrawPrincipalTest is ExocoreDeployer { // endpoint /// exocore gateway should return response message to exocore network layerzero endpoint - uint64 depositResponseNonce = 2; + uint64 depositResponseNonce = 3; lastlyUpdatedPrincipalBalance += depositAmount; bytes memory depositResponsePayload = abi.encodePacked(GatewayStorage.Action.RESPOND, depositRequestNonce, true, lastlyUpdatedPrincipalBalance); @@ -512,7 +512,7 @@ contract DepositWithdrawPrincipalTest is ExocoreDeployer { vm.stopPrank(); /// exocore gateway should return response message to exocore network layerzero endpoint - uint64 withdrawResponseNonce = 3; + uint64 withdrawResponseNonce = 4; lastlyUpdatedPrincipalBalance -= withdrawalAmount; bytes memory withdrawResponsePayload = abi.encodePacked(GatewayStorage.Action.RESPOND, withdrawRequestNonce, true, lastlyUpdatedPrincipalBalance); diff --git a/test/foundry/ExocoreDeployer.t.sol b/test/foundry/ExocoreDeployer.t.sol index 1b8bcb68..d49e7693 100644 --- a/test/foundry/ExocoreDeployer.t.sol +++ b/test/foundry/ExocoreDeployer.t.sol @@ -136,56 +136,87 @@ contract ExocoreDeployer is Test { uint8[] memory decimals = new uint8[](2); uint256[] memory tvlLimits = new uint256[](2); string[] memory names = new string[](2); - string[] memory metaData = new string[](2); + string[] memory metaDatas = new string[](2); + string[] memory oracleInfos = new string[](2); + bytes[] memory payloads = new bytes[](2); + bytes32[] memory requestIds = new bytes32[](2); whitelistTokens.push(bytes32(bytes20(address(restakeToken)))); decimals[0] = 18; tvlLimits[0] = 1e8 ether; names[0] = "RestakeToken"; - metaData[0] = "ERC20 LST token"; + metaDatas[0] = "ERC20 LST token"; + oracleInfos[0] = "{'a': 'b'}"; whitelistTokens.push(bytes32(bytes20(VIRTUAL_STAKED_ETH_ADDRESS))); decimals[1] = 18; tvlLimits[1] = 1e8 ether; names[1] = "NativeStakedETH"; - metaData[1] = "natively staked ETH on Ethereum"; + metaDatas[1] = "natively staked ETH on Ethereum"; + oracleInfos[1] = "{'b': 'a'}"; // -- add whitelist tokens workflow test -- vm.startPrank(exocoreValidatorSet.addr); + uint256 nativeFee; + for (uint256 i = 0; i < whitelistTokens.length; i++) { + payloads[i] = abi.encodePacked( + GatewayStorage.Action.REQUEST_ADD_WHITELIST_TOKEN, abi.encodePacked(whitelistTokens[i]) + ); + nativeFee = exocoreGateway.quote(clientChainId, payloads[i]); + requestIds[i] = generateUID(uint64(i + 1), false); + vm.expectEmit(address(exocoreLzEndpoint)); + emit NewPacket( + clientChainId, + address(exocoreGateway), + address(clientGateway).toBytes32(), + uint64(i) + 1, // nonce + payloads[i] + ); + vm.expectEmit(address(exocoreGateway)); + emit MessageSent( + GatewayStorage.Action.REQUEST_ADD_WHITELIST_TOKEN, + requestIds[i], + uint64(i) + 1, // nonce + nativeFee + ); + exocoreGateway.addOrUpdateWhitelistToken{value: nativeFee}( + clientChainId, whitelistTokens[i], decimals[i], tvlLimits[i], names[i], metaDatas[i], oracleInfos[i] + ); + } - // first user call exocore gateway to add whitelist tokens - - // estimate l0 relay fee that the user should pay - bytes memory registerTokensRequestPayload = abi.encodePacked( - GatewayStorage.Action.REQUEST_ADD_WHITELIST_TOKENS, - uint8(whitelistTokens.length), - bytes32(bytes20(address(restakeToken))), - bytes32(bytes20(VIRTUAL_STAKED_ETH_ADDRESS)) - ); - uint256 registerTokensRequestNativeFee = clientGateway.quote(registerTokensRequestPayload); - bytes32 registerTokensRequestId = generateUID(1, false); - - // exocore layerzero endpoint should emit the message packet including whitelist tokens payload. - vm.expectEmit(true, true, true, true, address(exocoreLzEndpoint)); - emit NewPacket( - clientChainId, - address(exocoreGateway), - address(clientGateway).toBytes32(), - uint64(1), - registerTokensRequestPayload - ); - // exocore gateway gateway should emit MessageSent event - vm.expectEmit(true, true, true, true, address(exocoreGateway)); - emit MessageSent( - GatewayStorage.Action.REQUEST_ADD_WHITELIST_TOKENS, - registerTokensRequestId, - uint64(1), - registerTokensRequestNativeFee - ); - exocoreGateway.addOrUpdateWhitelistTokens{value: registerTokensRequestNativeFee}( - clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData - ); + // // first user call exocore gateway to add whitelist tokens + + // // estimate l0 relay fee that the user should pay + // bytes memory registerTokensRequestPayload = abi.encodePacked( + // GatewayStorage.Action.REQUEST_ADD_WHITELIST_TOKEN, + // uint8(whitelistTokens.length), + // bytes32(bytes20(address(restakeToken))), + // bytes32(bytes20(VIRTUAL_STAKED_ETH_ADDRESS)) + // ); + // uint256 registerTokensRequestNativeFee = clientGateway.quote(registerTokensRequestPayload); + // bytes32 registerTokensRequestId = generateUID(1, false); + + // // exocore layerzero endpoint should emit the message packet including whitelist tokens payload. + // vm.expectEmit(true, true, true, true, address(exocoreLzEndpoint)); + // emit NewPacket( + // clientChainId, + // address(exocoreGateway), + // address(clientGateway).toBytes32(), + // uint64(1), + // registerTokensRequestPayload + // ); + // // exocore gateway gateway should emit MessageSent event + // vm.expectEmit(true, true, true, true, address(exocoreGateway)); + // emit MessageSent( + // GatewayStorage.Action.REQUEST_ADD_WHITELIST_TOKENS, + // registerTokensRequestId, + // uint64(1), + // registerTokensRequestNativeFee + // ); + // exocoreGateway.addOrUpdateWhitelistTokens{value: registerTokensRequestNativeFee}( + // clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData + // ); // second layerzero relayers should watch the request message packet and relay the message to destination // endpoint @@ -195,15 +226,24 @@ contract ExocoreDeployer is Test { keccak256(abi.encodePacked(BEACON_PROXY_BYTECODE, abi.encode(address(vaultBeacon), ""))), address(clientGateway) ); - vm.expectEmit(true, true, true, true, address(clientGateway)); + vm.expectEmit(address(clientGateway)); emit VaultCreated(address(restakeToken), expectedVault); emit WhitelistTokenAdded(address(restakeToken)); - emit WhitelistTokenAdded(VIRTUAL_STAKED_ETH_ADDRESS); clientChainLzEndpoint.lzReceive( Origin(exocoreChainId, address(exocoreGateway).toBytes32(), uint64(1)), address(clientGateway), - registerTokensRequestId, - registerTokensRequestPayload, + requestIds[0], + payloads[0], + bytes("") + ); + + vm.expectEmit(address(clientGateway)); + emit WhitelistTokenAdded(VIRTUAL_STAKED_ETH_ADDRESS); + clientChainLzEndpoint.lzReceive( + Origin(exocoreChainId, address(exocoreGateway).toBytes32(), uint64(2)), + address(clientGateway), + requestIds[1], + payloads[1], bytes("") ); diff --git a/test/foundry/WithdrawReward.t.sol b/test/foundry/WithdrawReward.t.sol index 43446489..6336111b 100644 --- a/test/foundry/WithdrawReward.t.sol +++ b/test/foundry/WithdrawReward.t.sol @@ -71,7 +71,7 @@ contract WithdrawRewardTest is ExocoreDeployer { // endpoint // exocore gateway should return response message to exocore network layerzero endpoint - uint64 withdrawResponseNonce = 2; + uint64 withdrawResponseNonce = 3; bytes memory withdrawResponsePayload = abi.encodePacked(GatewayStorage.Action.RESPOND, withdrawRequestNonce, true, uint256(1234)); uint256 responseNativeFee = exocoreGateway.quote(clientChainId, withdrawResponsePayload); diff --git a/test/foundry/unit/ExocoreGateway.t.sol b/test/foundry/unit/ExocoreGateway.t.sol index c6c9c24c..e6ac7aff 100644 --- a/test/foundry/unit/ExocoreGateway.t.sol +++ b/test/foundry/unit/ExocoreGateway.t.sol @@ -514,130 +514,64 @@ contract AddWhitelistTokens is SetUp { using stdStorage for StdStorage; using AddressCast for address; - uint256 internal constant TOKEN_ADDRESS_BYTES_LENGTH = 32; + uint256 MESSAGE_LENGTH = 1 + 32; // action + token address as bytes32 + uint256 nativeFee; event WhitelistTokenAdded(uint32 clientChainId, bytes32 token); - bytes32[] whitelistTokens; - uint8[] decimals; - uint256[] tvlLimits; - string[] names; - string[] metaData; + function setUp() public virtual override { + super.setUp(); + nativeFee = exocoreGateway.quote(clientChainId, new bytes(MESSAGE_LENGTH)); + } function test_RevertWhen_CallerNotOwner() public { - _prepareInputs(2); - - uint256 messageLength = TOKEN_ADDRESS_BYTES_LENGTH * whitelistTokens.length + 2; - uint256 nativeFee = exocoreGateway.quote(clientChainId, new bytes(messageLength)); - vm.startPrank(deployer.addr); vm.expectRevert("Ownable: caller is not the owner"); - exocoreGateway.addOrUpdateWhitelistTokens{value: nativeFee}( - clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData + exocoreGateway.addOrUpdateWhitelistToken{value: nativeFee}( + clientChainId, bytes32(0), 18, type(uint256).max, "name", "metadata", "oracleInfo" ); } function test_RevertWhen_Paused() public { vm.startPrank(exocoreValidatorSet.addr); exocoreGateway.pause(); - - _prepareInputs(2); - - uint256 messageLength = TOKEN_ADDRESS_BYTES_LENGTH * whitelistTokens.length + 2; - uint256 nativeFee = exocoreGateway.quote(clientChainId, new bytes(messageLength)); vm.expectRevert("Pausable: paused"); - exocoreGateway.addOrUpdateWhitelistTokens{value: nativeFee}( - clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData - ); - } - - function test_RevertWhen_TokensListTooLong() public { - _prepareInputs(256); - - uint256 messageLength = TOKEN_ADDRESS_BYTES_LENGTH * whitelistTokens.length + 2; - uint256 nativeFee = exocoreGateway.quote(clientChainId, new bytes(messageLength)); - - vm.startPrank(exocoreValidatorSet.addr); - vm.expectRevert(abi.encodeWithSelector(ExocoreGatewayStorage.WhitelistTokensListTooLong.selector)); - exocoreGateway.addOrUpdateWhitelistTokens{value: nativeFee}( - clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData - ); - } - - function test_RevertWhen_LengthNotMatch() public { - _prepareInputs(2); - decimals.push(18); - - uint256 messageLength = TOKEN_ADDRESS_BYTES_LENGTH * whitelistTokens.length + 2; - uint256 nativeFee = exocoreGateway.quote(clientChainId, new bytes(messageLength)); - - vm.startPrank(exocoreValidatorSet.addr); - vm.expectRevert(abi.encodeWithSelector(ExocoreGatewayStorage.InvalidWhitelistTokensInput.selector)); - exocoreGateway.addOrUpdateWhitelistTokens{value: nativeFee}( - clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData + exocoreGateway.addOrUpdateWhitelistToken{value: nativeFee}( + clientChainId, bytes32(0), 18, type(uint256).max, "name", "metadata", "oracleInfo" ); } function test_RevertWhen_HasZeroAddressToken() public { - _prepareInputs(2); - whitelistTokens[0] = bytes32(bytes20(address(restakeToken))); - tvlLimits[0] = 1e8 ether; - tvlLimits[1] = 1e8 ether; - names[0] = "LST-1"; - names[1] = "LST-2"; - metaData[0] = "LST token"; - metaData[1] = "LST token"; - - uint256 messageLength = TOKEN_ADDRESS_BYTES_LENGTH * whitelistTokens.length + 2; - uint256 nativeFee = exocoreGateway.quote(clientChainId, new bytes(messageLength)); - vm.startPrank(exocoreValidatorSet.addr); vm.expectRevert("ExocoreGateway: token cannot be zero address"); - exocoreGateway.addOrUpdateWhitelistTokens{value: nativeFee}( - clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData + exocoreGateway.addOrUpdateWhitelistToken{value: nativeFee}( + clientChainId, bytes32(0), 18, type(uint256).max, "name", "metadata", "oracleInfo" ); } function test_RevertWhen_HasZeroTVMLimit() public { - _prepareInputs(1); - whitelistTokens[0] = bytes32(bytes20(address(restakeToken))); - - uint256 messageLength = TOKEN_ADDRESS_BYTES_LENGTH * whitelistTokens.length + 2; - uint256 nativeFee = exocoreGateway.quote(clientChainId, new bytes(messageLength)); - vm.startPrank(exocoreValidatorSet.addr); vm.expectRevert("ExocoreGateway: tvl limit should not be zero"); - exocoreGateway.addOrUpdateWhitelistTokens{value: nativeFee}( - clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData + exocoreGateway.addOrUpdateWhitelistToken{value: nativeFee}( + clientChainId, bytes32(bytes20(address(restakeToken))), 18, 0, "name", "metadata", "oracleInfo" ); } - function test_Success_AddWhiteListTokens() public { - _prepareInputs(1); - whitelistTokens[0] = bytes32(bytes20(address(restakeToken))); - decimals[0] = 18; - tvlLimits[0] = 1e8 ether; - names[0] = "RestakeToken"; - metaData[0] = "ERC20 LST token"; - - uint256 messageLength = TOKEN_ADDRESS_BYTES_LENGTH * whitelistTokens.length + 2; - uint256 nativeFee = exocoreGateway.quote(clientChainId, new bytes(messageLength)); - + function test_Success_AddWhiteListToken() public { vm.startPrank(exocoreValidatorSet.addr); - vm.expectEmit(true, true, true, true, address(exocoreGateway)); - emit WhitelistTokenAdded(clientChainId, whitelistTokens[0]); - emit MessageSent(GatewayStorage.Action.REQUEST_ADD_WHITELIST_TOKENS, generateUID(1, false), 1, nativeFee); - exocoreGateway.addOrUpdateWhitelistTokens{value: nativeFee}( - clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData + vm.expectEmit(address(exocoreGateway)); + emit WhitelistTokenAdded(clientChainId, bytes32(bytes20(address(restakeToken)))); + emit MessageSent(GatewayStorage.Action.REQUEST_ADD_WHITELIST_TOKEN, generateUID(1, false), 1, nativeFee); + exocoreGateway.addOrUpdateWhitelistToken{value: nativeFee}( + clientChainId, + bytes32(bytes20(address(restakeToken))), + 18, + 1e8 ether, + "RestakeToken", + "ERC20 LST token", + "oracleInfo" ); - } - - function _prepareInputs(uint256 listLength) internal { - whitelistTokens = new bytes32[](listLength); - decimals = new uint8[](listLength); - tvlLimits = new uint256[](listLength); - names = new string[](listLength); - metaData = new string[](listLength); + vm.stopPrank(); } function generateUID(uint64 nonce, bool fromClientChainToExocore) internal view returns (bytes32 uid) { @@ -654,136 +588,113 @@ contract AddWhitelistTokens is SetUp { } -contract UpdateWhitelistTokens is SetUp { +contract UpdateWhitelistTokens is AddWhitelistTokens { - using AddressCast for address; + struct TokenDetails { + bytes32 tokenAddress; + uint8 decimals; + uint256 tvlLimit; + string name; + string metaData; + string oracleInfo; + } - uint256 internal constant TOKEN_ADDRESS_BYTES_LENGTH = 32; + TokenDetails tokenDetails; event WhitelistTokenUpdated(uint32 clientChainId, bytes32 token); - bytes32[] whitelistTokens; - uint8[] decimals; - uint256[] tvlLimits; - string[] names; - string[] metaData; - - function test_RevertWhen_CallerNotOwner() public { - _prepareInputs(2); - + function setUp() public virtual override { + super.setUp(); + tokenDetails = TokenDetails({ + tokenAddress: bytes32(bytes20(address(restakeToken))), + decimals: 18, + tvlLimit: 1e8 ether, + name: "RestakeToken", + metaData: "ERC20 LST token", + oracleInfo: "oracleInfo" + }); + // since we are inheriting add whitelist token, the test + // test_Success_AddWhiteListToken() will run. + // if we add a token to whitelist here, that test will fail. + // hence, we call test_Success_AddWhiteListToken() in each + // test case. + } + + function test_RevertUpdateWhen_CallerNotOwner() public { + test_Success_AddWhiteListToken(); // focus only on updates vm.startPrank(deployer.addr); vm.expectRevert("Ownable: caller is not the owner"); - exocoreGateway.addOrUpdateWhitelistTokens(clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData); + exocoreGateway.addOrUpdateWhitelistToken{value: 0}( + clientChainId, + tokenDetails.tokenAddress, + tokenDetails.decimals, + tokenDetails.tvlLimit, + tokenDetails.name, + tokenDetails.metaData, + tokenDetails.oracleInfo + ); } - function test_RevertWhen_Paused() public { + function test_RevertUpdateWhen_Paused() public { + test_Success_AddWhiteListToken(); // focus only on updates vm.startPrank(exocoreValidatorSet.addr); exocoreGateway.pause(); - - _prepareInputs(2); - vm.expectRevert("Pausable: paused"); - exocoreGateway.addOrUpdateWhitelistTokens(clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData); - } - - function test_RevertWhen_TokensListTooLong() public { - _prepareInputs(256); - - vm.startPrank(exocoreValidatorSet.addr); - vm.expectRevert(abi.encodeWithSelector(ExocoreGatewayStorage.WhitelistTokensListTooLong.selector)); - exocoreGateway.addOrUpdateWhitelistTokens(clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData); - } - - function test_RevertWhen_LengthNotMatch() public { - _prepareInputs(2); - decimals.push(18); - - vm.startPrank(exocoreValidatorSet.addr); - vm.expectRevert(abi.encodeWithSelector(ExocoreGatewayStorage.InvalidWhitelistTokensInput.selector)); - exocoreGateway.addOrUpdateWhitelistTokens(clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData); - } - - function test_RevertWhen_HasZeroAddressToken() public { - _prepareInputs(1); - whitelistTokens[0] = bytes32(bytes20(address(restakeToken))); - decimals[0] = 18; - tvlLimits[0] = 1e8 ether; - names[0] = "RestakeToken"; - metaData[0] = "ERC20 LST token"; - _addWhitelistTokens(clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData); - - _prepareInputs(2); - whitelistTokens[0] = bytes32(bytes20(address(restakeToken))); - tvlLimits[0] = 1e8 ether; - tvlLimits[1] = 1e8 ether; - names[0] = "LST-1"; - names[1] = "LST-2"; - metaData[0] = "LST token"; - metaData[1] = "LST token"; - - vm.startPrank(exocoreValidatorSet.addr); - vm.expectRevert("ExocoreGateway: token cannot be zero address"); - exocoreGateway.addOrUpdateWhitelistTokens(clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData); + exocoreGateway.addOrUpdateWhitelistToken{value: 0}( + clientChainId, + tokenDetails.tokenAddress, + tokenDetails.decimals, + tokenDetails.tvlLimit, + tokenDetails.name, + tokenDetails.metaData, + tokenDetails.oracleInfo + ); } - function test_RevertWhen_HasZeroTVMLimit() public { - _prepareInputs(1); - whitelistTokens[0] = bytes32(bytes20(address(restakeToken))); - decimals[0] = 18; - tvlLimits[0] = 1e8 ether; - names[0] = "RestakeToken"; - metaData[0] = "ERC20 LST token"; - _addWhitelistTokens(clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData); - - tvlLimits[0] = 0; - + function test_RevertUpdateWhen_HasZeroTVMLimit() public { + test_Success_AddWhiteListToken(); // focus only on updates vm.startPrank(exocoreValidatorSet.addr); vm.expectRevert("ExocoreGateway: tvl limit should not be zero"); - exocoreGateway.addOrUpdateWhitelistTokens(clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData); + exocoreGateway.addOrUpdateWhitelistToken{value: 0}( + clientChainId, + tokenDetails.tokenAddress, + tokenDetails.decimals, + 0, + tokenDetails.name, + tokenDetails.metaData, + tokenDetails.oracleInfo + ); } - function test_Success_UpdateWhitelistTokens() public { - _prepareInputs(1); - whitelistTokens[0] = bytes32(bytes20(address(restakeToken))); - decimals[0] = 18; - tvlLimits[0] = 1e8 ether; - names[0] = "RestakeToken"; - metaData[0] = "ERC20 LST token"; - - // add token to whitelist first - _addWhitelistTokens(clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData); - - // then update token info - tvlLimits[0] = 1e10 ether; - vm.expectEmit(true, true, true, true, address(exocoreGateway)); - emit WhitelistTokenUpdated(clientChainId, whitelistTokens[0]); + function test_Success_UpdateWhitelistToken() public { + test_Success_AddWhiteListToken(); // focus only on updates vm.startPrank(exocoreValidatorSet.addr); - exocoreGateway.addOrUpdateWhitelistTokens(clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData); - } - - function _prepareInputs(uint256 listLength) internal { - whitelistTokens = new bytes32[](listLength); - decimals = new uint8[](listLength); - tvlLimits = new uint256[](listLength); - names = new string[](listLength); - metaData = new string[](listLength); + vm.expectEmit(address(exocoreGateway)); + emit WhitelistTokenUpdated(clientChainId, tokenDetails.tokenAddress); + exocoreGateway.addOrUpdateWhitelistToken{value: 0}( + clientChainId, + tokenDetails.tokenAddress, + tokenDetails.decimals, + tokenDetails.tvlLimit * 5, + tokenDetails.name, + tokenDetails.metaData, + tokenDetails.oracleInfo + ); } - function _addWhitelistTokens( - uint32 clientChainId_, - bytes32[] memory whitelistTokens_, - uint8[] memory decimals_, - uint256[] memory tvlLimits_, - string[] memory names_, - string[] memory metaData_ - ) internal { + function test_RevertUpdate_NonZeroValue() public { + test_Success_AddWhiteListToken(); // focus only on updates vm.startPrank(exocoreValidatorSet.addr); - uint256 messageLength = TOKEN_ADDRESS_BYTES_LENGTH * whitelistTokens.length + 2; - uint256 nativeFee = exocoreGateway.quote(clientChainId, new bytes(messageLength)); - exocoreGateway.addOrUpdateWhitelistTokens{value: nativeFee}( - clientChainId_, whitelistTokens_, decimals_, tvlLimits_, names_, metaData_ + vm.expectRevert(Errors.NonZeroValue.selector); + exocoreGateway.addOrUpdateWhitelistToken{value: nativeFee}( + clientChainId, + tokenDetails.tokenAddress, + tokenDetails.decimals, + tokenDetails.tvlLimit * 5, + tokenDetails.name, + tokenDetails.metaData, + tokenDetails.oracleInfo ); - vm.stopPrank(); } } diff --git a/test/mocks/AssetsMock.sol b/test/mocks/AssetsMock.sol index 29283505..2fdb9037 100644 --- a/test/mocks/AssetsMock.sol +++ b/test/mocks/AssetsMock.sol @@ -71,7 +71,8 @@ contract AssetsMock is IAssets { uint8 decimals, uint256 tvlLimit, string calldata name, - string calldata metaData + string calldata metaData, + string calldata oracleInfo ) external returns (bool success, bool updated) { require(isRegisteredChain[clientChainId], "the chain is not registered before"); diff --git a/test/mocks/ExocoreGatewayMock.sol b/test/mocks/ExocoreGatewayMock.sol index 45b795b7..13af69a3 100644 --- a/test/mocks/ExocoreGatewayMock.sol +++ b/test/mocks/ExocoreGatewayMock.sol @@ -175,73 +175,51 @@ contract ExocoreGatewayMock is super.setPeer(clientChainId, clientChainGateway); } - // Though this function would call precompiled contract, all precompiled contracts belong to Exocore - // and we could make sure its implementation does not have dangerous behavior like reentrancy. - // slither-disable-next-line reentrancy-no-eth - function addOrUpdateWhitelistTokens( + function addOrUpdateWhitelistToken( uint32 clientChainId, - bytes32[] calldata tokens, - uint8[] calldata decimals, - uint256[] calldata tvlLimits, - string[] calldata names, - string[] calldata metaData + bytes32 token, + uint8 decimals, + uint256 tvlLimit, + string calldata name, + string calldata metaData, + string calldata oracleInfo ) external payable onlyOwner whenNotPaused nonReentrant { - _validateWhitelistTokensInput(tokens, decimals, tvlLimits, names, metaData); - - bool success; - bool updated; - for (uint256 i; i < tokens.length; i++) { - require(tokens[i] != bytes32(0), "ExocoreGateway: token cannot be zero address"); - require(tvlLimits[i] > 0, "ExocoreGateway: tvl limit should not be zero"); - require(bytes(names[i]).length != 0, "ExocoreGateway: name cannot be empty"); - require(bytes(metaData[i]).length != 0, "ExocoreGateway: meta data cannot be empty"); - - (success, updated) = ASSETS_CONTRACT.registerOrUpdateTokens( - clientChainId, abi.encodePacked(tokens[i]), decimals[i], tvlLimits[i], names[i], metaData[i] - ); + require(token != bytes32(0), "ExocoreGateway: token cannot be zero address"); + require(tvlLimit > 0, "ExocoreGateway: tvl limit should not be zero"); + require(bytes(name).length != 0, "ExocoreGateway: name cannot be empty"); + require(bytes(metaData).length != 0, "ExocoreGateway: meta data cannot be empty"); + + (bool success, bool updated) = ASSETS_CONTRACT.registerOrUpdateTokens( + clientChainId, + abi.encodePacked(token), // convert to bytes from bytes32 + decimals, + tvlLimit, + name, + metaData, + oracleInfo + ); - if (success) { - if (!updated) { - emit WhitelistTokenAdded(clientChainId, tokens[i]); - } else { - emit WhitelistTokenUpdated(clientChainId, tokens[i]); + if (success) { + if (!updated) { + if (msg.value == 0) { + revert Errors.ZeroValue(); } + emit WhitelistTokenAdded(clientChainId, token); + _sendInterchainMsg( + clientChainId, + Action.REQUEST_ADD_WHITELIST_TOKEN, + abi.encodePacked(token), // convert for decoding it on the receiving end + false + ); } else { - if (!updated) { - revert AddWhitelistTokenFailed(tokens[i]); - } else { - revert UpdateWhitelistTokenFailed(tokens[i]); + if (msg.value != 0) { + revert Errors.NonZeroValue(); } + emit WhitelistTokenUpdated(clientChainId, token); } - } - - if (!updated) { - _sendInterchainMsg( - clientChainId, - Action.REQUEST_ADD_WHITELIST_TOKENS, - abi.encodePacked(uint8(tokens.length), tokens), - false - ); - } - } - - function _validateWhitelistTokensInput( - bytes32[] calldata tokens, - uint8[] calldata decimals, - uint256[] calldata tvlLimits, - string[] calldata names, - string[] calldata metaData - ) internal pure { - uint256 expectedLength = tokens.length; - if (expectedLength > type(uint8).max) { - revert WhitelistTokensListTooLong(); - } - - if ( - decimals.length != expectedLength || tvlLimits.length != expectedLength || names.length != expectedLength - || metaData.length != expectedLength - ) { - revert InvalidWhitelistTokensInput(); + } else { + // if the precompile call didn't succeed, we don't know if it is an update. + revert AddOrUpdateWhitelistTokenFailed(token); } } diff --git a/test/mocks/NonShortCircuitEndpointV2Mock.sol b/test/mocks/NonShortCircuitEndpointV2Mock.sol index b4c2b661..8744ec7f 100644 --- a/test/mocks/NonShortCircuitEndpointV2Mock.sol +++ b/test/mocks/NonShortCircuitEndpointV2Mock.sol @@ -718,9 +718,7 @@ contract NonShortCircuitEndpointV2Mock is ILayerZeroEndpointV2, MessagingContext } else if (optionType == ExecutorOptions.OPTION_TYPE_ORDERED_EXECUTION) { // ordered = true; } - else { - revert IExecutorFeeLib.Executor_UnsupportedOptionType(optionType); - } + else revert IExecutorFeeLib.Executor_UnsupportedOptionType(optionType); } if (cursor != _options.length) { From 69e2406939a19c3b566d0ae35cbb6ac292d1c001 Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Sat, 31 Aug 2024 15:46:10 +0000 Subject: [PATCH 02/10] chore(fmt): forge fmt with newer nightly --- test/mocks/NonShortCircuitEndpointV2Mock.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/mocks/NonShortCircuitEndpointV2Mock.sol b/test/mocks/NonShortCircuitEndpointV2Mock.sol index 8744ec7f..3da36f56 100644 --- a/test/mocks/NonShortCircuitEndpointV2Mock.sol +++ b/test/mocks/NonShortCircuitEndpointV2Mock.sol @@ -717,8 +717,9 @@ contract NonShortCircuitEndpointV2Mock is ILayerZeroEndpointV2, MessagingContext } } else if (optionType == ExecutorOptions.OPTION_TYPE_ORDERED_EXECUTION) { // ordered = true; + } else { + revert IExecutorFeeLib.Executor_UnsupportedOptionType(optionType); } - else revert IExecutorFeeLib.Executor_UnsupportedOptionType(optionType); } if (cursor != _options.length) { From dbd9d808c0207a368f6c859c3d50403a448c76c5 Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Sat, 31 Aug 2024 15:55:20 +0000 Subject: [PATCH 03/10] doc: update comments --- src/core/ClientGatewayLzReceiver.sol | 2 +- test/foundry/ExocoreDeployer.t.sol | 36 +++------------------------- 2 files changed, 4 insertions(+), 34 deletions(-) diff --git a/src/core/ClientGatewayLzReceiver.sol b/src/core/ClientGatewayLzReceiver.sol index 3d7af67b..170331ec 100644 --- a/src/core/ClientGatewayLzReceiver.sol +++ b/src/core/ClientGatewayLzReceiver.sol @@ -311,7 +311,7 @@ abstract contract ClientGatewayLzReceiver is PausableUpgradeable, OAppReceiverUp } isWhitelistedToken[token] = true; whitelistTokens.push(token); - // deploy the corresponding vault if not deployed before + // deploy the corresponding vault, if required if (token != VIRTUAL_STAKED_ETH_ADDRESS) { // since it is no longer possible to remove a token from the whitelist, the check for existing vault // is not necessary. diff --git a/test/foundry/ExocoreDeployer.t.sol b/test/foundry/ExocoreDeployer.t.sol index d49e7693..16839d0e 100644 --- a/test/foundry/ExocoreDeployer.t.sol +++ b/test/foundry/ExocoreDeployer.t.sol @@ -157,14 +157,17 @@ contract ExocoreDeployer is Test { // -- add whitelist tokens workflow test -- + // first user call exocore gateway to add whitelist tokens vm.startPrank(exocoreValidatorSet.addr); uint256 nativeFee; for (uint256 i = 0; i < whitelistTokens.length; i++) { + // estimate the fee from the payload payloads[i] = abi.encodePacked( GatewayStorage.Action.REQUEST_ADD_WHITELIST_TOKEN, abi.encodePacked(whitelistTokens[i]) ); nativeFee = exocoreGateway.quote(clientChainId, payloads[i]); requestIds[i] = generateUID(uint64(i + 1), false); + // gateway should emit the packet for the outgoing message vm.expectEmit(address(exocoreLzEndpoint)); emit NewPacket( clientChainId, @@ -185,39 +188,6 @@ contract ExocoreDeployer is Test { ); } - // // first user call exocore gateway to add whitelist tokens - - // // estimate l0 relay fee that the user should pay - // bytes memory registerTokensRequestPayload = abi.encodePacked( - // GatewayStorage.Action.REQUEST_ADD_WHITELIST_TOKEN, - // uint8(whitelistTokens.length), - // bytes32(bytes20(address(restakeToken))), - // bytes32(bytes20(VIRTUAL_STAKED_ETH_ADDRESS)) - // ); - // uint256 registerTokensRequestNativeFee = clientGateway.quote(registerTokensRequestPayload); - // bytes32 registerTokensRequestId = generateUID(1, false); - - // // exocore layerzero endpoint should emit the message packet including whitelist tokens payload. - // vm.expectEmit(true, true, true, true, address(exocoreLzEndpoint)); - // emit NewPacket( - // clientChainId, - // address(exocoreGateway), - // address(clientGateway).toBytes32(), - // uint64(1), - // registerTokensRequestPayload - // ); - // // exocore gateway gateway should emit MessageSent event - // vm.expectEmit(true, true, true, true, address(exocoreGateway)); - // emit MessageSent( - // GatewayStorage.Action.REQUEST_ADD_WHITELIST_TOKENS, - // registerTokensRequestId, - // uint64(1), - // registerTokensRequestNativeFee - // ); - // exocoreGateway.addOrUpdateWhitelistTokens{value: registerTokensRequestNativeFee}( - // clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData - // ); - // second layerzero relayers should watch the request message packet and relay the message to destination // endpoint From 01f1493f72a09db12f16ec520392a5373c4d9381 Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Mon, 2 Sep 2024 07:24:28 +0000 Subject: [PATCH 04/10] fix(test): update test from merge --- test/foundry/unit/ClientChainGateway.t.sol | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/test/foundry/unit/ClientChainGateway.t.sol b/test/foundry/unit/ClientChainGateway.t.sol index 2c9bd7e6..c0bb9505 100644 --- a/test/foundry/unit/ClientChainGateway.t.sol +++ b/test/foundry/unit/ClientChainGateway.t.sol @@ -373,14 +373,24 @@ contract WithdrawalPrincipalFromExocore is SetUp { tokens[1] = bytes32(bytes20(address(restakeToken))); // Simulate adding VIRTUAL_STAKED_ETH_ADDRESS to whitelist via lzReceive - bytes memory message = - abi.encodePacked(GatewayStorage.Action.REQUEST_ADD_WHITELIST_TOKENS, uint8(tokens.length), tokens); - Origin memory origin = Origin({srcEid: exocoreChainId, sender: address(exocoreGateway).toBytes32(), nonce: 1}); + bytes memory message = abi.encodePacked( + GatewayStorage.Action.REQUEST_ADD_WHITELIST_TOKEN, + abi.encodePacked(tokens[0]) + ); + Origin memory origin = Origin({ + srcEid: exocoreChainId, sender: address(exocoreGateway).toBytes32(), nonce: 1 + }); vm.prank(address(clientChainLzEndpoint)); clientGateway.lzReceive(origin, bytes32(0), message, address(0), bytes("")); // assert that VIRTUAL_STAKED_ETH_ADDRESS and restake token is whitelisted assertTrue(clientGateway.isWhitelistedToken(VIRTUAL_STAKED_ETH_ADDRESS)); + origin.nonce = 2; + message = abi.encodePacked( + GatewayStorage.Action.REQUEST_ADD_WHITELIST_TOKEN, abi.encodePacked(tokens[1]) + ); + vm.prank(address(clientChainLzEndpoint)); + clientGateway.lzReceive(origin, bytes32(0), message, address(0), bytes("")); assertTrue(clientGateway.isWhitelistedToken(address(restakeToken))); } From a74091cb76052d582d54d7a6e3bc1825dcfe450e Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Mon, 2 Sep 2024 07:25:20 +0000 Subject: [PATCH 05/10] forge fmt --- test/foundry/unit/ClientChainGateway.t.sol | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/test/foundry/unit/ClientChainGateway.t.sol b/test/foundry/unit/ClientChainGateway.t.sol index c0bb9505..fb98139d 100644 --- a/test/foundry/unit/ClientChainGateway.t.sol +++ b/test/foundry/unit/ClientChainGateway.t.sol @@ -373,22 +373,16 @@ contract WithdrawalPrincipalFromExocore is SetUp { tokens[1] = bytes32(bytes20(address(restakeToken))); // Simulate adding VIRTUAL_STAKED_ETH_ADDRESS to whitelist via lzReceive - bytes memory message = abi.encodePacked( - GatewayStorage.Action.REQUEST_ADD_WHITELIST_TOKEN, - abi.encodePacked(tokens[0]) - ); - Origin memory origin = Origin({ - srcEid: exocoreChainId, sender: address(exocoreGateway).toBytes32(), nonce: 1 - }); + bytes memory message = + abi.encodePacked(GatewayStorage.Action.REQUEST_ADD_WHITELIST_TOKEN, abi.encodePacked(tokens[0])); + Origin memory origin = Origin({srcEid: exocoreChainId, sender: address(exocoreGateway).toBytes32(), nonce: 1}); vm.prank(address(clientChainLzEndpoint)); clientGateway.lzReceive(origin, bytes32(0), message, address(0), bytes("")); // assert that VIRTUAL_STAKED_ETH_ADDRESS and restake token is whitelisted assertTrue(clientGateway.isWhitelistedToken(VIRTUAL_STAKED_ETH_ADDRESS)); origin.nonce = 2; - message = abi.encodePacked( - GatewayStorage.Action.REQUEST_ADD_WHITELIST_TOKEN, abi.encodePacked(tokens[1]) - ); + message = abi.encodePacked(GatewayStorage.Action.REQUEST_ADD_WHITELIST_TOKEN, abi.encodePacked(tokens[1])); vm.prank(address(clientChainLzEndpoint)); clientGateway.lzReceive(origin, bytes32(0), message, address(0), bytes("")); assertTrue(clientGateway.isWhitelistedToken(address(restakeToken))); From 6cb5197465b75b1b372d546998c1756e6ee071c4 Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Mon, 2 Sep 2024 08:15:55 +0000 Subject: [PATCH 06/10] feat(assets): split precompile into add/update Since only certain parameters can be updated (TVL limit and metadata), it does not make sense to use the same function for token additions and updates --- script/3_Setup.s.sol | 2 +- src/core/ExocoreGateway.sol | 53 ++++++---- src/interfaces/IExocoreGateway.sol | 15 ++- src/interfaces/precompiles/IAssets.sol | 23 +++-- src/storage/ExocoreGatewayStorage.sol | 10 +- test/foundry/ExocoreDeployer.t.sol | 2 +- test/foundry/unit/ExocoreGateway.t.sol | 138 ++++++++++++------------- test/mocks/AssetsMock.sol | 24 +++-- test/mocks/ExocoreGatewayMock.sol | 53 ++++++---- 9 files changed, 187 insertions(+), 133 deletions(-) diff --git a/script/3_Setup.s.sol b/script/3_Setup.s.sol index 234937e9..0cf0afe8 100644 --- a/script/3_Setup.s.sol +++ b/script/3_Setup.s.sol @@ -125,7 +125,7 @@ contract SetupScript is BaseScript { GatewayStorage.Action.REQUEST_ADD_WHITELIST_TOKEN, abi.encodePacked(whitelistTokensBytes32[i]) ) ); - exocoreGateway.addOrUpdateWhitelistToken{value: nativeFee}( + exocoreGateway.addWhitelistToken{value: nativeFee}( clientChainId, whitelistTokensBytes32[i], decimals[i], diff --git a/src/core/ExocoreGateway.sol b/src/core/ExocoreGateway.sol index 3cf5f9b4..83ea3c68 100644 --- a/src/core/ExocoreGateway.sol +++ b/src/core/ExocoreGateway.sol @@ -177,7 +177,7 @@ contract ExocoreGateway is /// @notice If we want to activate client chain's native restaking, we should add the corresponding virtual /// token address to the whitelist, bytes32(bytes20(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE)) for Ethereum /// native restaking for example. - function addOrUpdateWhitelistToken( + function addWhitelistToken( uint32 clientChainId, bytes32 token, uint8 decimals, @@ -186,13 +186,17 @@ contract ExocoreGateway is string calldata metaData, string calldata oracleInfo ) external payable onlyOwner whenNotPaused nonReentrant { + if (msg.value == 0) { + revert Errors.ZeroValue(); + } + require(clientChainId != 0, "ExocoreGateway: client chain id cannot be zero"); require(token != bytes32(0), "ExocoreGateway: token cannot be zero address"); require(tvlLimit > 0, "ExocoreGateway: tvl limit should not be zero"); require(bytes(name).length != 0, "ExocoreGateway: name cannot be empty"); require(bytes(metaData).length != 0, "ExocoreGateway: meta data cannot be empty"); require(bytes(oracleInfo).length != 0, "ExocoreGateway: oracleInfo cannot be empty"); - (bool success, bool updated) = ASSETS_CONTRACT.registerOrUpdateTokens( + bool success = ASSETS_CONTRACT.registerToken( clientChainId, abi.encodePacked(token), // convert to bytes from bytes32 decimals, @@ -201,28 +205,35 @@ contract ExocoreGateway is metaData, oracleInfo ); + if (success) { + emit WhitelistTokenAdded(clientChainId, token); + _sendInterchainMsg( + clientChainId, + Action.REQUEST_ADD_WHITELIST_TOKEN, + abi.encodePacked(token), // convert for decoding it on the receiving end + false + ); + } else { + revert AddWhitelistTokenFailed(clientChainId, token); + } + } + /// @inheritdoc IExocoreGateway + function updateWhitelistToken(uint32 clientChainId, bytes32 token, uint256 tvlLimit, string calldata metaData) + external + onlyOwner + whenNotPaused + nonReentrant + { + require(clientChainId != 0, "ExocoreGateway: client chain id cannot be zero"); + require(token != bytes32(0), "ExocoreGateway: token cannot be zero address"); + // setting tvlLimit to 0 is allowed as a way to disable the token + require(bytes(metaData).length != 0, "ExocoreGateway: meta data cannot be empty"); + bool success = ASSETS_CONTRACT.updateToken(clientChainId, abi.encodePacked(token), tvlLimit, metaData); if (success) { - if (!updated) { - if (msg.value == 0) { - revert Errors.ZeroValue(); - } - emit WhitelistTokenAdded(clientChainId, token); - _sendInterchainMsg( - clientChainId, - Action.REQUEST_ADD_WHITELIST_TOKEN, - abi.encodePacked(token), // convert for decoding it on the receiving end - false - ); - } else { - if (msg.value != 0) { - revert Errors.NonZeroValue(); - } - emit WhitelistTokenUpdated(clientChainId, token); - } + emit WhitelistTokenUpdated(clientChainId, token); } else { - // if the precompile call didn't succeed, we don't know if it is an update. - revert AddOrUpdateWhitelistTokenFailed(token); + revert UpdateWhitelistTokenFailed(clientChainId, token); } } diff --git a/src/interfaces/IExocoreGateway.sol b/src/interfaces/IExocoreGateway.sol index 661db622..b50abb6b 100644 --- a/src/interfaces/IExocoreGateway.sol +++ b/src/interfaces/IExocoreGateway.sol @@ -50,11 +50,10 @@ interface IExocoreGateway is IOAppReceiver, IOAppCore { /// @param metaData The meta information of the token. /// @param oracleInfo The oracle information of the token. /// @dev The chain must be registered before adding tokens. - /// @dev This function is payable because it sends a message to the client chain if - /// the token is not already registered. + /// @dev This function is payable because it sends a message to the client chain. /// @dev Previously, we tried to use this function for multiple tokens, but that /// results in too many local variables (stack too deep). - function addOrUpdateWhitelistToken( + function addWhitelistToken( uint32 clientChainId, bytes32 token, uint8 decimals, @@ -64,4 +63,14 @@ interface IExocoreGateway is IOAppReceiver, IOAppCore { string calldata oracleInfo ) external payable; + /// @notice Updates the parameters for a whitelisted token on the client chain. + /// @param clientChainId The LayerZero chain id of the client chain. + /// @param token The token address to be updated. + /// @param tvlLimit The new TVL limit of the token. + /// @param metaData The new meta information of the token. + /// @dev The token must exist in the whitelist before updating. + /// @dev Since this function does not send a cross chain message, it is not payable. + function updateWhitelistToken(uint32 clientChainId, bytes32 token, uint256 tvlLimit, string calldata metaData) + external; + } diff --git a/src/interfaces/precompiles/IAssets.sol b/src/interfaces/precompiles/IAssets.sol index 2d8fa035..04ec53d8 100644 --- a/src/interfaces/precompiles/IAssets.sol +++ b/src/interfaces/precompiles/IAssets.sol @@ -55,10 +55,10 @@ interface IAssets { string calldata signatureType ) external returns (bool success, bool updated); - /// @dev register or update token addresses to exocore + /// @dev register a token to allow deposits / staking, etc. /// @dev note that there is no way to delete a token. If a token is to be removed, /// the TVL limit should be set to 0. - /// @param clientChainID is the identifier of the token's home chain (LZ or otherwise) + /// @param clientChainId is the identifier of the token's home chain (LZ or otherwise) /// @param token is the address of the token on the home chain /// @param decimals is the number of decimals of the token /// @param tvlLimit is the number of tokens that can be deposited in the system. Set to @@ -67,16 +67,27 @@ interface IAssets { /// @param metaData is the arbitrary metadata of the token /// @param oracleInfo is the oracle information of the token /// @return success if the token registration is successful - /// @return updated whether the token was added or updated - function registerOrUpdateTokens( - uint32 clientChainID, + function registerToken( + uint32 clientChainId, bytes calldata token, uint8 decimals, uint256 tvlLimit, string calldata name, string calldata metaData, string calldata oracleInfo - ) external returns (bool success, bool updated); + ) external returns (bool success); + + /// @dev update a token to allow deposits / staking, etc. + /// @param clientChainId is the identifier of the token's home chain (LZ or otherwise) + /// @param token is the address of the token on the home chain + /// @param tvlLimit is the number of tokens that can be deposited in the system. Set to + /// maxSupply if there is no limit + /// @param metaData is the arbitrary metadata of the token + /// @return success if the token update is successful + /// @dev The token must previously be registered before updating. + function updateToken(uint32 clientChainId, bytes calldata token, uint256 tvlLimit, string calldata metaData) + external + returns (bool success); /// QUERIES /// @dev Returns the chain indices of the client chains. diff --git a/src/storage/ExocoreGatewayStorage.sol b/src/storage/ExocoreGatewayStorage.sol index 03e5c958..1e34a2ce 100644 --- a/src/storage/ExocoreGatewayStorage.sol +++ b/src/storage/ExocoreGatewayStorage.sol @@ -156,9 +156,15 @@ contract ExocoreGatewayStorage is GatewayStorage { /// @param clientChainId The LayerZero chain ID of the client chain. error RegisterClientChainToExocoreFailed(uint32 clientChainId); - /// @notice Thrown when a whitelist token addition or update fails + /// @notice Thrown when a whitelist token addition fails + /// @param clientChainId The LayerZero chain ID (or otherwise) of the client chain. /// @param token The address of the token. - error AddOrUpdateWhitelistTokenFailed(bytes32 token); + error AddWhitelistTokenFailed(uint32 clientChainId, bytes32 token); + + /// @notice Thrown when a whitelist token update fails + /// @param clientChainId The LayerZero chain ID (or otherwise) of the client chain. + /// @param token The address of the token. + error UpdateWhitelistTokenFailed(uint32 clientChainId, bytes32 token); /// @notice Thrown when the whitelist tokens input is invalid. error InvalidWhitelistTokensInput(); diff --git a/test/foundry/ExocoreDeployer.t.sol b/test/foundry/ExocoreDeployer.t.sol index 16839d0e..7252a4d0 100644 --- a/test/foundry/ExocoreDeployer.t.sol +++ b/test/foundry/ExocoreDeployer.t.sol @@ -183,7 +183,7 @@ contract ExocoreDeployer is Test { uint64(i) + 1, // nonce nativeFee ); - exocoreGateway.addOrUpdateWhitelistToken{value: nativeFee}( + exocoreGateway.addWhitelistToken{value: nativeFee}( clientChainId, whitelistTokens[i], decimals[i], tvlLimits[i], names[i], metaDatas[i], oracleInfos[i] ); } diff --git a/test/foundry/unit/ExocoreGateway.t.sol b/test/foundry/unit/ExocoreGateway.t.sol index e6ac7aff..110a8cbb 100644 --- a/test/foundry/unit/ExocoreGateway.t.sol +++ b/test/foundry/unit/ExocoreGateway.t.sol @@ -113,6 +113,18 @@ contract SetUp is Test { deal(address(exocoreGateway), 1e22); } + function generateUID(uint64 nonce, bool fromClientChainToExocore) internal view returns (bytes32 uid) { + if (fromClientChainToExocore) { + uid = GUID.generate( + nonce, clientChainId, address(clientGateway), exocoreChainId, address(exocoreGateway).toBytes32() + ); + } else { + uid = GUID.generate( + nonce, exocoreChainId, address(exocoreGateway), clientChainId, address(clientGateway).toBytes32() + ); + } + } + } contract Pausable is SetUp { @@ -527,7 +539,7 @@ contract AddWhitelistTokens is SetUp { function test_RevertWhen_CallerNotOwner() public { vm.startPrank(deployer.addr); vm.expectRevert("Ownable: caller is not the owner"); - exocoreGateway.addOrUpdateWhitelistToken{value: nativeFee}( + exocoreGateway.addWhitelistToken{value: nativeFee}( clientChainId, bytes32(0), 18, type(uint256).max, "name", "metadata", "oracleInfo" ); } @@ -536,7 +548,15 @@ contract AddWhitelistTokens is SetUp { vm.startPrank(exocoreValidatorSet.addr); exocoreGateway.pause(); vm.expectRevert("Pausable: paused"); - exocoreGateway.addOrUpdateWhitelistToken{value: nativeFee}( + exocoreGateway.addWhitelistToken{value: nativeFee}( + clientChainId, bytes32(0), 18, type(uint256).max, "name", "metadata", "oracleInfo" + ); + } + + function test_RevertWhen_ZeroValue() public { + vm.startPrank(exocoreValidatorSet.addr); + vm.expectRevert(Errors.ZeroValue.selector); + exocoreGateway.addWhitelistToken{value: 0}( clientChainId, bytes32(0), 18, type(uint256).max, "name", "metadata", "oracleInfo" ); } @@ -544,7 +564,7 @@ contract AddWhitelistTokens is SetUp { function test_RevertWhen_HasZeroAddressToken() public { vm.startPrank(exocoreValidatorSet.addr); vm.expectRevert("ExocoreGateway: token cannot be zero address"); - exocoreGateway.addOrUpdateWhitelistToken{value: nativeFee}( + exocoreGateway.addWhitelistToken{value: nativeFee}( clientChainId, bytes32(0), 18, type(uint256).max, "name", "metadata", "oracleInfo" ); } @@ -552,7 +572,7 @@ contract AddWhitelistTokens is SetUp { function test_RevertWhen_HasZeroTVMLimit() public { vm.startPrank(exocoreValidatorSet.addr); vm.expectRevert("ExocoreGateway: tvl limit should not be zero"); - exocoreGateway.addOrUpdateWhitelistToken{value: nativeFee}( + exocoreGateway.addWhitelistToken{value: nativeFee}( clientChainId, bytes32(bytes20(address(restakeToken))), 18, 0, "name", "metadata", "oracleInfo" ); } @@ -562,7 +582,7 @@ contract AddWhitelistTokens is SetUp { vm.expectEmit(address(exocoreGateway)); emit WhitelistTokenAdded(clientChainId, bytes32(bytes20(address(restakeToken)))); emit MessageSent(GatewayStorage.Action.REQUEST_ADD_WHITELIST_TOKEN, generateUID(1, false), 1, nativeFee); - exocoreGateway.addOrUpdateWhitelistToken{value: nativeFee}( + exocoreGateway.addWhitelistToken{value: nativeFee}( clientChainId, bytes32(bytes20(address(restakeToken))), 18, @@ -574,21 +594,9 @@ contract AddWhitelistTokens is SetUp { vm.stopPrank(); } - function generateUID(uint64 nonce, bool fromClientChainToExocore) internal view returns (bytes32 uid) { - if (fromClientChainToExocore) { - uid = GUID.generate( - nonce, clientChainId, address(clientGateway), exocoreChainId, address(exocoreGateway).toBytes32() - ); - } else { - uid = GUID.generate( - nonce, exocoreChainId, address(exocoreGateway), clientChainId, address(clientGateway).toBytes32() - ); - } - } - } -contract UpdateWhitelistTokens is AddWhitelistTokens { +contract UpdateWhitelistTokens is SetUp { struct TokenDetails { bytes32 tokenAddress; @@ -601,10 +609,29 @@ contract UpdateWhitelistTokens is AddWhitelistTokens { TokenDetails tokenDetails; + event WhitelistTokenAdded(uint32 clientChainId, bytes32 token); event WhitelistTokenUpdated(uint32 clientChainId, bytes32 token); function setUp() public virtual override { super.setUp(); + // the below code is intentionally repeated here, instead of inheriting it from AddWhitelistTokens + // this is done to not conflate the tests of AddWhitelistTokens with UpdateWhitelistTokens + uint256 MESSAGE_LENGTH = 1 + 32; // action + token address as bytes32 + uint256 nativeFee = exocoreGateway.quote(clientChainId, new bytes(MESSAGE_LENGTH)); + vm.startPrank(exocoreValidatorSet.addr); + vm.expectEmit(address(exocoreGateway)); + emit WhitelistTokenAdded(clientChainId, bytes32(bytes20(address(restakeToken)))); + emit MessageSent(GatewayStorage.Action.REQUEST_ADD_WHITELIST_TOKEN, generateUID(1, false), 1, nativeFee); + exocoreGateway.addWhitelistToken{value: nativeFee}( + clientChainId, + bytes32(bytes20(address(restakeToken))), + 18, + 1e8 ether, + "RestakeToken", + "ERC20 LST token", + "oracleInfo" + ); + vm.stopPrank(); tokenDetails = TokenDetails({ tokenAddress: bytes32(bytes20(address(restakeToken))), decimals: 18, @@ -621,79 +648,46 @@ contract UpdateWhitelistTokens is AddWhitelistTokens { } function test_RevertUpdateWhen_CallerNotOwner() public { - test_Success_AddWhiteListToken(); // focus only on updates vm.startPrank(deployer.addr); vm.expectRevert("Ownable: caller is not the owner"); - exocoreGateway.addOrUpdateWhitelistToken{value: 0}( - clientChainId, - tokenDetails.tokenAddress, - tokenDetails.decimals, - tokenDetails.tvlLimit, - tokenDetails.name, - tokenDetails.metaData, - tokenDetails.oracleInfo + exocoreGateway.updateWhitelistToken( + clientChainId, tokenDetails.tokenAddress, tokenDetails.tvlLimit, tokenDetails.metaData ); } function test_RevertUpdateWhen_Paused() public { - test_Success_AddWhiteListToken(); // focus only on updates vm.startPrank(exocoreValidatorSet.addr); exocoreGateway.pause(); vm.expectRevert("Pausable: paused"); - exocoreGateway.addOrUpdateWhitelistToken{value: 0}( - clientChainId, - tokenDetails.tokenAddress, - tokenDetails.decimals, - tokenDetails.tvlLimit, - tokenDetails.name, - tokenDetails.metaData, - tokenDetails.oracleInfo + exocoreGateway.updateWhitelistToken( + clientChainId, tokenDetails.tokenAddress, tokenDetails.tvlLimit, tokenDetails.metaData ); } - function test_RevertUpdateWhen_HasZeroTVMLimit() public { - test_Success_AddWhiteListToken(); // focus only on updates + function test_RevertUpdateWhen_HasZeroAddress() public { vm.startPrank(exocoreValidatorSet.addr); - vm.expectRevert("ExocoreGateway: tvl limit should not be zero"); - exocoreGateway.addOrUpdateWhitelistToken{value: 0}( - clientChainId, - tokenDetails.tokenAddress, - tokenDetails.decimals, - 0, - tokenDetails.name, - tokenDetails.metaData, - tokenDetails.oracleInfo - ); + vm.expectRevert("ExocoreGateway: token cannot be zero address"); + exocoreGateway.updateWhitelistToken(clientChainId, bytes32(0), tokenDetails.tvlLimit, tokenDetails.metaData); } - function test_Success_UpdateWhitelistToken() public { - test_Success_AddWhiteListToken(); // focus only on updates + function test_RevertUpdateWhen_HasZeroChainId() public { vm.startPrank(exocoreValidatorSet.addr); - vm.expectEmit(address(exocoreGateway)); - emit WhitelistTokenUpdated(clientChainId, tokenDetails.tokenAddress); - exocoreGateway.addOrUpdateWhitelistToken{value: 0}( - clientChainId, - tokenDetails.tokenAddress, - tokenDetails.decimals, - tokenDetails.tvlLimit * 5, - tokenDetails.name, - tokenDetails.metaData, - tokenDetails.oracleInfo - ); + vm.expectRevert("ExocoreGateway: client chain id cannot be zero"); + exocoreGateway.updateWhitelistToken(0, tokenDetails.tokenAddress, tokenDetails.tvlLimit, tokenDetails.metaData); } - function test_RevertUpdate_NonZeroValue() public { - test_Success_AddWhiteListToken(); // focus only on updates + function test_RevertUpdateWhen_HasZeroMetadata() public { vm.startPrank(exocoreValidatorSet.addr); - vm.expectRevert(Errors.NonZeroValue.selector); - exocoreGateway.addOrUpdateWhitelistToken{value: nativeFee}( - clientChainId, - tokenDetails.tokenAddress, - tokenDetails.decimals, - tokenDetails.tvlLimit * 5, - tokenDetails.name, - tokenDetails.metaData, - tokenDetails.oracleInfo + vm.expectRevert("ExocoreGateway: meta data cannot be empty"); + exocoreGateway.updateWhitelistToken(clientChainId, tokenDetails.tokenAddress, tokenDetails.tvlLimit, ""); + } + + function test_Success_UpdateWhitelistToken() public { + vm.startPrank(exocoreValidatorSet.addr); + vm.expectEmit(address(exocoreGateway)); + emit WhitelistTokenUpdated(clientChainId, tokenDetails.tokenAddress); + exocoreGateway.updateWhitelistToken( + clientChainId, tokenDetails.tokenAddress, tokenDetails.tvlLimit * 5, "new metadata" ); } diff --git a/test/mocks/AssetsMock.sol b/test/mocks/AssetsMock.sol index 2fdb9037..0bd5592e 100644 --- a/test/mocks/AssetsMock.sol +++ b/test/mocks/AssetsMock.sol @@ -65,7 +65,7 @@ contract AssetsMock is IAssets { return (true, updated); } - function registerOrUpdateTokens( + function registerToken( uint32 clientChainId, bytes calldata token, uint8 decimals, @@ -73,16 +73,28 @@ contract AssetsMock is IAssets { string calldata name, string calldata metaData, string calldata oracleInfo - ) external returns (bool success, bool updated) { + ) external returns (bool success) { require(isRegisteredChain[clientChainId], "the chain is not registered before"); - updated = isRegisteredToken[clientChainId][token]; + if (isRegisteredToken[clientChainId][token]) { + return false; + } + isRegisteredToken[clientChainId][token] = true; + + return true; + } + + function updateToken(uint32 clientChainId, bytes calldata token, uint256 tvlLimit, string calldata metaData) + external + returns (bool success) + { + require(isRegisteredChain[clientChainId], "the chain is not registered before"); - if (!updated) { - isRegisteredToken[clientChainId][token] = true; + if (!isRegisteredToken[clientChainId][token]) { + return false; } - return (true, updated); + return true; } function getPrincipalBalance(uint32 clientChainLzId, bytes memory token, bytes memory staker) diff --git a/test/mocks/ExocoreGatewayMock.sol b/test/mocks/ExocoreGatewayMock.sol index 13af69a3..be4b71cc 100644 --- a/test/mocks/ExocoreGatewayMock.sol +++ b/test/mocks/ExocoreGatewayMock.sol @@ -175,7 +175,7 @@ contract ExocoreGatewayMock is super.setPeer(clientChainId, clientChainGateway); } - function addOrUpdateWhitelistToken( + function addWhitelistToken( uint32 clientChainId, bytes32 token, uint8 decimals, @@ -184,12 +184,17 @@ contract ExocoreGatewayMock is string calldata metaData, string calldata oracleInfo ) external payable onlyOwner whenNotPaused nonReentrant { + if (msg.value == 0) { + revert Errors.ZeroValue(); + } + require(clientChainId != 0, "ExocoreGateway: client chain id cannot be zero"); require(token != bytes32(0), "ExocoreGateway: token cannot be zero address"); require(tvlLimit > 0, "ExocoreGateway: tvl limit should not be zero"); require(bytes(name).length != 0, "ExocoreGateway: name cannot be empty"); require(bytes(metaData).length != 0, "ExocoreGateway: meta data cannot be empty"); + require(bytes(oracleInfo).length != 0, "ExocoreGateway: oracleInfo cannot be empty"); - (bool success, bool updated) = ASSETS_CONTRACT.registerOrUpdateTokens( + bool success = ASSETS_CONTRACT.registerToken( clientChainId, abi.encodePacked(token), // convert to bytes from bytes32 decimals, @@ -198,28 +203,34 @@ contract ExocoreGatewayMock is metaData, oracleInfo ); + if (success) { + emit WhitelistTokenAdded(clientChainId, token); + _sendInterchainMsg( + clientChainId, + Action.REQUEST_ADD_WHITELIST_TOKEN, + abi.encodePacked(token), // convert for decoding it on the receiving end + false + ); + } else { + revert AddWhitelistTokenFailed(clientChainId, token); + } + } + function updateWhitelistToken(uint32 clientChainId, bytes32 token, uint256 tvlLimit, string calldata metaData) + external + onlyOwner + whenNotPaused + nonReentrant + { + require(clientChainId != 0, "ExocoreGateway: client chain id cannot be zero"); + require(token != bytes32(0), "ExocoreGateway: token cannot be zero address"); + // setting tvlLimit to 0 is allowed as a way to disable the token + require(bytes(metaData).length != 0, "ExocoreGateway: meta data cannot be empty"); + bool success = ASSETS_CONTRACT.updateToken(clientChainId, abi.encodePacked(token), tvlLimit, metaData); if (success) { - if (!updated) { - if (msg.value == 0) { - revert Errors.ZeroValue(); - } - emit WhitelistTokenAdded(clientChainId, token); - _sendInterchainMsg( - clientChainId, - Action.REQUEST_ADD_WHITELIST_TOKEN, - abi.encodePacked(token), // convert for decoding it on the receiving end - false - ); - } else { - if (msg.value != 0) { - revert Errors.NonZeroValue(); - } - emit WhitelistTokenUpdated(clientChainId, token); - } + emit WhitelistTokenUpdated(clientChainId, token); } else { - // if the precompile call didn't succeed, we don't know if it is an update. - revert AddOrUpdateWhitelistTokenFailed(token); + revert UpdateWhitelistTokenFailed(clientChainId, token); } } From fe5393e04a89b5139a35b8c1ea3555ee63c10387 Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Mon, 2 Sep 2024 08:44:36 +0000 Subject: [PATCH 07/10] fix: accept empty metadata --- src/core/ExocoreGateway.sol | 2 +- src/interfaces/precompiles/IAssets.sol | 4 +++- test/foundry/unit/ExocoreGateway.t.sol | 6 ------ 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/core/ExocoreGateway.sol b/src/core/ExocoreGateway.sol index 83ea3c68..9e356e91 100644 --- a/src/core/ExocoreGateway.sol +++ b/src/core/ExocoreGateway.sol @@ -228,7 +228,7 @@ contract ExocoreGateway is require(clientChainId != 0, "ExocoreGateway: client chain id cannot be zero"); require(token != bytes32(0), "ExocoreGateway: token cannot be zero address"); // setting tvlLimit to 0 is allowed as a way to disable the token - require(bytes(metaData).length != 0, "ExocoreGateway: meta data cannot be empty"); + // empty metaData indicates that the token's metadata should not be updated bool success = ASSETS_CONTRACT.updateToken(clientChainId, abi.encodePacked(token), tvlLimit, metaData); if (success) { emit WhitelistTokenUpdated(clientChainId, token); diff --git a/src/interfaces/precompiles/IAssets.sol b/src/interfaces/precompiles/IAssets.sol index 04ec53d8..0bd886cc 100644 --- a/src/interfaces/precompiles/IAssets.sol +++ b/src/interfaces/precompiles/IAssets.sol @@ -84,7 +84,9 @@ interface IAssets { /// maxSupply if there is no limit /// @param metaData is the arbitrary metadata of the token /// @return success if the token update is successful - /// @dev The token must previously be registered before updating. + /// @dev The token must previously be registered before updating + /// @dev Pass a tvlLimit of 0 to disable any deposits of the token + /// @dev Pass en empty metadata to keep the existing metadata function updateToken(uint32 clientChainId, bytes calldata token, uint256 tvlLimit, string calldata metaData) external returns (bool success); diff --git a/test/foundry/unit/ExocoreGateway.t.sol b/test/foundry/unit/ExocoreGateway.t.sol index 110a8cbb..7d5824e8 100644 --- a/test/foundry/unit/ExocoreGateway.t.sol +++ b/test/foundry/unit/ExocoreGateway.t.sol @@ -676,12 +676,6 @@ contract UpdateWhitelistTokens is SetUp { exocoreGateway.updateWhitelistToken(0, tokenDetails.tokenAddress, tokenDetails.tvlLimit, tokenDetails.metaData); } - function test_RevertUpdateWhen_HasZeroMetadata() public { - vm.startPrank(exocoreValidatorSet.addr); - vm.expectRevert("ExocoreGateway: meta data cannot be empty"); - exocoreGateway.updateWhitelistToken(clientChainId, tokenDetails.tokenAddress, tokenDetails.tvlLimit, ""); - } - function test_Success_UpdateWhitelistToken() public { vm.startPrank(exocoreValidatorSet.addr); vm.expectEmit(address(exocoreGateway)); From 73e7b59ca78ac3c8105354dccf9e0a23152c70ed Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Mon, 2 Sep 2024 09:31:45 +0000 Subject: [PATCH 08/10] fix: remove superfluous check --- src/core/ExocoreGateway.sol | 3 --- test/foundry/unit/ExocoreGateway.t.sol | 5 +++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/core/ExocoreGateway.sol b/src/core/ExocoreGateway.sol index 9e356e91..6054ec59 100644 --- a/src/core/ExocoreGateway.sol +++ b/src/core/ExocoreGateway.sol @@ -186,9 +186,6 @@ contract ExocoreGateway is string calldata metaData, string calldata oracleInfo ) external payable onlyOwner whenNotPaused nonReentrant { - if (msg.value == 0) { - revert Errors.ZeroValue(); - } require(clientChainId != 0, "ExocoreGateway: client chain id cannot be zero"); require(token != bytes32(0), "ExocoreGateway: token cannot be zero address"); require(tvlLimit > 0, "ExocoreGateway: tvl limit should not be zero"); diff --git a/test/foundry/unit/ExocoreGateway.t.sol b/test/foundry/unit/ExocoreGateway.t.sol index 7d5824e8..62b6a899 100644 --- a/test/foundry/unit/ExocoreGateway.t.sol +++ b/test/foundry/unit/ExocoreGateway.t.sol @@ -529,6 +529,7 @@ contract AddWhitelistTokens is SetUp { uint256 MESSAGE_LENGTH = 1 + 32; // action + token address as bytes32 uint256 nativeFee; + error IncorrectNativeFee(uint256 amount); event WhitelistTokenAdded(uint32 clientChainId, bytes32 token); function setUp() public virtual override { @@ -555,9 +556,9 @@ contract AddWhitelistTokens is SetUp { function test_RevertWhen_ZeroValue() public { vm.startPrank(exocoreValidatorSet.addr); - vm.expectRevert(Errors.ZeroValue.selector); + vm.expectRevert(abi.encodeWithSelector(IncorrectNativeFee.selector, uint256(0))); exocoreGateway.addWhitelistToken{value: 0}( - clientChainId, bytes32(0), 18, type(uint256).max, "name", "metadata", "oracleInfo" + clientChainId, bytes32(bytes20(address(restakeToken))), 18, type(uint256).max, "name", "metadata", "oracleInfo" ); } From 2900e342170446e08b84a3d48596ec57ee122669 Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Mon, 2 Sep 2024 09:32:23 +0000 Subject: [PATCH 09/10] forge fmt --- test/foundry/unit/ExocoreGateway.t.sol | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/foundry/unit/ExocoreGateway.t.sol b/test/foundry/unit/ExocoreGateway.t.sol index 62b6a899..197772ec 100644 --- a/test/foundry/unit/ExocoreGateway.t.sol +++ b/test/foundry/unit/ExocoreGateway.t.sol @@ -530,6 +530,7 @@ contract AddWhitelistTokens is SetUp { uint256 nativeFee; error IncorrectNativeFee(uint256 amount); + event WhitelistTokenAdded(uint32 clientChainId, bytes32 token); function setUp() public virtual override { @@ -558,7 +559,13 @@ contract AddWhitelistTokens is SetUp { vm.startPrank(exocoreValidatorSet.addr); vm.expectRevert(abi.encodeWithSelector(IncorrectNativeFee.selector, uint256(0))); exocoreGateway.addWhitelistToken{value: 0}( - clientChainId, bytes32(bytes20(address(restakeToken))), 18, type(uint256).max, "name", "metadata", "oracleInfo" + clientChainId, + bytes32(bytes20(address(restakeToken))), + 18, + type(uint256).max, + "name", + "metadata", + "oracleInfo" ); } From bf9eaae1a5dfa7f91df35c46626b3f72daf834a8 Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Tue, 3 Sep 2024 08:48:45 +0000 Subject: [PATCH 10/10] doc: fix outdated comments --- test/foundry/unit/ExocoreGateway.t.sol | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/foundry/unit/ExocoreGateway.t.sol b/test/foundry/unit/ExocoreGateway.t.sol index 197772ec..aa767036 100644 --- a/test/foundry/unit/ExocoreGateway.t.sol +++ b/test/foundry/unit/ExocoreGateway.t.sol @@ -648,11 +648,6 @@ contract UpdateWhitelistTokens is SetUp { metaData: "ERC20 LST token", oracleInfo: "oracleInfo" }); - // since we are inheriting add whitelist token, the test - // test_Success_AddWhiteListToken() will run. - // if we add a token to whitelist here, that test will fail. - // hence, we call test_Success_AddWhiteListToken() in each - // test case. } function test_RevertUpdateWhen_CallerNotOwner() public {