From 9c469d42fb321f641fcbeb2457e69c259acd99b7 Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Tue, 30 Jul 2024 05:29:18 +0000 Subject: [PATCH] refactor(precompiles): chain as source of truth The `ExocoreGateway` allows the contract owner to call precompiles which allow the registration of a client chain or a token. It also enables updated of these items. To check whether a request is an update or an addition, it relies on the ExocoreGateway keeping a copy of registered items. However, this does not work for items which were registered at genesis. This PR works around that problem by requiring that the single source of truth be the precompiles, whose responsibility is to report if a request was an update or an addition. --- docs/contracts-owner-manual.md | 8 +- script/3_Setup.s.sol | 2 +- src/core/ExocoreGateway.sol | 147 +++++++++---------------- src/interfaces/IExocoreGateway.sol | 19 +--- src/interfaces/precompiles/IAssets.sol | 52 +++++---- src/libraries/Errors.sol | 3 - src/storage/ExocoreGatewayStorage.sol | 12 -- test/foundry/ExocoreDeployer.t.sol | 2 +- test/foundry/unit/ExoCapsule.t.sol | 2 - test/foundry/unit/ExocoreGateway.t.sol | 78 +++---------- test/mocks/AssetsMock.sol | 17 +-- test/mocks/ExocoreGatewayMock.sol | 88 ++++++--------- 12 files changed, 143 insertions(+), 287 deletions(-) diff --git a/docs/contracts-owner-manual.md b/docs/contracts-owner-manual.md index e2a867a7..42e84ecf 100644 --- a/docs/contracts-owner-manual.md +++ b/docs/contracts-owner-manual.md @@ -17,12 +17,10 @@ While for `Vault` and `ExoCapsule`, they are deployed with upgradeable beacon pr After all contracts are deployed, before the protocol starts to work, there are a few works left to be done by contract owner to enable restaking. One of the most important tasks is to register the client chain id and meta info to Exocore to mark this client chain as valid. This is done by the contract caller calling `ExocoreGateway.registerOrUpdateClientChain` to write `clientChainId`, `addressLength`, `name`, `signatureType` and other meta data to Exocore native module to finish registration. This operation would also call `ExocoreGateway.setPeer` to enable LayerZero messaging by setting remote `ClientChainGateway` as trusted peer to send/receive messages. After finishing registration, contract owner could call `ExocoreGateway.registerOrUpdateClientChain` again to update the meta data and set new peer, or contract owner could solely call `ExocoreGateway.setPeer` to change the address of remote peer contract. -## add tokens to whitelist +## add or update tokens to whitelist -Another important task before restaking being activated is to add tokens to whitelist to mark them as stake-able on both Exocore and client chain. This is done by contract owner calling `ExocoreGateway.addWhitelistTokens` to write token addresses, decimals, TVL limits and other metadata to Exocore, as well as sending a cross-chain message through layerzero to client chain to add these token addresses to the whitelist of `ClientChainGateway`. +Another important task before restaking being activated is to add tokens to whitelist to mark them as stake-able on both Exocore and client chain. This is done by contract owner calling `ExocoreGateway.addOrUpdateWhitelistTokens` to write token addresses, decimals, TVL limits and other metadata to Exocore, as well as sending a cross-chain message through layerzero to client chain to add these token addresses to the whitelist of `ClientChainGateway`. Notice: contract owner must make sure the token data is correct like address, decimals and TVL limit, more importantly contract owner must ensure that for the same index, the data in different arrays like `tokens`, `decimals`, `tvlLimits` must point to the same token to be composed as complete token data. -## upgrade the meta data of whitelisted tokens - -After adding tokens to whitelist, contract owner could call `ExocoreGateway.updateWhitelistedTokens` to update the meta data of already whitelisted tokens, and this function would not send a cross-chain message to client chain since the whitelist of `ClientChainGateway` only stores the token addresses. \ No newline at end of file +After adding tokens to whitelist, contract owner could call `ExocoreGateway.addOrUpdateWhitelistTokens` to update the meta data of already whitelisted tokens, and this function would not send a cross-chain message to client chain since the whitelist of `ClientChainGateway` only stores the token addresses. \ No newline at end of file diff --git a/script/3_Setup.s.sol b/script/3_Setup.s.sol index e73e482e..966a35b4 100644 --- a/script/3_Setup.s.sol +++ b/script/3_Setup.s.sol @@ -118,7 +118,7 @@ contract SetupScript is BaseScript { vm.selectFork(exocore); uint256 messageLength = TOKEN_ADDRESS_BYTES_LENGTH * whitelistTokensBytes32.length + 2; uint256 nativeFee = exocoreGateway.quote(clientChainId, new bytes(messageLength)); - exocoreGateway.addWhitelistTokens{value: nativeFee}( + exocoreGateway.addOrUpdateWhitelistTokens{value: nativeFee}( clientChainId, whitelistTokensBytes32, decimals, tvlLimits, names, metaData ); vm.stopBroadcast(); diff --git a/src/core/ExocoreGateway.sol b/src/core/ExocoreGateway.sol index 33bd0828..ee1da1a6 100644 --- a/src/core/ExocoreGateway.sol +++ b/src/core/ExocoreGateway.sol @@ -142,20 +142,20 @@ contract ExocoreGateway is ) { revert Errors.ZeroValue(); } - // signature type could be left as empty for current implementation - _registerClientChain(clientChainId, addressLength, name, metaInfo, signatureType); + + bool updated = _registerOrUpdateClientChain(clientChainId, addressLength, name, metaInfo, signatureType); + // the peer is always set, regardless of `updated` super.setPeer(clientChainId, peer); - if (!isRegisteredClientChain[clientChainId]) { - isRegisteredClientChain[clientChainId] = true; - emit ClientChainRegistered(clientChainId); - } else { + if (updated) { emit ClientChainUpdated(clientChainId); + } else { + emit ClientChainRegistered(clientChainId); } } /// @notice Sets a peer on the destination chain for this contract. - /// @dev This is the LayerZero peer. + /// @dev This is the LayerZero peer. This function is only here for the modifiers. /// @param clientChainId The id of the client chain. /// @param clientChainGateway The address of the peer as bytes32. function setPeer(uint32 clientChainId, bytes32 clientChainGateway) @@ -164,15 +164,11 @@ contract ExocoreGateway is onlyOwner whenNotPaused { - if (!isRegisteredClientChain[clientChainId]) { - revert Errors.ExocoreGatewayNotRegisteredClientChainId(); - } - super.setPeer(clientChainId, clientChainGateway); } /// @inheritdoc IExocoreGateway - function addWhitelistTokens( + function addOrUpdateWhitelistTokens( uint32 clientChainId, bytes32[] calldata tokens, uint8[] calldata decimals, @@ -180,19 +176,43 @@ contract ExocoreGateway is string[] calldata names, string[] calldata metaData ) external payable onlyOwner whenNotPaused nonReentrant { - _addOrUpdateWhitelistTokens(clientChainId, tokens, decimals, tvlLimits, names, metaData, true); - } + _validateWhitelistTokensInput(tokens, decimals, tvlLimits, names, metaData); - /// @inheritdoc IExocoreGateway - function updateWhitelistedTokens( - uint32 clientChainId, - bytes32[] calldata tokens, - uint8[] calldata decimals, - uint256[] calldata tvlLimits, - string[] calldata names, - string[] calldata metaData - ) external onlyOwner whenNotPaused { - _addOrUpdateWhitelistTokens(clientChainId, tokens, decimals, tvlLimits, names, metaData, false); + 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.registerOrUpdateToken( + 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) { + _sendInterchainMsg( + clientChainId, + Action.REQUEST_ADD_WHITELIST_TOKENS, + abi.encodePacked(uint8(tokens.length), tokens), + false + ); + } } /** @@ -228,86 +248,19 @@ contract ExocoreGateway is } } - /// @dev The internal version of addWhitelistTokens and updateWhitelistedTokens. - /// @param clientChainId Source client chain id - /// @param tokens List of token addresses - /// @param decimals List of token decimals (like 18) - /// @param tvlLimits List of TVL limits (like max supply) - /// @param names List of token names - /// @param metaData List of arbitrary meta data for each token - /// @param add Whether to add or update the tokens - /// @dev Validates that lengths are equal, <= 255, and that the chain is registered. - // 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( - uint32 clientChainId, - bytes32[] calldata tokens, - uint8[] calldata decimals, - uint256[] calldata tvlLimits, - string[] calldata names, - string[] calldata metaData, - bool add - ) internal { - _validateWhitelistTokensInput(clientChainId, tokens, decimals, tvlLimits, names, metaData); - - for (uint256 i; i < tokens.length; i++) { - require(tokens[i] != bytes32(0), "ExocoreGateway: token cannot be zero address"); - if (!add) { - require(isWhitelistedToken[tokens[i]], "ExocoreGateway: token has not been added to whitelist before"); - } - 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"); - - bool success = ASSETS_CONTRACT.registerToken( - clientChainId, abi.encodePacked(tokens[i]), decimals[i], tvlLimits[i], names[i], metaData[i] - ); - - if (success) { - if (add) { - isWhitelistedToken[tokens[i]] = true; - emit WhitelistTokenAdded(clientChainId, tokens[i]); - } else { - emit WhitelistTokenUpdated(clientChainId, tokens[i]); - } - } else { - if (add) { - revert AddWhitelistTokenFailed(tokens[i]); - } else { - revert UpdateWhitelistTokenFailed(tokens[i]); - } - } - } - if (add) { - _sendInterchainMsg( - clientChainId, - Action.REQUEST_ADD_WHITELIST_TOKENS, - abi.encodePacked(uint8(tokens.length), tokens), - false - ); - } - } - /// @dev Validates the input for whitelist tokens. - /// @param clientChainId The client chain id, which must have been previously registered. /// @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( - uint32 clientChainId, bytes32[] calldata tokens, uint8[] calldata decimals, uint256[] calldata tvlLimits, string[] calldata names, string[] calldata metaData - ) internal view { - if (!isRegisteredClientChain[clientChainId]) { - revert ClientChainIDNotRegisteredBefore(clientChainId); - } - + ) internal pure { uint256 expectedLength = tokens.length; if (expectedLength > type(uint8).max) { revert WhitelistTokensListTooLong(); @@ -321,23 +274,25 @@ contract ExocoreGateway is } } - /// @dev The internal version of registerClientChain. + /// @dev The internal version of registerOrUpdateClientChain. /// @param clientChainId The client chain id. /// @param addressLength The length of the address type on the client chain. /// @param name The name of the client chain. /// @param metaInfo The arbitrary metadata for the client chain. /// @param signatureType The signature type supported by the client chain. - function _registerClientChain( + function _registerOrUpdateClientChain( uint32 clientChainId, uint8 addressLength, string calldata name, string calldata metaInfo, string calldata signatureType - ) internal { - bool success = ASSETS_CONTRACT.registerClientChain(clientChainId, addressLength, name, metaInfo, signatureType); + ) internal returns (bool) { + (bool success, bool updated) = + ASSETS_CONTRACT.registerOrUpdateClientChain(clientChainId, addressLength, name, metaInfo, signatureType); if (!success) { revert RegisterClientChainToExocoreFailed(clientChainId); } + return updated; } /// @inheritdoc OAppReceiverUpgradeable diff --git a/src/interfaces/IExocoreGateway.sol b/src/interfaces/IExocoreGateway.sol index aba810bc..c43e2dbe 100644 --- a/src/interfaces/IExocoreGateway.sol +++ b/src/interfaces/IExocoreGateway.sol @@ -49,7 +49,7 @@ interface IExocoreGateway is IOAppReceiver, IOAppCore { /// @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. /// @dev The chain must be registered before adding tokens. - function addWhitelistTokens( + function addOrUpdateWhitelistTokens( uint32 clientChainId, bytes32[] calldata tokens, uint8[] calldata decimals, @@ -58,21 +58,4 @@ interface IExocoreGateway is IOAppReceiver, IOAppCore { string[] calldata metaData ) external payable; - /// @notice Updates a list of whitelisted tokens 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. - /// @dev The chain must be registered before updating tokens, and the token as well. - function updateWhitelistedTokens( - uint32 clientChainId, - bytes32[] calldata tokens, - uint8[] calldata decimals, - uint256[] calldata tvlLimits, - string[] calldata names, - string[] calldata metaData - ) external; - } diff --git a/src/interfaces/precompiles/IAssets.sol b/src/interfaces/precompiles/IAssets.sol index 90eb474f..12f0c822 100644 --- a/src/interfaces/precompiles/IAssets.sol +++ b/src/interfaces/precompiles/IAssets.sol @@ -17,59 +17,67 @@ interface IAssets { /// @dev deposit the client chain assets for the staker, /// that will change the state in deposit module /// Note that this address cannot be a module account. - /// @param clientChainLzID The LzID of client chain + /// @param clientChainID is the layerZero chainID if it is supported. + // It might be allocated by Exocore when the client chain isn't supported + // by layerZero /// @param assetsAddress The client chain asset address /// @param stakerAddress The staker address /// @param opAmount The amount to deposit - function depositTo(uint32 clientChainLzID, bytes memory assetsAddress, bytes memory stakerAddress, uint256 opAmount) + function depositTo(uint32 clientChainID, bytes memory assetsAddress, bytes memory stakerAddress, uint256 opAmount) external returns (bool success, uint256 latestAssetState); - /// TRANSACTIONS /// @dev withdraw To the staker, that will change the state in withdraw module /// Note that this address cannot be a module account. - /// @param clientChainLzID The LzID of client chain + /// @param clientChainID is the layerZero chainID if it is supported. + // It might be allocated by Exocore when the client chain isn't supported + // by layerZero /// @param assetsAddress The client chain asset Address /// @param withdrawAddress The withdraw address /// @param opAmount The withdraw amount function withdrawPrincipal( - uint32 clientChainLzID, + uint32 clientChainID, bytes memory assetsAddress, bytes memory withdrawAddress, uint256 opAmount ) external returns (bool success, uint256 latestAssetState); - /// QUERIES - /// @dev Returns the chain indices of the client chains. - function getClientChains() external view returns (bool, uint32[] memory); - - /// TRANSACTIONS - /// @dev register some client chain to allow token registration from that chain, staking - /// from that chain, and other operations from that chain. + /// @dev registers or updates a client chain to allow deposits / staking, etc. + /// from that chain. /// @param clientChainID is the layerZero chainID if it is supported. // It might be allocated by Exocore when the client chain isn't supported // by layerZero - function registerClientChain( + function registerOrUpdateClientChain( uint32 clientChainID, uint8 addressLength, string calldata name, string calldata metaInfo, string calldata signatureType - ) external returns (bool success); + ) external returns (bool success, bool updated); - /// TRANSACTIONS - /// @dev register unwhitelisted token address to exocore assets module - /// @param clientChainID is the layerZero chainID if it is supported. - // It might be allocated by Exocore when the client chain isn't supported - // by layerZero - /// @param token The token address that would be registered to exocore - function registerToken( + /// @dev register or update token addresses to exocore + /// @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 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 + /// @return success if the token registration is successful + /// @return updated whether the token was added or updated + function registerOrUpdateToken( uint32 clientChainID, bytes calldata token, uint8 decimals, uint256 tvlLimit, string calldata name, string calldata metaData - ) external returns (bool success); + ) external returns (bool success, bool updated); + + /// QUERIES + /// @dev Returns the chain indices of the client chains. + function getClientChains() external view returns (bool, uint32[] memory); } diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol index bc210b6a..903dd64a 100644 --- a/src/libraries/Errors.sol +++ b/src/libraries/Errors.sol @@ -163,9 +163,6 @@ library Errors { /// @dev ExocoreGateway: failed to decode client chain ids error ExocoreGatewayFailedToDecodeClientChainIds(); - /// @dev ExocoreGateway: client chain should be registered before setting peer to change peer address - error ExocoreGatewayNotRegisteredClientChainId(); - /// @dev ExocoreGateway: thrown when associateOperatorWithEVMStaker failed error AssociateOperatorFailed(uint32 clientChainId, address staker, string operator); diff --git a/src/storage/ExocoreGatewayStorage.sol b/src/storage/ExocoreGatewayStorage.sol index db224c3d..f1edb763 100644 --- a/src/storage/ExocoreGatewayStorage.sol +++ b/src/storage/ExocoreGatewayStorage.sol @@ -47,14 +47,6 @@ contract ExocoreGatewayStorage is GatewayStorage { /// @dev Used to ensure no repeated bootstrap requests are sent. mapping(uint32 clienChainId => bool) public chainToBootstrapped; - /// @notice A mapping from client chain IDs to whether the chain has been registered. - /// @dev Used to ensure that only registered client chains can interact with the gateway. - mapping(uint32 clienChainId => bool registered) public isRegisteredClientChain; - - /// @notice A mapping from token address to whether the token is whitelisted. - /// @dev Used to ensure no duplicate tokens are added to the whitelist. - mapping(bytes32 token => bool whitelisted) public isWhitelistedToken; - /// @notice Emitted when a precompile call fails. /// @param precompile Address of the precompile contract. /// @param nonce The LayerZero nonce @@ -171,10 +163,6 @@ contract ExocoreGatewayStorage is GatewayStorage { /// @notice Thrown when the whitelist tokens input is invalid. error InvalidWhitelistTokensInput(); - /// @notice Thrown when the client chain ID is not registered. - /// @param clientChainId The LayerZero chain ID of the client chain. - error ClientChainIDNotRegisteredBefore(uint32 clientChainId); - /// @notice Thrown when the whitelist tokens list is too long. error WhitelistTokensListTooLong(); diff --git a/test/foundry/ExocoreDeployer.t.sol b/test/foundry/ExocoreDeployer.t.sol index f2d4b8e9..8bd9e442 100644 --- a/test/foundry/ExocoreDeployer.t.sol +++ b/test/foundry/ExocoreDeployer.t.sol @@ -183,7 +183,7 @@ contract ExocoreDeployer is Test { uint64(1), registerTokensRequestNativeFee ); - exocoreGateway.addWhitelistTokens{value: registerTokensRequestNativeFee}( + exocoreGateway.addOrUpdateWhitelistTokens{value: registerTokensRequestNativeFee}( clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData ); diff --git a/test/foundry/unit/ExoCapsule.t.sol b/test/foundry/unit/ExoCapsule.t.sol index abc42fc6..102538ac 100644 --- a/test/foundry/unit/ExoCapsule.t.sol +++ b/test/foundry/unit/ExoCapsule.t.sol @@ -241,8 +241,6 @@ contract VerifyDepositProof is DepositSetup { capsule.getRegisteredValidatorByPubkey(_getPubkey(validatorContainer)); assertEq(uint8(validator.status), uint8(ExoCapsuleStorage.VALIDATOR_STATUS.REGISTERED)); assertEq(validator.validatorIndex, validatorProof.validatorIndex); - assertEq(validator.mostRecentBalanceUpdateTimestamp, validatorProof.beaconBlockTimestamp); - assertEq(validator.restakedBalanceGwei, _getEffectiveBalance(validatorContainer)); } function test_verifyDepositProof_revert_mismatchWithdrawalCredentials() public { diff --git a/test/foundry/unit/ExocoreGateway.t.sol b/test/foundry/unit/ExocoreGateway.t.sol index 9b9e2367..55168576 100644 --- a/test/foundry/unit/ExocoreGateway.t.sol +++ b/test/foundry/unit/ExocoreGateway.t.sol @@ -501,12 +501,6 @@ contract SetPeer is SetUp { exocoreGateway.setPeer(anotherClientChain, newPeer); } - function test_RevertWhen_ClientChainNotRegistered() public { - vm.startPrank(exocoreValidatorSet.addr); - vm.expectRevert(Errors.ExocoreGatewayNotRegisteredClientChainId.selector); - exocoreGateway.setPeer(anotherClientChain, newPeer); - } - } contract AddWhitelistTokens is SetUp { @@ -532,7 +526,7 @@ contract AddWhitelistTokens is SetUp { vm.startPrank(deployer.addr); vm.expectRevert("Ownable: caller is not the owner"); - exocoreGateway.addWhitelistTokens{value: nativeFee}( + exocoreGateway.addOrUpdateWhitelistTokens{value: nativeFee}( clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData ); } @@ -546,27 +540,11 @@ contract AddWhitelistTokens is SetUp { uint256 messageLength = TOKEN_ADDRESS_BYTES_LENGTH * whitelistTokens.length + 2; uint256 nativeFee = exocoreGateway.quote(clientChainId, new bytes(messageLength)); vm.expectRevert("Pausable: paused"); - exocoreGateway.addWhitelistTokens{value: nativeFee}( + exocoreGateway.addOrUpdateWhitelistTokens{value: nativeFee}( clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData ); } - function test_RevertWhen_ClientChainNotRegisteredBefore() public { - uint32 anotherClientChain = 3; - _prepareInputs(2); - - 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.ClientChainIDNotRegisteredBefore.selector, anotherClientChain) - ); - exocoreGateway.addWhitelistTokens{value: nativeFee}( - anotherClientChain, whitelistTokens, decimals, tvlLimits, names, metaData - ); - } - function test_RevertWhen_TokensListTooLong() public { _prepareInputs(256); @@ -575,7 +553,7 @@ contract AddWhitelistTokens is SetUp { vm.startPrank(exocoreValidatorSet.addr); vm.expectRevert(abi.encodeWithSelector(ExocoreGatewayStorage.WhitelistTokensListTooLong.selector)); - exocoreGateway.addWhitelistTokens{value: nativeFee}( + exocoreGateway.addOrUpdateWhitelistTokens{value: nativeFee}( clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData ); } @@ -589,7 +567,7 @@ contract AddWhitelistTokens is SetUp { vm.startPrank(exocoreValidatorSet.addr); vm.expectRevert(abi.encodeWithSelector(ExocoreGatewayStorage.InvalidWhitelistTokensInput.selector)); - exocoreGateway.addWhitelistTokens{value: nativeFee}( + exocoreGateway.addOrUpdateWhitelistTokens{value: nativeFee}( clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData ); } @@ -609,7 +587,7 @@ contract AddWhitelistTokens is SetUp { vm.startPrank(exocoreValidatorSet.addr); vm.expectRevert("ExocoreGateway: token cannot be zero address"); - exocoreGateway.addWhitelistTokens{value: nativeFee}( + exocoreGateway.addOrUpdateWhitelistTokens{value: nativeFee}( clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData ); } @@ -623,7 +601,7 @@ contract AddWhitelistTokens is SetUp { vm.startPrank(exocoreValidatorSet.addr); vm.expectRevert("ExocoreGateway: tvl limit should not be zero"); - exocoreGateway.addWhitelistTokens{value: nativeFee}( + exocoreGateway.addOrUpdateWhitelistTokens{value: nativeFee}( clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData ); } @@ -643,7 +621,7 @@ contract AddWhitelistTokens is SetUp { 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.addWhitelistTokens{value: nativeFee}( + exocoreGateway.addOrUpdateWhitelistTokens{value: nativeFee}( clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData ); } @@ -689,7 +667,7 @@ contract UpdateWhitelistTokens is SetUp { vm.startPrank(deployer.addr); vm.expectRevert("Ownable: caller is not the owner"); - exocoreGateway.updateWhitelistedTokens(clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData); + exocoreGateway.addOrUpdateWhitelistTokens(clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData); } function test_RevertWhen_Paused() public { @@ -699,20 +677,7 @@ contract UpdateWhitelistTokens is SetUp { _prepareInputs(2); vm.expectRevert("Pausable: paused"); - exocoreGateway.updateWhitelistedTokens(clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData); - } - - function test_RevertWhen_ClientChainNotRegisteredBefore() public { - uint32 anotherClientChain = 3; - _prepareInputs(2); - - vm.startPrank(exocoreValidatorSet.addr); - vm.expectRevert( - abi.encodeWithSelector(ExocoreGatewayStorage.ClientChainIDNotRegisteredBefore.selector, anotherClientChain) - ); - exocoreGateway.updateWhitelistedTokens( - anotherClientChain, whitelistTokens, decimals, tvlLimits, names, metaData - ); + exocoreGateway.addOrUpdateWhitelistTokens(clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData); } function test_RevertWhen_TokensListTooLong() public { @@ -720,7 +685,7 @@ contract UpdateWhitelistTokens is SetUp { vm.startPrank(exocoreValidatorSet.addr); vm.expectRevert(abi.encodeWithSelector(ExocoreGatewayStorage.WhitelistTokensListTooLong.selector)); - exocoreGateway.updateWhitelistedTokens(clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData); + exocoreGateway.addOrUpdateWhitelistTokens(clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData); } function test_RevertWhen_LengthNotMatch() public { @@ -729,7 +694,7 @@ contract UpdateWhitelistTokens is SetUp { vm.startPrank(exocoreValidatorSet.addr); vm.expectRevert(abi.encodeWithSelector(ExocoreGatewayStorage.InvalidWhitelistTokensInput.selector)); - exocoreGateway.updateWhitelistedTokens(clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData); + exocoreGateway.addOrUpdateWhitelistTokens(clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData); } function test_RevertWhen_HasZeroAddressToken() public { @@ -752,7 +717,7 @@ contract UpdateWhitelistTokens is SetUp { vm.startPrank(exocoreValidatorSet.addr); vm.expectRevert("ExocoreGateway: token cannot be zero address"); - exocoreGateway.updateWhitelistedTokens(clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData); + exocoreGateway.addOrUpdateWhitelistTokens(clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData); } function test_RevertWhen_HasZeroTVMLimit() public { @@ -768,20 +733,7 @@ contract UpdateWhitelistTokens is SetUp { vm.startPrank(exocoreValidatorSet.addr); vm.expectRevert("ExocoreGateway: tvl limit should not be zero"); - exocoreGateway.updateWhitelistedTokens(clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData); - } - - function test_RevertWhen_HasTokenNotRegisteredBefore() public { - _prepareInputs(1); - whitelistTokens[0] = bytes32(bytes20(address(restakeToken))); - decimals[0] = 18; - tvlLimits[0] = 1e10 ether; - names[0] = "RestakeToken"; - metaData[0] = "ERC20 LST token"; - - vm.startPrank(exocoreValidatorSet.addr); - vm.expectRevert("ExocoreGateway: token has not been added to whitelist before"); - exocoreGateway.updateWhitelistedTokens(clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData); + exocoreGateway.addOrUpdateWhitelistTokens(clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData); } function test_Success_UpdateWhitelistTokens() public { @@ -800,7 +752,7 @@ contract UpdateWhitelistTokens is SetUp { vm.expectEmit(true, true, true, true, address(exocoreGateway)); emit WhitelistTokenUpdated(clientChainId, whitelistTokens[0]); vm.startPrank(exocoreValidatorSet.addr); - exocoreGateway.updateWhitelistedTokens(clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData); + exocoreGateway.addOrUpdateWhitelistTokens(clientChainId, whitelistTokens, decimals, tvlLimits, names, metaData); } function _prepareInputs(uint256 listLength) internal { @@ -822,7 +774,7 @@ contract UpdateWhitelistTokens is SetUp { vm.startPrank(exocoreValidatorSet.addr); uint256 messageLength = TOKEN_ADDRESS_BYTES_LENGTH * whitelistTokens.length + 2; uint256 nativeFee = exocoreGateway.quote(clientChainId, new bytes(messageLength)); - exocoreGateway.addWhitelistTokens{value: nativeFee}( + exocoreGateway.addOrUpdateWhitelistTokens{value: nativeFee}( clientChainId_, whitelistTokens_, decimals_, tvlLimits_, names_, metaData_ ); vm.stopPrank(); diff --git a/test/mocks/AssetsMock.sol b/test/mocks/AssetsMock.sol index 2d2cb00e..0f4f7a4e 100644 --- a/test/mocks/AssetsMock.sol +++ b/test/mocks/AssetsMock.sol @@ -50,35 +50,38 @@ contract AssetsMock is IAssets { return (true, chainIds); } - function registerClientChain( + function registerOrUpdateClientChain( uint32 clientChainId, uint8 addressLength, string calldata name, string calldata metaInfo, string calldata signatureType - ) external returns (bool) { + ) external returns (bool, bool) { + bool updated = isRegisteredChain[clientChainId]; if (!isRegisteredChain[clientChainId]) { isRegisteredChain[clientChainId] = true; chainIds.push(clientChainId); } - return true; + return (true, updated); } - function registerToken( + function registerOrUpdateToken( uint32 clientChainId, bytes calldata token, uint8 decimals, uint256 tvlLimit, string calldata name, string calldata metaData - ) external returns (bool) { + ) external returns (bool success, bool updated) { require(isRegisteredChain[clientChainId], "the chain is not registered before"); - if (!isRegisteredToken[clientChainId][token]) { + updated = isRegisteredToken[clientChainId][token]; + + if (!updated) { isRegisteredToken[clientChainId][token] = true; } - return true; + return (true, updated); } function getPrincipalBalance(uint32 clientChainLzId, bytes memory token, bytes memory staker) diff --git a/test/mocks/ExocoreGatewayMock.sol b/test/mocks/ExocoreGatewayMock.sol index 0e79167f..25d8741f 100644 --- a/test/mocks/ExocoreGatewayMock.sol +++ b/test/mocks/ExocoreGatewayMock.sol @@ -154,14 +154,13 @@ contract ExocoreGatewayMock is require(bytes(metaInfo).length != 0, "ExocoreGateway: meta data cannot be empty"); // signature type could be left as empty for current implementation - _registerClientChain(clientChainId, addressLength, name, metaInfo, signatureType); + bool updated = _registerOrUpdateClientChain(clientChainId, addressLength, name, metaInfo, signatureType); super.setPeer(clientChainId, peer); - if (!isRegisteredClientChain[clientChainId]) { - isRegisteredClientChain[clientChainId] = true; - emit ClientChainRegistered(clientChainId); - } else { + if (updated) { emit ClientChainUpdated(clientChainId); + } else { + emit ClientChainRegistered(clientChainId); } } @@ -171,18 +170,13 @@ contract ExocoreGatewayMock is onlyOwner whenNotPaused { - require( - isRegisteredClientChain[clientChainId], - "ExocoreGateway: client chain should be registered before setting peer to change peer address" - ); - 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 addWhitelistTokens( + function addOrUpdateWhitelistTokens( uint32 clientChainId, bytes32[] calldata tokens, uint8[] calldata decimals, @@ -190,74 +184,52 @@ contract ExocoreGatewayMock is string[] calldata names, string[] calldata metaData ) external payable onlyOwner whenNotPaused nonReentrant { - _validateWhitelistTokensInput(clientChainId, tokens, decimals, tvlLimits, names, metaData); + _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(!isWhitelistedToken[tokens[i]], "ExocoreGateway: token has already been added to whitelist before"); 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"); - bool success = ASSETS_CONTRACT.registerToken( + (success, updated) = ASSETS_CONTRACT.registerOrUpdateToken( clientChainId, abi.encodePacked(tokens[i]), decimals[i], tvlLimits[i], names[i], metaData[i] ); if (success) { - isWhitelistedToken[tokens[i]] = true; + if (!updated) { + emit WhitelistTokenAdded(clientChainId, tokens[i]); + } else { + emit WhitelistTokenUpdated(clientChainId, tokens[i]); + } } else { - revert AddWhitelistTokenFailed(tokens[i]); + if (!updated) { + revert AddWhitelistTokenFailed(tokens[i]); + } else { + revert UpdateWhitelistTokenFailed(tokens[i]); + } } - - emit WhitelistTokenAdded(clientChainId, tokens[i]); } - _sendInterchainMsg( - clientChainId, Action.REQUEST_ADD_WHITELIST_TOKENS, abi.encodePacked(uint8(tokens.length), tokens), false - ); - } - - function updateWhitelistedTokens( - uint32 clientChainId, - bytes32[] calldata tokens, - uint8[] calldata decimals, - uint256[] calldata tvlLimits, - string[] calldata names, - string[] calldata metaData - ) external onlyOwner whenNotPaused { - _validateWhitelistTokensInput(clientChainId, tokens, decimals, tvlLimits, names, metaData); - - for (uint256 i; i < tokens.length; i++) { - require(tokens[i] != bytes32(0), "ExocoreGateway: token cannot be zero address"); - require(isWhitelistedToken[tokens[i]], "ExocoreGateway: token has not been added to whitelist before"); - 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"); - - bool success = ASSETS_CONTRACT.registerToken( - clientChainId, abi.encodePacked(tokens[i]), decimals[i], tvlLimits[i], names[i], metaData[i] + if (!updated) { + _sendInterchainMsg( + clientChainId, + Action.REQUEST_ADD_WHITELIST_TOKENS, + abi.encodePacked(uint8(tokens.length), tokens), + false ); - - if (!success) { - revert UpdateWhitelistTokenFailed(tokens[i]); - } - - emit WhitelistTokenUpdated(clientChainId, tokens[i]); } } function _validateWhitelistTokensInput( - uint32 clientChainId, bytes32[] calldata tokens, uint8[] calldata decimals, uint256[] calldata tvlLimits, string[] calldata names, string[] calldata metaData - ) internal view { - if (!isRegisteredClientChain[clientChainId]) { - revert ClientChainIDNotRegisteredBefore(clientChainId); - } - + ) internal pure { uint256 expectedLength = tokens.length; if (expectedLength > type(uint8).max) { revert WhitelistTokensListTooLong(); @@ -271,17 +243,19 @@ contract ExocoreGatewayMock is } } - function _registerClientChain( + function _registerOrUpdateClientChain( uint32 clientChainId, uint8 addressLength, string calldata name, string calldata metaInfo, string calldata signatureType - ) internal { - bool success = ASSETS_CONTRACT.registerClientChain(clientChainId, addressLength, name, metaInfo, signatureType); + ) internal returns (bool) { + (bool success, bool updated) = + ASSETS_CONTRACT.registerOrUpdateClientChain(clientChainId, addressLength, name, metaInfo, signatureType); if (!success) { revert RegisterClientChainToExocoreFailed(clientChainId); } + return updated; } function _lzReceive(Origin calldata _origin, bytes calldata payload)