diff --git a/script/3_Setup.s.sol b/script/3_Setup.s.sol index 966a35b4..0cf0afe8 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.addWhitelistToken{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 8c93795a..6be11459 100644 --- a/src/core/Bootstrap.sol +++ b/src/core/Bootstrap.sol @@ -198,9 +198,11 @@ contract Bootstrap is whitelistTokens.push(token); isWhitelistedToken[token] = true; - // do not deploy the vault for the virtual token address representing natively staked ETH - // deploy the corresponding vault if not deployed before - if (token != VIRTUAL_STAKED_ETH_ADDRESS && address(tokenToVault[token]) == address(0)) { + // 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. however, we still do ensure that the vault is not deployed + // for restaking natively staked ETH. + if (token != VIRTUAL_STAKED_ETH_ADDRESS) { _deployVault(token); } diff --git a/src/core/ClientChainGateway.sol b/src/core/ClientChainGateway.sol index 8ce2db1d..9e215960 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; // overwrite the bootstrap function selector _whiteListFunctionSelectors[Action.REQUEST_MARK_BOOTSTRAP] = this.afterReceiveMarkBootstrapRequest.selector; diff --git a/src/core/ClientGatewayLzReceiver.sol b/src/core/ClientGatewayLzReceiver.sol index 792ea1ef..d51bfd95 100644 --- a/src/core/ClientGatewayLzReceiver.sol +++ b/src/core/ClientGatewayLzReceiver.sol @@ -290,41 +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); - - // do not deploy the vault for the virtual token address representing natively staked ETH - // 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); + // since tokens cannot be removed from the whitelist, it is not possible for a vault + // to already exist. however, we should still ensure that a vault is not deployed for + // restaking native staked eth + if (token != VIRTUAL_STAKED_ETH_ADDRESS) { + _deployVault(token); } + emit WhitelistTokenAdded(token); } /// @notice Called after a mark-bootstrap response is received. diff --git a/src/core/ExocoreGateway.sol b/src/core/ExocoreGateway.sol index 9fbe64e5..6bbb64e0 100644 --- a/src/core/ExocoreGateway.sol +++ b/src/core/ExocoreGateway.sol @@ -168,51 +168,60 @@ 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 addWhitelistToken( 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] - ); - - if (success) { - if (!updated) { - emit WhitelistTokenAdded(clientChainId, tokens[i]); - } else { - emit WhitelistTokenUpdated(clientChainId, tokens[i]); - } - } else { - if (!updated) { - revert AddWhitelistTokenFailed(tokens[i]); - } else { - revert UpdateWhitelistTokenFailed(tokens[i]); - } - } - } - - if (!updated) { + 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 = ASSETS_CONTRACT.registerToken( + clientChainId, + abi.encodePacked(token), // convert to bytes from bytes32 + decimals, + tvlLimit, + name, + metaData, + oracleInfo + ); + if (success) { + emit WhitelistTokenAdded(clientChainId, token); _sendInterchainMsg( clientChainId, - Action.REQUEST_ADD_WHITELIST_TOKENS, - abi.encodePacked(uint8(tokens.length), tokens), + 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 + // 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); + } else { + revert UpdateWhitelistTokenFailed(clientChainId, token); } } @@ -249,32 +258,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 76bca735..711d6983 100644 --- a/src/interfaces/IExocoreGateway.sol +++ b/src/interfaces/IExocoreGateway.sol @@ -41,23 +41,38 @@ 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. + /// @dev Previously, we tried to use this function for multiple tokens, but that + /// results in too many local variables (stack too deep). + function addWhitelistToken( 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; + /// @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; + /// @notice Marks the network as bootstrapped, on the client chain. /// @dev Causes an upgrade of the Bootstrap contract to the ClientChainGateway contract. /// @dev Only works if LZ infrastructure is set up and SetPeer has been called. diff --git a/src/interfaces/precompiles/IAssets.sol b/src/interfaces/precompiles/IAssets.sol index 65580341..0bd886cc 100644 --- a/src/interfaces/precompiles/IAssets.sol +++ b/src/interfaces/precompiles/IAssets.sol @@ -55,26 +55,41 @@ 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 /// 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( - uint32 clientChainID, + function registerToken( + uint32 clientChainId, bytes calldata token, uint8 decimals, uint256 tvlLimit, string calldata name, - string calldata metaData - ) external returns (bool success, bool updated); + string calldata metaData, + string calldata oracleInfo + ) 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 + /// @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); /// QUERIES /// @dev Returns the chain indices of the client chains. diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol index eec6caee..9d6bbe9f 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 @@ -118,7 +117,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 35415e8f..3f758feb 100644 --- a/src/storage/ExocoreGatewayStorage.sol +++ b/src/storage/ExocoreGatewayStorage.sol @@ -153,12 +153,14 @@ contract ExocoreGatewayStorage is GatewayStorage { error RegisterClientChainToExocoreFailed(uint32 clientChainId); /// @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 AddWhitelistTokenFailed(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(bytes32 token); + error UpdateWhitelistTokenFailed(uint32 clientChainId, 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..7252a4d0 100644 --- a/test/foundry/ExocoreDeployer.t.sol +++ b/test/foundry/ExocoreDeployer.t.sol @@ -136,56 +136,57 @@ 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); - // 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 - ); + 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, + 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.addWhitelistToken{value: nativeFee}( + clientChainId, whitelistTokens[i], decimals[i], tvlLimits[i], names[i], metaDatas[i], oracleInfos[i] + ); + } // second layerzero relayers should watch the request message packet and relay the message to destination // endpoint @@ -195,15 +196,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/ClientChainGateway.t.sol b/test/foundry/unit/ClientChainGateway.t.sol index 2c9bd7e6..fb98139d 100644 --- a/test/foundry/unit/ClientChainGateway.t.sol +++ b/test/foundry/unit/ClientChainGateway.t.sol @@ -374,13 +374,17 @@ contract WithdrawalPrincipalFromExocore is SetUp { // 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); + 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))); } diff --git a/test/foundry/unit/ExocoreGateway.t.sol b/test/foundry/unit/ExocoreGateway.t.sol index 7a27766c..f3bc09bf 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 { @@ -514,276 +526,166 @@ 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; + + error IncorrectNativeFee(uint256 amount); 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.addWhitelistToken{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 + exocoreGateway.addWhitelistToken{value: nativeFee}( + clientChainId, bytes32(0), 18, type(uint256).max, "name", "metadata", "oracleInfo" ); } - 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)); - + function test_RevertWhen_ZeroValue() public { vm.startPrank(exocoreValidatorSet.addr); - vm.expectRevert(abi.encodeWithSelector(ExocoreGatewayStorage.InvalidWhitelistTokensInput.selector)); - exocoreGateway.addOrUpdateWhitelistTokens{value: nativeFee}( - clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData + vm.expectRevert(abi.encodeWithSelector(IncorrectNativeFee.selector, uint256(0))); + exocoreGateway.addWhitelistToken{value: 0}( + clientChainId, + bytes32(bytes20(address(restakeToken))), + 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.addWhitelistToken{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.addWhitelistToken{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.addWhitelistToken{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); - } - - 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() - ); - } + vm.stopPrank(); } } contract UpdateWhitelistTokens is SetUp { - 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 WhitelistTokenAdded(uint32 clientChainId, bytes32 token); 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(); + // 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, + tvlLimit: 1e8 ether, + name: "RestakeToken", + metaData: "ERC20 LST token", + oracleInfo: "oracleInfo" + }); + } + function test_RevertUpdateWhen_CallerNotOwner() public { vm.startPrank(deployer.addr); vm.expectRevert("Ownable: caller is not the owner"); - exocoreGateway.addOrUpdateWhitelistTokens(clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData); + exocoreGateway.updateWhitelistToken( + clientChainId, tokenDetails.tokenAddress, tokenDetails.tvlLimit, tokenDetails.metaData + ); } - function test_RevertWhen_Paused() public { + function test_RevertUpdateWhen_Paused() public { 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); + exocoreGateway.updateWhitelistToken( + clientChainId, tokenDetails.tokenAddress, tokenDetails.tvlLimit, tokenDetails.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"; - + function test_RevertUpdateWhen_HasZeroAddress() public { vm.startPrank(exocoreValidatorSet.addr); vm.expectRevert("ExocoreGateway: token cannot be zero address"); - exocoreGateway.addOrUpdateWhitelistTokens(clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData); + exocoreGateway.updateWhitelistToken(clientChainId, bytes32(0), tokenDetails.tvlLimit, tokenDetails.metaData); } - 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_HasZeroChainId() public { vm.startPrank(exocoreValidatorSet.addr); - vm.expectRevert("ExocoreGateway: tvl limit should not be zero"); - exocoreGateway.addOrUpdateWhitelistTokens(clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData); + vm.expectRevert("ExocoreGateway: client chain id cannot be zero"); + exocoreGateway.updateWhitelistToken(0, tokenDetails.tokenAddress, tokenDetails.tvlLimit, tokenDetails.metaData); } - 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 { 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); - } - - function _addWhitelistTokens( - uint32 clientChainId_, - bytes32[] memory whitelistTokens_, - uint8[] memory decimals_, - uint256[] memory tvlLimits_, - string[] memory names_, - string[] memory metaData_ - ) internal { - 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.expectEmit(address(exocoreGateway)); + emit WhitelistTokenUpdated(clientChainId, tokenDetails.tokenAddress); + exocoreGateway.updateWhitelistToken( + clientChainId, tokenDetails.tokenAddress, tokenDetails.tvlLimit * 5, "new metadata" ); - vm.stopPrank(); } } diff --git a/test/mocks/AssetsMock.sol b/test/mocks/AssetsMock.sol index 29283505..0bd5592e 100644 --- a/test/mocks/AssetsMock.sol +++ b/test/mocks/AssetsMock.sol @@ -65,23 +65,36 @@ contract AssetsMock is IAssets { return (true, updated); } - function registerOrUpdateTokens( + function registerToken( uint32 clientChainId, bytes calldata token, uint8 decimals, uint256 tvlLimit, string calldata name, - string calldata metaData - ) external returns (bool success, bool updated) { + string calldata metaData, + string calldata oracleInfo + ) 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 643f99ac..36dc86fa 100644 --- a/test/mocks/ExocoreGatewayMock.sol +++ b/test/mocks/ExocoreGatewayMock.sol @@ -166,73 +166,62 @@ 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 addWhitelistToken( 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] - ); - - if (success) { - if (!updated) { - emit WhitelistTokenAdded(clientChainId, tokens[i]); - } else { - emit WhitelistTokenUpdated(clientChainId, tokens[i]); - } - } else { - if (!updated) { - revert AddWhitelistTokenFailed(tokens[i]); - } else { - revert UpdateWhitelistTokenFailed(tokens[i]); - } - } + if (msg.value == 0) { + revert Errors.ZeroValue(); } - - if (!updated) { + 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 = ASSETS_CONTRACT.registerToken( + clientChainId, + abi.encodePacked(token), // convert to bytes from bytes32 + decimals, + tvlLimit, + name, + metaData, + oracleInfo + ); + if (success) { + emit WhitelistTokenAdded(clientChainId, token); _sendInterchainMsg( clientChainId, - Action.REQUEST_ADD_WHITELIST_TOKENS, - abi.encodePacked(uint8(tokens.length), tokens), + Action.REQUEST_ADD_WHITELIST_TOKEN, + abi.encodePacked(token), // convert for decoding it on the receiving end false ); + } else { + revert AddWhitelistTokenFailed(clientChainId, token); } } - 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(); + 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) { + emit WhitelistTokenUpdated(clientChainId, token); + } else { + revert UpdateWhitelistTokenFailed(clientChainId, token); } }