diff --git a/script/11_SetPeers.s.sol b/script/11_SetPeers.s.sol index a2d0d888..83c62bf7 100644 --- a/script/11_SetPeers.s.sol +++ b/script/11_SetPeers.s.sol @@ -3,8 +3,6 @@ pragma solidity ^0.8.19; import {Bootstrap} from "../src/core/Bootstrap.sol"; import {ExocoreGateway} from "../src/core/ExocoreGateway.sol"; -import {CLIENT_CHAINS_PRECOMPILE_ADDRESS} from "../src/interfaces/precompiles/IClientChains.sol"; - import {BaseScript} from "./BaseScript.sol"; import "forge-std/Script.sol"; diff --git a/script/8_DepositValidator.s.sol b/script/13_DepositValidator.s.sol similarity index 95% rename from script/8_DepositValidator.s.sol rename to script/13_DepositValidator.s.sol index ff2a2b7a..dd13f51e 100644 --- a/script/8_DepositValidator.s.sol +++ b/script/13_DepositValidator.s.sol @@ -5,10 +5,6 @@ import "../src/interfaces/IClientChainGateway.sol"; import "../src/interfaces/IExocoreGateway.sol"; import "../src/interfaces/IVault.sol"; -import "../src/interfaces/precompiles/IClaimReward.sol"; -import "../src/interfaces/precompiles/IDelegation.sol"; -import "../src/interfaces/precompiles/IDeposit.sol"; -import "../src/interfaces/precompiles/IWithdrawPrinciple.sol"; import "../src/storage/GatewayStorage.sol"; import "@beacon-oracle/contracts/src/EigenLayerBeaconOracle.sol"; import "@layerzero-v2/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol"; diff --git a/script/1_Prerequisities.s.sol b/script/1_Prerequisities.s.sol index bc4503b7..cfa9274f 100644 --- a/script/1_Prerequisities.s.sol +++ b/script/1_Prerequisities.s.sol @@ -6,9 +6,9 @@ import "@layerzero-v2/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol"; import {ERC20PresetFixedSupply} from "@openzeppelin-contracts/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; import "forge-std/Script.sol"; +import "test/mocks/AssetsMock.sol"; import "test/mocks/ClaimRewardMock.sol"; import "test/mocks/DelegationMock.sol"; -import "test/mocks/DepositWithdrawMock.sol"; import {NonShortCircuitEndpointV2Mock} from "test/mocks/NonShortCircuitEndpointV2Mock.sol"; contract PrerequisitiesScript is BaseScript { @@ -43,8 +43,7 @@ contract PrerequisitiesScript is BaseScript { if (useExocorePrecompileMock) { vm.selectFork(exocore); vm.startBroadcast(deployer.privateKey); - depositMock = address(new DepositWithdrawMock()); - withdrawMock = depositMock; + assetsMock = address(new AssetsMock()); delegationMock = address(new DelegationMock()); claimRewardMock = address(new ClaimRewardMock()); vm.stopBroadcast(); @@ -62,8 +61,7 @@ contract PrerequisitiesScript is BaseScript { vm.serializeAddress(clientChainContracts, "erc20Token", address(restakeToken)); if (useExocorePrecompileMock) { - vm.serializeAddress(exocoreContracts, "depositPrecompileMock", depositMock); - vm.serializeAddress(exocoreContracts, "withdrawPrecompileMock", withdrawMock); + vm.serializeAddress(exocoreContracts, "assetsPrecompileMock", assetsMock); vm.serializeAddress(exocoreContracts, "delegationPrecompileMock", delegationMock); vm.serializeAddress(exocoreContracts, "claimRewardPrecompileMock", claimRewardMock); } diff --git a/script/2_DeployBoth.s.sol b/script/2_DeployBoth.s.sol index f5e9824b..b3b60750 100644 --- a/script/2_DeployBoth.s.sol +++ b/script/2_DeployBoth.s.sol @@ -5,7 +5,7 @@ import "../src/core/ClientChainGateway.sol"; import "../src/core/ExoCapsule.sol"; import "../src/core/ExocoreGateway.sol"; import {Vault} from "../src/core/Vault.sol"; -import "../test/mocks/ExocoreGatewayMock.sol"; +import {ExocoreGatewayMock} from "../test/mocks/ExocoreGatewayMock.sol"; import {BaseScript} from "./BaseScript.sol"; import "@beacon-oracle/contracts/src/EigenLayerBeaconOracle.sol"; @@ -33,11 +33,8 @@ contract DeployScript is BaseScript { require(address(exocoreLzEndpoint) != address(0), "exocore l0 endpoint should not be empty"); if (useExocorePrecompileMock) { - depositMock = stdJson.readAddress(prerequisities, ".exocore.depositPrecompileMock"); - require(depositMock != address(0), "depositMock should not be empty"); - - withdrawMock = stdJson.readAddress(prerequisities, ".exocore.withdrawPrecompileMock"); - require(withdrawMock != address(0), "withdrawMock should not be empty"); + assetsMock = stdJson.readAddress(prerequisities, ".exocore.assetsPrecompileMock"); + require(assetsMock != address(0), "assetsMock should not be empty"); delegationMock = stdJson.readAddress(prerequisities, ".exocore.delegationPrecompileMock"); require(delegationMock != address(0), "delegationMock should not be empty"); @@ -72,8 +69,6 @@ contract DeployScript is BaseScript { // deploy BeaconProxyBytecode to store BeaconProxyBytecode beaconProxyBytecode = new BeaconProxyBytecode(); - whitelistTokens.push(address(restakeToken)); - /// deploy client chain gateway ProxyAdmin clientChainProxyAdmin = new ProxyAdmin(); ClientChainGateway clientGatewayLogic = new ClientChainGateway( @@ -91,7 +86,7 @@ contract DeployScript is BaseScript { address(clientGatewayLogic), address(clientChainProxyAdmin), abi.encodeWithSelector( - clientGatewayLogic.initialize.selector, payable(exocoreValidatorSet.addr), whitelistTokens + clientGatewayLogic.initialize.selector, payable(exocoreValidatorSet.addr) ) ) ) @@ -111,9 +106,8 @@ contract DeployScript is BaseScript { ProxyAdmin exocoreProxyAdmin = new ProxyAdmin(); if (useExocorePrecompileMock) { - ExocoreGatewayMock exocoreGatewayLogic = new ExocoreGatewayMock( - address(exocoreLzEndpoint), depositMock, withdrawMock, delegationMock, claimRewardMock - ); + ExocoreGatewayMock exocoreGatewayLogic = + new ExocoreGatewayMock(address(exocoreLzEndpoint), assetsMock, claimRewardMock, delegationMock); exocoreGateway = ExocoreGateway( payable( address( @@ -164,8 +158,7 @@ contract DeployScript is BaseScript { vm.serializeAddress(exocoreContracts, "exocoreGateway", address(exocoreGateway)); if (useExocorePrecompileMock) { - vm.serializeAddress(exocoreContracts, "depositPrecompileMock", depositMock); - vm.serializeAddress(exocoreContracts, "withdrawPrecompileMock", withdrawMock); + vm.serializeAddress(exocoreContracts, "assetsPrecompileMock", assetsMock); vm.serializeAddress(exocoreContracts, "delegationPrecompileMock", delegationMock); vm.serializeAddress(exocoreContracts, "claimRewardPrecompileMock", claimRewardMock); } diff --git a/script/3_Setup.s.sol b/script/3_Setup.s.sol index 483ff859..6fae4825 100644 --- a/script/3_Setup.s.sol +++ b/script/3_Setup.s.sol @@ -49,7 +49,7 @@ contract SetupScript is BaseScript { } function run() public { - // setup client chain contracts state + // 1. setup client chain contracts to make them ready for sending and receiving messages from exocore gateway vm.selectFork(clientChain); // Exocore validator set should be the owner of these contracts and only owner could setup contracts state vm.startBroadcast(exocoreValidatorSet.privateKey); @@ -60,12 +60,12 @@ contract SetupScript is BaseScript { ); } - // as LzReceivers, gateway should set bytes(sourceChainGatewayAddress+thisAddress) as trusted remote to receive - // messages + // as LzReceivers, client chain gateway should set exocoreGateway as trusted remote to receive messages from it clientGateway.setPeer(exocoreChainId, address(exocoreGateway).toBytes32()); vm.stopBroadcast(); - // setup Exocore testnet contracts state + // 2. setup Exocore contracts to make them ready for sending and receiving messages from client chain + // gateway vm.selectFork(exocore); // Exocore validator set should be the owner of these contracts and only owner could setup contracts state vm.startBroadcast(exocoreValidatorSet.privateKey); @@ -75,8 +75,17 @@ contract SetupScript is BaseScript { address(clientGateway), address(clientChainLzEndpoint) ); } + // this would also register clientChainId to Exocore native module exocoreGateway.setPeer(clientChainId, address(clientGateway).toBytes32()); vm.stopBroadcast(); + + // 3. we should register whitelist tokens to exocore + vm.selectFork(clientChain); + // Exocore validator set should be the owner of these contracts and only owner could add whitelist tokens + vm.startBroadcast(exocoreValidatorSet.privateKey); + whitelistTokens.push(address(restakeToken)); + clientGateway.addWhitelistTokens(whitelistTokens); + vm.stopBroadcast(); } } diff --git a/script/5_Withdraw.s.sol b/script/5_Withdraw.s.sol index c786111e..4cf6f467 100644 --- a/script/5_Withdraw.s.sol +++ b/script/5_Withdraw.s.sol @@ -5,10 +5,6 @@ import "../src/interfaces/IClientChainGateway.sol"; import "../src/interfaces/IExocoreGateway.sol"; import "../src/interfaces/IVault.sol"; -import "../src/interfaces/precompiles/IClaimReward.sol"; -import "../src/interfaces/precompiles/IDelegation.sol"; -import "../src/interfaces/precompiles/IDeposit.sol"; -import "../src/interfaces/precompiles/IWithdrawPrinciple.sol"; import "../src/storage/GatewayStorage.sol"; import {BaseScript} from "./BaseScript.sol"; diff --git a/script/6_CreateExoCapsule.s.sol b/script/6_CreateExoCapsule.s.sol index c72acbbf..3b0cc27a 100644 --- a/script/6_CreateExoCapsule.s.sol +++ b/script/6_CreateExoCapsule.s.sol @@ -5,10 +5,6 @@ import "../src/interfaces/IClientChainGateway.sol"; import "../src/interfaces/IExocoreGateway.sol"; import "../src/interfaces/IVault.sol"; -import "../src/interfaces/precompiles/IClaimReward.sol"; -import "../src/interfaces/precompiles/IDelegation.sol"; -import "../src/interfaces/precompiles/IDeposit.sol"; -import "../src/interfaces/precompiles/IWithdrawPrinciple.sol"; import "../src/storage/GatewayStorage.sol"; import {BaseScript} from "./BaseScript.sol"; diff --git a/script/BaseScript.sol b/script/BaseScript.sol index 045cddf0..bd647b9b 100644 --- a/script/BaseScript.sol +++ b/script/BaseScript.sol @@ -6,10 +6,9 @@ import "../src/interfaces/IExoCapsule.sol"; import "../src/interfaces/IExocoreGateway.sol"; import "../src/interfaces/IVault.sol"; +import "../src/interfaces/precompiles/IAssets.sol"; import "../src/interfaces/precompiles/IClaimReward.sol"; import "../src/interfaces/precompiles/IDelegation.sol"; -import "../src/interfaces/precompiles/IDeposit.sol"; -import "../src/interfaces/precompiles/IWithdrawPrinciple.sol"; import "@beacon-oracle/contracts/src/EigenLayerBeaconOracle.sol"; import "@layerzero-v2/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol"; @@ -33,6 +32,8 @@ contract BaseScript is Script { Player depositor; Player relayer; + string sepoliaRPCURL; + string holeskyRPCURL; string clientChainRPCURL; string exocoreRPCURL; @@ -53,8 +54,7 @@ contract BaseScript is Script { BeaconProxyBytecode beaconProxyBytecode; address delegationMock; - address depositMock; - address withdrawMock; + address assetsMock; address claimRewardMock; uint256 clientChain; @@ -94,7 +94,7 @@ contract BaseScript is Script { useExocorePrecompileMock = vm.envBool("USE_EXOCORE_PRECOMPILE_MOCK"); console.log("NOTICE: using exocore precompiles mock", useExocorePrecompileMock); - clientChainRPCURL = vm.envString("HOLESKY_RPC"); + clientChainRPCURL = vm.envString("CLIENT_CHAIN_RPC"); exocoreRPCURL = vm.envString("EXOCORE_TESETNET_RPC"); } @@ -119,15 +119,12 @@ contract BaseScript is Script { function _bindPrecompileMocks() internal { // bind precompile mock contracts code to constant precompile address so that local simulation could pass - bytes memory DepositMockCode = vm.getDeployedCode("DepositWithdrawMock.sol"); - vm.etch(DEPOSIT_PRECOMPILE_ADDRESS, DepositMockCode); + bytes memory AssetsMockCode = vm.getDeployedCode("AssetsMock.sol"); + vm.etch(ASSETS_PRECOMPILE_ADDRESS, AssetsMockCode); bytes memory DelegationMockCode = vm.getDeployedCode("DelegationMock.sol"); vm.etch(DELEGATION_PRECOMPILE_ADDRESS, DelegationMockCode); - bytes memory WithdrawPrincipleMockCode = vm.getDeployedCode("DepositWithdrawMock.sol"); - vm.etch(WITHDRAW_PRECOMPILE_ADDRESS, WithdrawPrincipleMockCode); - bytes memory WithdrawRewardMockCode = vm.getDeployedCode("ClaimRewardMock.sol"); vm.etch(CLAIM_REWARD_PRECOMPILE_ADDRESS, WithdrawRewardMockCode); } diff --git a/script/TestPrecompileErrorFixed.s.sol b/script/TestPrecompileErrorFixed.s.sol index 5c94b463..02672778 100644 --- a/script/TestPrecompileErrorFixed.s.sol +++ b/script/TestPrecompileErrorFixed.s.sol @@ -5,10 +5,9 @@ import "../src/interfaces/IClientChainGateway.sol"; import "../src/interfaces/IExocoreGateway.sol"; import "../src/interfaces/IVault.sol"; +import "../src/interfaces/precompiles/IAssets.sol"; import "../src/interfaces/precompiles/IClaimReward.sol"; import "../src/interfaces/precompiles/IDelegation.sol"; -import "../src/interfaces/precompiles/IDeposit.sol"; -import "../src/interfaces/precompiles/IWithdrawPrinciple.sol"; import "../src/storage/GatewayStorage.sol"; import {NonShortCircuitEndpointV2Mock} from "../test/mocks/NonShortCircuitEndpointV2Mock.sol"; @@ -59,16 +58,15 @@ contract DepositScript is BaseScript { } vm.stopBroadcast(); - // bind precompile mock contracts code to constant precompile address - bytes memory DepositMockCode = vm.getDeployedCode("DepositMock.sol"); - vm.etch(DEPOSIT_PRECOMPILE_ADDRESS, DepositMockCode); + // bind precompile mock contracts code to constant precompile address so that local simulation could pass + // Ensure that the address constants used here (ASSETS_PRECOMPILE_ADDRESS, etc.) are designated for testing and + // do not conflict with production addresses. + bytes memory AssetsMockCode = vm.getDeployedCode("AssetsMock.sol"); + vm.etch(ASSETS_PRECOMPILE_ADDRESS, AssetsMockCode); bytes memory DelegationMockCode = vm.getDeployedCode("DelegationMock.sol"); vm.etch(DELEGATION_PRECOMPILE_ADDRESS, DelegationMockCode); - bytes memory WithdrawPrincipleMockCode = vm.getDeployedCode("WithdrawPrincipleMock.sol"); - vm.etch(WITHDRAW_PRECOMPILE_ADDRESS, WithdrawPrincipleMockCode); - bytes memory WithdrawRewardMockCode = vm.getDeployedCode("ClaimRewardMock.sol"); vm.etch(CLAIM_REWARD_PRECOMPILE_ADDRESS, WithdrawRewardMockCode); } diff --git a/script/TestPrecompileErrorFixed_Deploy.s.sol b/script/TestPrecompileErrorFixed_Deploy.s.sol index b83efc99..4616ab02 100644 --- a/script/TestPrecompileErrorFixed_Deploy.s.sol +++ b/script/TestPrecompileErrorFixed_Deploy.s.sol @@ -5,10 +5,6 @@ import "../src/interfaces/IClientChainGateway.sol"; import "../src/interfaces/IExocoreGateway.sol"; import "../src/interfaces/IVault.sol"; -import "../src/interfaces/precompiles/IClaimReward.sol"; -import "../src/interfaces/precompiles/IDelegation.sol"; -import "../src/interfaces/precompiles/IDeposit.sol"; -import "../src/interfaces/precompiles/IWithdrawPrinciple.sol"; import "../src/storage/GatewayStorage.sol"; import {NonShortCircuitEndpointV2Mock} from "../test/mocks/NonShortCircuitEndpointV2Mock.sol"; diff --git a/script/TokenTransfer.s.sol b/script/TokenTransfer.s.sol index eed9ba1e..bfcd51aa 100644 --- a/script/TokenTransfer.s.sol +++ b/script/TokenTransfer.s.sol @@ -5,11 +5,6 @@ import "../src/core/ClientChainGateway.sol"; import "../src/core/ExocoreGateway.sol"; import {Vault} from "../src/core/Vault.sol"; -import "../src/interfaces/precompiles/IClaimReward.sol"; -import "../src/interfaces/precompiles/IDelegation.sol"; -import "../src/interfaces/precompiles/IDeposit.sol"; -import "../src/interfaces/precompiles/IWithdrawPrinciple.sol"; - import "../src/storage/GatewayStorage.sol"; import "@layerzero-contracts/interfaces/ILayerZeroEndpoint.sol"; import "@openzeppelin-contracts/contracts/proxy/transparent/ProxyAdmin.sol"; diff --git a/script/deployBeaconOracle.s.sol b/script/deployBeaconOracle.s.sol index 40afc341..8e418cec 100644 --- a/script/deployBeaconOracle.s.sol +++ b/script/deployBeaconOracle.s.sol @@ -6,9 +6,6 @@ import "@layerzero-v2/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol"; import {ERC20PresetFixedSupply} from "@openzeppelin-contracts/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; import "forge-std/Script.sol"; -import "test/mocks/ClaimRewardMock.sol"; -import "test/mocks/DelegationMock.sol"; -import "test/mocks/DepositWithdrawMock.sol"; import {NonShortCircuitEndpointV2Mock} from "test/mocks/NonShortCircuitEndpointV2Mock.sol"; contract PrerequisitiesScript is BaseScript { diff --git a/script/integration/1_DeployBootstrap.s.sol b/script/integration/1_DeployBootstrap.s.sol index 8a28d4eb..faca81f2 100644 --- a/script/integration/1_DeployBootstrap.s.sol +++ b/script/integration/1_DeployBootstrap.s.sol @@ -16,7 +16,7 @@ import {CustomProxyAdmin} from "../../src/core/CustomProxyAdmin.sol"; import {Vault} from "../../src/core/Vault.sol"; import {IOperatorRegistry} from "../../src/interfaces/IOperatorRegistry.sol"; import {IVault} from "../../src/interfaces/IVault.sol"; -import {MyToken} from "../../test/foundry/MyToken.sol"; +import {MyToken} from "../../test/foundry/unit/MyToken.sol"; // Technically this is used for testing but it is marked as a script // because it is a script that is used to deploy the contracts on Anvil diff --git a/src/core/BaseRestakingController.sol b/src/core/BaseRestakingController.sol index 403f4886..95a1cac2 100644 --- a/src/core/BaseRestakingController.sol +++ b/src/core/BaseRestakingController.sol @@ -29,14 +29,12 @@ abstract contract BaseRestakingController is if (token == VIRTUAL_STAKED_ETH_ADDRESS) { IExoCapsule capsule = _getCapsule(msg.sender); capsule.withdraw(amount, payable(recipient)); - - emit ClaimSucceeded(token, recipient, amount); } else { IVault vault = _getVault(token); vault.withdraw(msg.sender, recipient, amount); - - emit ClaimSucceeded(token, recipient, amount); } + + emit ClaimSucceeded(token, recipient, amount); } function delegateTo(string calldata operator, address token, uint256 amount) @@ -47,7 +45,10 @@ abstract contract BaseRestakingController is isValidBech32Address(operator) whenNotPaused { - _processRequest(token, msg.sender, amount, Action.REQUEST_DELEGATE_TO, operator); + bytes memory actionArgs = + abi.encodePacked(bytes32(bytes20(token)), bytes32(bytes20(msg.sender)), bytes(operator), amount); + bytes memory encodedRequest = abi.encode(token, msg.sender, operator, amount); + _processRequest(Action.REQUEST_DELEGATE_TO, actionArgs, encodedRequest); } function undelegateFrom(string calldata operator, address token, uint256 amount) @@ -58,37 +59,17 @@ abstract contract BaseRestakingController is isValidBech32Address(operator) whenNotPaused { - _processRequest(token, msg.sender, amount, Action.REQUEST_UNDELEGATE_FROM, operator); + bytes memory actionArgs = + abi.encodePacked(bytes32(bytes20(token)), bytes32(bytes20(msg.sender)), bytes(operator), amount); + bytes memory encodedRequest = abi.encode(token, msg.sender, operator, amount); + _processRequest(Action.REQUEST_UNDELEGATE_FROM, actionArgs, encodedRequest); } - function _processRequest( - address token, - address sender, - uint256 amount, - Action action, - string memory operator // Optional parameter, empty string when not needed. - ) internal { - if (token != VIRTUAL_STAKED_ETH_ADDRESS) { - IVault vault = _getVault(token); - if ((action == Action.REQUEST_DEPOSIT) || (action == Action.REQUEST_DEPOSIT_THEN_DELEGATE_TO)) { - // if there is a deposit, we should transfer the tokens to the vault. - vault.deposit(sender, amount); - } - } + function _processRequest(Action action, bytes memory actionArgs, bytes memory encodedRequest) internal { outboundNonce++; - bool hasOperator = bytes(operator).length > 0; - - // Use a single abi.encode call via ternary operators to handle both cases. - _registeredRequests[outboundNonce] = - hasOperator ? abi.encode(token, operator, sender, amount) : abi.encode(token, sender, amount); - + _registeredRequests[outboundNonce] = encodedRequest; _registeredRequestActions[outboundNonce] = action; - // Use a single abi.encodePacked call via ternary operators to handle both cases. - bytes memory actionArgs = hasOperator - ? abi.encodePacked(bytes32(bytes20(token)), bytes32(bytes20(sender)), bytes(operator), amount) - : abi.encodePacked(bytes32(bytes20(token)), bytes32(bytes20(sender)), amount); - _sendMsgToExocore(action, actionArgs); } diff --git a/src/core/Bootstrap.sol b/src/core/Bootstrap.sol index 2efe5807..ec5c674b 100644 --- a/src/core/Bootstrap.sol +++ b/src/core/Bootstrap.sol @@ -15,6 +15,8 @@ import {OAppCoreUpgradeable} from "../lzApp/OAppCoreUpgradeable.sol"; import {ICustomProxyAdmin} from "../interfaces/ICustomProxyAdmin.sol"; import {ILSTRestakingController} from "../interfaces/ILSTRestakingController.sol"; import {IOperatorRegistry} from "../interfaces/IOperatorRegistry.sol"; + +import {ITokenWhitelister} from "../interfaces/ITokenWhitelister.sol"; import {IVault} from "../interfaces/IVault.sol"; import {BootstrapStorage} from "../storage/BootstrapStorage.sol"; @@ -30,6 +32,7 @@ contract Bootstrap is PausableUpgradeable, OwnableUpgradeable, ReentrancyGuardUpgradeable, + ITokenWhitelister, ILSTRestakingController, IOperatorRegistry, BootstrapLzReceiver @@ -65,14 +68,7 @@ contract Bootstrap is offsetDuration = offsetDuration_; exocoreValidatorSetAddress = exocoreValidatorSetAddress_; - for (uint256 i = 0; i < whitelistTokens_.length; i++) { - address underlyingToken = whitelistTokens_[i]; - whitelistTokens.push(underlyingToken); - isWhitelistedToken[underlyingToken] = true; - emit WhitelistTokenAdded(underlyingToken); - - _deployVault(underlyingToken); - } + _addWhitelistTokens(whitelistTokens_); _whiteListFunctionSelectors[Action.REQUEST_MARK_BOOTSTRAP] = this.markBootstrapped.selector; @@ -158,20 +154,31 @@ contract Bootstrap is } // implementation of ITokenWhitelister - function addWhitelistToken(address _token) public override beforeLocked onlyOwner whenNotPaused nonReentrant { - super.addWhitelistToken(_token); + function addWhitelistTokens(address[] calldata tokens) external payable beforeLocked onlyOwner whenNotPaused { + _addWhitelistTokens(tokens); + } + + function _addWhitelistTokens(address[] calldata tokens) internal { + for (uint256 i; i < tokens.length; i++) { + address token = tokens[i]; + require(token != address(0), "Bootstrap: zero token address"); + require(!isWhitelistedToken[token], "Bootstrap: token should be not whitelisted before"); + + whitelistTokens.push(token); + isWhitelistedToken[token] = true; + + // deploy the corresponding vault if not deployed before + if (address(tokenToVault[token]) == address(0)) { + _deployVault(token); + } + + emit WhitelistTokenAdded(token); + } } // implementation of ITokenWhitelister - function removeWhitelistToken(address _token) - public - override - beforeLocked - onlyOwner - whenNotPaused - isTokenWhitelisted(_token) - { - super.removeWhitelistToken(_token); + function getWhitelistedTokensCount() external view returns (uint256) { + return whitelistTokens.length; } // implementation of IOperatorRegistry diff --git a/src/core/ClientChainGateway.sol b/src/core/ClientChainGateway.sol index b87b77a9..09542759 100644 --- a/src/core/ClientChainGateway.sol +++ b/src/core/ClientChainGateway.sol @@ -9,8 +9,8 @@ import {ClientChainGatewayStorage} from "../storage/ClientChainGatewayStorage.so import {ClientGatewayLzReceiver} from "./ClientGatewayLzReceiver.sol"; import {LSTRestakingController} from "./LSTRestakingController.sol"; import {NativeRestakingController} from "./NativeRestakingController.sol"; -import {IOAppCore} from "@layerzero-v2/oapp/contracts/oapp/interfaces/IOAppCore.sol"; +import {IOAppCore} from "@layerzero-v2/oapp/contracts/oapp/interfaces/IOAppCore.sol"; import {OptionsBuilder} from "@layerzero-v2/oapp/contracts/oapp/libs/OptionsBuilder.sol"; import {OwnableUpgradeable} from "@openzeppelin-upgradeable/contracts/access/OwnableUpgradeable.sol"; import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; @@ -58,10 +58,7 @@ contract ClientChainGateway is // initialization happens from another contract so it must be external. // reinitializer(2) is used so that the ownable and oappcore functions can be called again. - function initialize(address payable exocoreValidatorSetAddress_, address[] calldata appendedWhitelistTokens_) - external - reinitializer(2) - { + function initialize(address payable exocoreValidatorSetAddress_) external reinitializer(2) { _clearBootstrapData(); require( @@ -71,20 +68,6 @@ contract ClientChainGateway is exocoreValidatorSetAddress = exocoreValidatorSetAddress_; - for (uint256 i = 0; i < appendedWhitelistTokens_.length; i++) { - address underlyingToken = appendedWhitelistTokens_[i]; - require(!isWhitelistedToken[underlyingToken], "ClientChainGateway: token should not be whitelisted before"); - - whitelistTokens.push(underlyingToken); - isWhitelistedToken[underlyingToken] = true; - emit WhitelistTokenAdded(underlyingToken); - - // deploy the corresponding vault if not deployed before - if (address(tokenToVault[underlyingToken]) == address(0)) { - _deployVault(underlyingToken); - } - } - _registeredResponseHooks[Action.REQUEST_DEPOSIT] = this.afterReceiveDepositResponse.selector; _registeredResponseHooks[Action.REQUEST_WITHDRAW_PRINCIPLE_FROM_EXOCORE] = this.afterReceiveWithdrawPrincipleResponse.selector; @@ -94,6 +77,7 @@ contract ClientChainGateway is this.afterReceiveWithdrawRewardResponse.selector; _registeredResponseHooks[Action.REQUEST_DEPOSIT_THEN_DELEGATE_TO] = this.afterReceiveDepositThenDelegateToResponse.selector; + _registeredResponseHooks[Action.REQUEST_REGISTER_TOKENS] = this.afterReceiveRegisterTokensResponse.selector; bootstrapped = true; @@ -134,12 +118,30 @@ contract ClientChainGateway is _unpause(); } - function addWhitelistToken(address _token) public override onlyOwner whenNotPaused { - super.addWhitelistToken(_token); + // implementation of ITokenWhitelister + function addWhitelistTokens(address[] calldata tokens) external payable onlyOwner whenNotPaused { + _addWhitelistTokens(tokens); + } + + function _addWhitelistTokens(address[] calldata tokens) internal { + require(tokens.length <= type(uint8).max, "ClientChainGateway: tokens length should not execeed 255"); + + bytes memory actionArgs = abi.encodePacked(uint8(tokens.length)); + for (uint256 i; i < tokens.length; i++) { + address token = tokens[i]; + require(token != address(0), "ClientChainGateway: zero token address"); + require(!isWhitelistedToken[token], "ClientChainGateway: token should not be whitelisted before"); + + actionArgs = abi.encodePacked(actionArgs, bytes32(bytes20(token))); + } + + bytes memory encodedRequest = abi.encode(tokens); + _processRequest(Action.REQUEST_REGISTER_TOKENS, actionArgs, encodedRequest); } - function removeWhitelistToken(address _token) public override isTokenWhitelisted(_token) onlyOwner whenNotPaused { - super.removeWhitelistToken(_token); + // implementation of ITokenWhitelister + function getWhitelistedTokensCount() external view returns (uint256) { + return whitelistTokens.length; } function quote(bytes memory _message) public view returns (uint256 nativeFee) { diff --git a/src/core/ClientGatewayLzReceiver.sol b/src/core/ClientGatewayLzReceiver.sol index ef72eb47..6f520c04 100644 --- a/src/core/ClientGatewayLzReceiver.sol +++ b/src/core/ClientGatewayLzReceiver.sol @@ -148,8 +148,8 @@ abstract contract ClientGatewayLzReceiver is PausableUpgradeable, OAppReceiverUp public onlyCalledFromThis { - (address token, string memory operator, address delegator, uint256 amount) = - abi.decode(requestPayload, (address, string, address, uint256)); + (address token, address delegator, string memory operator, uint256 amount) = + abi.decode(requestPayload, (address, address, string, uint256)); bool success = (uint8(bytes1(responsePayload[0])) == 1); @@ -160,8 +160,8 @@ abstract contract ClientGatewayLzReceiver is PausableUpgradeable, OAppReceiverUp public onlyCalledFromThis { - (address token, string memory operator, address undelegator, uint256 amount) = - abi.decode(requestPayload, (address, string, address, uint256)); + (address token, address undelegator, string memory operator, uint256 amount) = + abi.decode(requestPayload, (address, address, string, uint256)); bool success = (uint8(bytes1(responsePayload[0])) == 1); @@ -172,8 +172,8 @@ abstract contract ClientGatewayLzReceiver is PausableUpgradeable, OAppReceiverUp public onlyCalledFromThis { - (address token, string memory operator, address delegator, uint256 amount) = - abi.decode(requestPayload, (address, string, address, uint256)); + (address token, address delegator, string memory operator, uint256 amount) = + abi.decode(requestPayload, (address, address, string, uint256)); bool delegateSuccess = (uint8(bytes1(responsePayload[0])) == 1); uint256 lastlyUpdatedPrincipleBalance = uint256(bytes32(responsePayload[1:])); @@ -189,4 +189,30 @@ abstract contract ClientGatewayLzReceiver is PausableUpgradeable, OAppReceiverUp emit DepositThenDelegateResult(delegateSuccess, delegator, operator, token, amount); } + function afterReceiveRegisterTokensResponse(bytes calldata requestPayload, bytes calldata responsePayload) + public + onlyCalledFromThis + whenNotPaused + { + address[] memory tokens = abi.decode(requestPayload, (address[])); + + bool success = (uint8(bytes1(responsePayload[0])) == 1); + if (success) { + for (uint256 i; i < tokens.length; i++) { + address token = tokens[i]; + isWhitelistedToken[token] = true; + whitelistTokens.push(token); + + // deploy the corresponding vault if not deployed before + if (address(tokenToVault[token]) == address(0)) { + _deployVault(token); + } + + emit WhitelistTokenAdded(token); + } + } + + emit RegisterTokensResult(success); + } + } diff --git a/src/core/ExocoreGateway.sol b/src/core/ExocoreGateway.sol index fc98e500..c3c2db6f 100644 --- a/src/core/ExocoreGateway.sol +++ b/src/core/ExocoreGateway.sol @@ -2,12 +2,9 @@ pragma solidity ^0.8.19; import {IExocoreGateway} from "../interfaces/IExocoreGateway.sol"; +import {ASSETS_CONTRACT, ASSETS_PRECOMPILE_ADDRESS} from "../interfaces/precompiles/IAssets.sol"; import {CLAIM_REWARD_CONTRACT, CLAIM_REWARD_PRECOMPILE_ADDRESS} from "../interfaces/precompiles/IClaimReward.sol"; - -import {CLIENT_CHAINS_PRECOMPILE_ADDRESS, IClientChains} from "../interfaces/precompiles/IClientChains.sol"; import {DELEGATION_CONTRACT, DELEGATION_PRECOMPILE_ADDRESS} from "../interfaces/precompiles/IDelegation.sol"; -import {DEPOSIT_CONTRACT} from "../interfaces/precompiles/IDeposit.sol"; -import {WITHDRAW_CONTRACT, WITHDRAW_PRECOMPILE_ADDRESS} from "../interfaces/precompiles/IWithdrawPrinciple.sol"; import { MessagingFee, @@ -18,6 +15,8 @@ import { } from "../lzApp/OAppUpgradeable.sol"; import {ExocoreGatewayStorage} from "../storage/ExocoreGatewayStorage.sol"; +import {OAppCoreUpgradeable} from "../lzApp/OAppCoreUpgradeable.sol"; +import {IOAppCore} from "@layerzero-v2/oapp/contracts/oapp/interfaces/IOAppCore.sol"; import {OptionsBuilder} from "@layerzero-v2/oapp/contracts/oapp/libs/OptionsBuilder.sol"; import {ILayerZeroReceiver} from "@layerzero-v2/protocol/contracts/interfaces/ILayerZeroReceiver.sol"; import {OwnableUpgradeable} from "@openzeppelin-upgradeable/contracts/access/OwnableUpgradeable.sol"; @@ -50,7 +49,10 @@ contract ExocoreGateway is receive() external payable {} function initialize(address payable exocoreValidatorSetAddress_) external initializer { - require(exocoreValidatorSetAddress_ != address(0), "ExocoreGateway: invalid exocore validator set address"); + require( + exocoreValidatorSetAddress_ != address(0), + "ExocoreGateway: validator set address cannot be the zero address" + ); exocoreValidatorSetAddress = exocoreValidatorSetAddress_; @@ -69,43 +71,78 @@ contract ExocoreGateway is _whiteListFunctionSelectors[Action.REQUEST_WITHDRAW_REWARD_FROM_EXOCORE] = this.requestWithdrawReward.selector; _whiteListFunctionSelectors[Action.REQUEST_DEPOSIT_THEN_DELEGATE_TO] = this.requestDepositThenDelegateTo.selector; + _whiteListFunctionSelectors[Action.REQUEST_REGISTER_TOKENS] = this.requestRegisterTokens.selector; + } + + function pause() external { + require( + msg.sender == exocoreValidatorSetAddress, + "ExocoreGateway: caller is not Exocore validator set aggregated address" + ); + _pause(); + } + + function unpause() external { + require( + msg.sender == exocoreValidatorSetAddress, + "ExocoreGateway: caller is not Exocore validator set aggregated address" + ); + _unpause(); } // TODO: call this function automatically, either within the initializer (which requires // setPeer) or be triggered by Golang after the contract is deployed. // For manual calls, this function should be called immediately after deployment and // then never needs to be called again. - function markBootstrapOnAllChains() public { + function markBootstrapOnAllChains() public whenNotPaused { (bool success, bytes memory result) = - CLIENT_CHAINS_PRECOMPILE_ADDRESS.staticcall(abi.encodeWithSelector(IClientChains.getClientChains.selector)); + ASSETS_PRECOMPILE_ADDRESS.staticcall(abi.encodeWithSelector(ASSETS_CONTRACT.getClientChains.selector)); require(success, "ExocoreGateway: failed to get client chain ids"); - // TODO: change to uint32[] when the precompile is upgraded - (bool ok, uint16[] memory clientChainIds) = abi.decode(result, (bool, uint16[])); + (bool ok, uint32[] memory clientChainIds) = abi.decode(result, (bool, uint32[])); require(ok, "ExocoreGateway: failed to decode client chain ids"); for (uint256 i = 0; i < clientChainIds.length; i++) { - uint16 clientChainId = clientChainIds[i]; + uint32 clientChainId = clientChainIds[i]; if (!chainToBootstrapped[clientChainId]) { - _sendInterchainMsg(uint32(clientChainId), Action.REQUEST_MARK_BOOTSTRAP, ""); + _sendInterchainMsg(clientChainId, Action.REQUEST_MARK_BOOTSTRAP, ""); // TODO: should this be marked only upon receiving a response? chainToBootstrapped[clientChainId] = true; } } } - function pause() external { - require( - msg.sender == exocoreValidatorSetAddress, - "ExocoreGateway: caller is not Exocore validator set aggregated address" - ); - _pause(); + /** + * @notice Sets the peer address (OApp instance) for a corresponding endpoint. This would also + * register the `cientChainId` to Exocore native module if the peer address is first time being set. + * @param clientChainId The endpoint ID for client chain. + * @param clientChainGateway The contract address to be associated with the corresponding endpoint. + * + * @dev Only the owner/admin of the OApp can call this function. + * @dev Indicates that the peer is trusted to send LayerZero messages to this OApp. + * @dev Peer is a bytes32 to accommodate non-evm chains. + */ + function setPeer(uint32 clientChainId, bytes32 clientChainGateway) + public + override(IOAppCore, OAppCoreUpgradeable) + onlyOwner + whenNotPaused + { + _validatePeer(clientChainId, clientChainGateway); + _registerClientChain(clientChainId); + super.setPeer(clientChainId, clientChainGateway); } - function unpause() external { - require( - msg.sender == exocoreValidatorSetAddress, - "ExocoreGateway: caller is not Exocore validator set aggregated address" - ); - _unpause(); + function _validatePeer(uint32 clientChainId, bytes32 clientChainGateway) internal pure { + require(clientChainId != uint32(0), "ExocoreGateway: zero value is not invalid endpoint id"); + require(clientChainGateway != bytes32(0), "ExocoreGateway: client chain gateway cannot be empty"); + } + + function _registerClientChain(uint32 clientChainId) internal { + if (peers[clientChainId] == bytes32(0)) { + bool success = ASSETS_CONTRACT.registerClientChain(clientChainId); + if (!success) { + revert RegisterClientChainToExocoreFailed(clientChainId); + } + } } function _lzReceive(Origin calldata _origin, bytes calldata payload) internal virtual override whenNotPaused { @@ -124,6 +161,30 @@ contract ExocoreGateway is } } + function requestRegisterTokens(uint32 srcChainId, uint64 lzNonce, bytes calldata payload) + public + onlyCalledFromThis + { + uint8 count = uint8(payload[0]); + uint256 expectedLength = count * TOKEN_ADDRESS_BYTES_LENTH + 1; + _validatePayloadLength(payload, expectedLength, Action.REQUEST_DEPOSIT); + + bytes[] memory tokens = new bytes[](count); + for (uint256 i; i < count; i++) { + uint256 start = i * TOKEN_ADDRESS_BYTES_LENTH + 1; + uint256 end = start + TOKEN_ADDRESS_BYTES_LENTH; + tokens[i] = payload[start:end]; + } + + try ASSETS_CONTRACT.registerTokens(srcChainId, tokens) returns (bool success) { + _sendInterchainMsg(srcChainId, Action.RESPOND, abi.encodePacked(lzNonce, success)); + } catch { + emit ExocorePrecompileError(ASSETS_PRECOMPILE_ADDRESS, lzNonce); + + _sendInterchainMsg(srcChainId, Action.RESPOND, abi.encodePacked(lzNonce, false)); + } + } + function requestDeposit(uint32 srcChainId, uint64 lzNonce, bytes calldata payload) public onlyCalledFromThis { _validatePayloadLength(payload, DEPOSIT_REQUEST_LENGTH, Action.REQUEST_DEPOSIT); @@ -131,7 +192,7 @@ contract ExocoreGateway is bytes calldata depositor = payload[32:64]; uint256 amount = uint256(bytes32(payload[64:96])); - (bool success, uint256 updatedBalance) = DEPOSIT_CONTRACT.depositTo(srcChainId, token, depositor, amount); + (bool success, uint256 updatedBalance) = ASSETS_CONTRACT.depositTo(srcChainId, token, depositor, amount); if (!success) { revert DepositRequestShouldNotFail(srcChainId, lzNonce); } @@ -151,12 +212,12 @@ contract ExocoreGateway is bytes calldata withdrawer = payload[32:64]; uint256 amount = uint256(bytes32(payload[64:96])); - try WITHDRAW_CONTRACT.withdrawPrinciple(srcChainId, token, withdrawer, amount) returns ( + try ASSETS_CONTRACT.withdrawPrinciple(srcChainId, token, withdrawer, amount) returns ( bool success, uint256 updatedBalance ) { _sendInterchainMsg(srcChainId, Action.RESPOND, abi.encodePacked(lzNonce, success, updatedBalance)); } catch { - emit ExocorePrecompileError(WITHDRAW_PRECOMPILE_ADDRESS, lzNonce); + emit ExocorePrecompileError(ASSETS_PRECOMPILE_ADDRESS, lzNonce); _sendInterchainMsg(srcChainId, Action.RESPOND, abi.encodePacked(lzNonce, false, uint256(0))); } @@ -240,7 +301,7 @@ contract ExocoreGateway is // for example, you cannot index a bytes memory result from the requestDepositTo call, // if you were to modify it to return bytes and then process them here. - (bool success, uint256 updatedBalance) = DEPOSIT_CONTRACT.depositTo(srcChainId, token, depositor, amount); + (bool success, uint256 updatedBalance) = ASSETS_CONTRACT.depositTo(srcChainId, token, depositor, amount); if (!success) { revert DepositRequestShouldNotFail(srcChainId, lzNonce); } diff --git a/src/core/LSTRestakingController.sol b/src/core/LSTRestakingController.sol index bf194ce6..adbd5615 100644 --- a/src/core/LSTRestakingController.sol +++ b/src/core/LSTRestakingController.sol @@ -1,6 +1,8 @@ pragma solidity ^0.8.19; import {ILSTRestakingController} from "../interfaces/ILSTRestakingController.sol"; + +import {IVault} from "../interfaces/IVault.sol"; import {BaseRestakingController} from "./BaseRestakingController.sol"; import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/PausableUpgradeable.sol"; @@ -14,7 +16,13 @@ abstract contract LSTRestakingController is PausableUpgradeable, ILSTRestakingCo isValidAmount(amount) whenNotPaused { - _processRequest(token, msg.sender, amount, Action.REQUEST_DEPOSIT, ""); + IVault vault = _getVault(token); + vault.deposit(msg.sender, amount); + + bytes memory actionArgs = abi.encodePacked(bytes32(bytes20(token)), bytes32(bytes20(msg.sender)), amount); + bytes memory encodedRequest = abi.encode(token, msg.sender, amount); + + _processRequest(Action.REQUEST_DEPOSIT, actionArgs, encodedRequest); } function withdrawPrincipleFromExocore(address token, uint256 principleAmount) @@ -24,7 +32,11 @@ abstract contract LSTRestakingController is PausableUpgradeable, ILSTRestakingCo isValidAmount(principleAmount) whenNotPaused { - _processRequest(token, msg.sender, principleAmount, Action.REQUEST_WITHDRAW_PRINCIPLE_FROM_EXOCORE, ""); + bytes memory actionArgs = + abi.encodePacked(bytes32(bytes20(token)), bytes32(bytes20(msg.sender)), principleAmount); + bytes memory encodedRequest = abi.encode(token, msg.sender, principleAmount); + + _processRequest(Action.REQUEST_WITHDRAW_PRINCIPLE_FROM_EXOCORE, actionArgs, encodedRequest); } function withdrawRewardFromExocore(address token, uint256 rewardAmount) @@ -34,7 +46,9 @@ abstract contract LSTRestakingController is PausableUpgradeable, ILSTRestakingCo isValidAmount(rewardAmount) whenNotPaused { - _processRequest(token, msg.sender, rewardAmount, Action.REQUEST_WITHDRAW_REWARD_FROM_EXOCORE, ""); + bytes memory actionArgs = abi.encodePacked(bytes32(bytes20(token)), bytes32(bytes20(msg.sender)), rewardAmount); + bytes memory encodedRequest = abi.encode(token, msg.sender, rewardAmount); + _processRequest(Action.REQUEST_WITHDRAW_REWARD_FROM_EXOCORE, actionArgs, encodedRequest); } // implementation of ILSTRestakingController @@ -47,7 +61,13 @@ abstract contract LSTRestakingController is PausableUpgradeable, ILSTRestakingCo isValidBech32Address(operator) whenNotPaused { - _processRequest(token, msg.sender, amount, Action.REQUEST_DEPOSIT_THEN_DELEGATE_TO, operator); + IVault vault = _getVault(token); + vault.deposit(msg.sender, amount); + + bytes memory actionArgs = + abi.encodePacked(bytes32(bytes20(token)), bytes32(bytes20(msg.sender)), bytes(operator), amount); + bytes memory encodedRequest = abi.encode(token, msg.sender, operator, amount); + _processRequest(Action.REQUEST_DEPOSIT_THEN_DELEGATE_TO, actionArgs, encodedRequest); } } diff --git a/src/core/NativeRestakingController.sol b/src/core/NativeRestakingController.sol index f221093a..5789fa30 100644 --- a/src/core/NativeRestakingController.sol +++ b/src/core/NativeRestakingController.sol @@ -63,7 +63,11 @@ abstract contract NativeRestakingController is capsule.verifyDepositProof(validatorContainer, proof); uint256 depositValue = uint256(validatorContainer.getEffectiveBalance()) * GWEI_TO_WEI; - _processRequest(VIRTUAL_STAKED_ETH_ADDRESS, msg.sender, depositValue, Action.REQUEST_DEPOSIT, ""); + + bytes memory actionArgs = + abi.encodePacked(bytes32(bytes20(VIRTUAL_STAKED_ETH_ADDRESS)), bytes32(bytes20(msg.sender)), depositValue); + bytes memory encodedRequest = abi.encode(VIRTUAL_STAKED_ETH_ADDRESS, msg.sender, depositValue); + _processRequest(Action.REQUEST_DEPOSIT, actionArgs, encodedRequest); } function processBeaconChainPartialWithdrawal( diff --git a/src/interfaces/IClientChainGateway.sol b/src/interfaces/IClientChainGateway.sol index a71f5a46..95acfab9 100644 --- a/src/interfaces/IClientChainGateway.sol +++ b/src/interfaces/IClientChainGateway.sol @@ -1,11 +1,20 @@ pragma solidity ^0.8.19; import {INativeRestakingController} from "../interfaces/INativeRestakingController.sol"; + +import {ITokenWhitelister} from "../interfaces/ITokenWhitelister.sol"; import {ILSTRestakingController} from "./ILSTRestakingController.sol"; + import {IOAppCore} from "@layerzero-v2/oapp/contracts/oapp/interfaces/IOAppCore.sol"; import {IOAppReceiver} from "@layerzero-v2/oapp/contracts/oapp/interfaces/IOAppReceiver.sol"; -interface IClientChainGateway is IOAppReceiver, IOAppCore, ILSTRestakingController, INativeRestakingController { +interface IClientChainGateway is + ITokenWhitelister, + IOAppReceiver, + IOAppCore, + ILSTRestakingController, + INativeRestakingController +{ function quote(bytes memory _message) external view returns (uint256 nativeFee); diff --git a/src/interfaces/ITokenWhitelister.sol b/src/interfaces/ITokenWhitelister.sol index 7463917a..8931ebf6 100644 --- a/src/interfaces/ITokenWhitelister.sol +++ b/src/interfaces/ITokenWhitelister.sol @@ -2,25 +2,7 @@ pragma solidity ^0.8.19; interface ITokenWhitelister { - function addWhitelistToken(address _token) external; - function removeWhitelistToken(address _token) external; + function addWhitelistTokens(address[] calldata tokens) external payable; function getWhitelistedTokensCount() external returns (uint256); - /** - * @dev Emitted when a new token is added to the whitelist. - * @param _token The address of the token that has been added to the whitelist. - */ - event WhitelistTokenAdded(address _token); - - /** - * @dev Emitted when a token is removed from the whitelist. - * @param _token The address of the token that has been removed from the whitelist. - */ - event WhitelistTokenRemoved(address _token); - - /** - * @dev Indicates an operation was attempted with a token that is not authorized. - */ - error UnauthorizedToken(); - } diff --git a/src/interfaces/precompiles/IAssets.sol b/src/interfaces/precompiles/IAssets.sol new file mode 100644 index 00000000..9a9ac722 --- /dev/null +++ b/src/interfaces/precompiles/IAssets.sol @@ -0,0 +1,57 @@ +pragma solidity >=0.8.17; + +/// @dev The Assets contract's address. +address constant ASSETS_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000804; + +/// @dev The Assets contract's instance. +IAssets constant ASSETS_CONTRACT = IAssets(ASSETS_PRECOMPILE_ADDRESS); + +/// @author Exocore Team +/// @title Assets Precompile Contract +/// @dev The interface through which solidity contracts will interact with assets module +/// @custom:address 0x0000000000000000000000000000000000000804 +interface IAssets { + + /// TRANSACTIONS + /// @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 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) + 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 assetsAddress The client chain asset Address + /// @param withdrawAddress The withdraw address + /// @param opAmount The withdraw amount + function withdrawPrinciple( + uint32 clientChainLzID, + 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. + /// @param clientChainLzID The LzID of client chain + function registerClientChain(uint32 clientChainLzID) external returns (bool success); + + /// TRANSACTIONS + /// @dev register unwhitelisted token addresses to exocore + /// @param clientChainLzID The LzID of client chain + /// @param tokens The token addresses that would be registered to exocore + function registerTokens(uint32 clientChainLzID, bytes[] memory tokens) external returns (bool success); + +} diff --git a/src/interfaces/precompiles/IClaimReward.sol b/src/interfaces/precompiles/IClaimReward.sol index ba6f112a..b8b59944 100644 --- a/src/interfaces/precompiles/IClaimReward.sol +++ b/src/interfaces/precompiles/IClaimReward.sol @@ -1,5 +1,8 @@ pragma solidity >=0.8.17; +/// TODO: we might remove this precompile contract and merge it into assets precompile +/// if we decide to handle reward withdrawal request by assets precompile + /// @dev The claimReward contract's address. address constant CLAIM_REWARD_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000806; diff --git a/src/interfaces/precompiles/IClientChains.sol b/src/interfaces/precompiles/IClientChains.sol deleted file mode 100644 index cf14974a..00000000 --- a/src/interfaces/precompiles/IClientChains.sol +++ /dev/null @@ -1,18 +0,0 @@ -pragma solidity >=0.8.17; - -/// @dev The CLIENT_CHAINS contract's address. -address constant CLIENT_CHAINS_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000801; - -/// @dev The CLIENT_CHAINS contract's instance. -IClientChains constant CLIENT_CHAINS_CONTRACT = IClientChains(CLIENT_CHAINS_PRECOMPILE_ADDRESS); - -/// @author Exocore Team -/// @title Client Chains Precompile Contract -/// @dev The interface through which solidity contracts will interact with ClientChains -/// @custom:address 0x0000000000000000000000000000000000000801 -interface IClientChains { - - /// @dev Returns the chain indices of the client chains. - function getClientChains() external view returns (bool, uint16[] memory); - -} diff --git a/src/interfaces/precompiles/IDeposit.sol b/src/interfaces/precompiles/IDeposit.sol deleted file mode 100644 index 44a06c24..00000000 --- a/src/interfaces/precompiles/IDeposit.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -pragma solidity >=0.8.17; - -/// @dev The DEPOSIT contract's address. -address constant DEPOSIT_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000804; - -/// @dev The DEPOSIT contract's instance. -IDeposit constant DEPOSIT_CONTRACT = IDeposit(DEPOSIT_PRECOMPILE_ADDRESS); - -/// @author Exocore Team -/// @title Deposit Precompile Contract -/// @dev The interface through which solidity contracts will interact with Deposit -/// @custom:address 0x0000000000000000000000000000000000000804 -interface IDeposit { - - /// TRANSACTIONS - /// @dev deposit the client chain assets to 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 assetsAddress The client chain asset Address - /// @param stakerAddress The staker address - /// @param opAmount The deposit amount - function depositTo(uint32 clientChainLzId, bytes memory assetsAddress, bytes memory stakerAddress, uint256 opAmount) - external - returns (bool success, uint256 latestAssetState); - -} diff --git a/src/interfaces/precompiles/IWithdrawPrinciple.sol b/src/interfaces/precompiles/IWithdrawPrinciple.sol deleted file mode 100644 index d3346af2..00000000 --- a/src/interfaces/precompiles/IWithdrawPrinciple.sol +++ /dev/null @@ -1,29 +0,0 @@ -pragma solidity >=0.8.17; - -/// @dev The WITHDRAW contract's address. -address constant WITHDRAW_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000808; - -/// @dev The WITHDRAW contract's instance. -IWithdraw constant WITHDRAW_CONTRACT = IWithdraw(WITHDRAW_PRECOMPILE_ADDRESS); - -/// @author Exocore Team -/// @title WITHDRAW Precompile Contract -/// @dev The interface through which solidity contracts will interact with WITHDRAW -/// @custom:address 0x0000000000000000000000000000000000000808 -interface IWithdraw { - - /// 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 assetsAddress The client chain asset Address - /// @param withdrawAddress The withdraw address - /// @param opAmount The withdraw amount - function withdrawPrinciple( - uint32 clientChainLzId, - bytes memory assetsAddress, - bytes memory withdrawAddress, - uint256 opAmount - ) external returns (bool success, uint256 latestAssetState); - -} diff --git a/src/storage/BootstrapStorage.sol b/src/storage/BootstrapStorage.sol index 1f2e7b0e..36f1bc42 100644 --- a/src/storage/BootstrapStorage.sol +++ b/src/storage/BootstrapStorage.sol @@ -4,7 +4,6 @@ import {BeaconProxyBytecode} from "../core/BeaconProxyBytecode.sol"; import {Vault} from "../core/Vault.sol"; import {IOperatorRegistry} from "../interfaces/IOperatorRegistry.sol"; -import {ITokenWhitelister} from "../interfaces/ITokenWhitelister.sol"; import {IVault} from "../interfaces/IVault.sol"; import {GatewayStorage} from "./GatewayStorage.sol"; import {IBeacon} from "@openzeppelin-contracts/contracts/proxy/beacon/IBeacon.sol"; @@ -14,7 +13,7 @@ import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; // prior to ClientChainGateway. ClientChainStorage should inherit from // BootstrapStorage to ensure overlap of positioning between the // members of each contract. -contract BootstrapStorage is GatewayStorage, ITokenWhitelister { +contract BootstrapStorage is GatewayStorage { /* -------------------------------------------------------------------------- */ /* state variables exclusively owned by Bootstrap */ @@ -359,6 +358,12 @@ contract BootstrapStorage is GatewayStorage, ITokenWhitelister { */ event VaultCreated(address underlyingToken, address vault); + /** + * @dev Emitted when a new token is added to the whitelist. + * @param _token The address of the token that has been added to the whitelist. + */ + event WhitelistTokenAdded(address _token); + /* -------------------------------------------------------------------------- */ /* Errors */ /* -------------------------------------------------------------------------- */ @@ -480,39 +485,4 @@ contract BootstrapStorage is GatewayStorage, ITokenWhitelister { return vault; } - // implementation of ITokenWhitelister - function addWhitelistToken(address _token) public virtual override { - require(!isWhitelistedToken[_token], "BootstrapStorage: token should be not whitelisted before"); - whitelistTokens.push(_token); - isWhitelistedToken[_token] = true; - - // deploy the corresponding vault if not deployed before - if (address(tokenToVault[_token]) == address(0)) { - _deployVault(_token); - } - - emit WhitelistTokenAdded(_token); - } - - // implementation of ITokenWhitelister - function removeWhitelistToken(address _token) public virtual override { - isWhitelistedToken[_token] = false; - // the implicit assumption here is that the _token must be included in whitelistTokens - // if isWhitelistedToken[_token] is true - for (uint256 i = 0; i < whitelistTokens.length; i++) { - if (whitelistTokens[i] == _token) { - whitelistTokens[i] = whitelistTokens[whitelistTokens.length - 1]; - whitelistTokens.pop(); - break; - } - } - - emit WhitelistTokenRemoved(_token); - } - - // implementation of ITokenWhitelister - function getWhitelistedTokensCount() external view returns (uint256) { - return whitelistTokens.length; - } - } diff --git a/src/storage/ClientChainGatewayStorage.sol b/src/storage/ClientChainGatewayStorage.sol index 0b5445d1..5e3afaf7 100644 --- a/src/storage/ClientChainGatewayStorage.sol +++ b/src/storage/ClientChainGatewayStorage.sol @@ -43,6 +43,7 @@ contract ClientChainGatewayStorage is BootstrapStorage { /* ----------------------------- restaking ----------------------------- */ event ClaimSucceeded(address token, address recipient, uint256 amount); event WithdrawRewardResult(bool indexed success, address indexed token, address indexed withdrawer, uint256 amount); + event RegisterTokensResult(bool indexed success); /* -------------------------------------------------------------------------- */ /* Errors */ diff --git a/src/storage/ExocoreGatewayStorage.sol b/src/storage/ExocoreGatewayStorage.sol index 304b926b..175d1b2c 100644 --- a/src/storage/ExocoreGatewayStorage.sol +++ b/src/storage/ExocoreGatewayStorage.sol @@ -16,12 +16,13 @@ contract ExocoreGatewayStorage is GatewayStorage { uint256 internal constant CLAIM_REWARD_REQUEST_LENGTH = 96; // bytes32 token + bytes32 delegator + bytes(42) operator + uint256 amount uint256 internal constant DEPOSIT_THEN_DELEGATE_REQUEST_LENGTH = DELEGATE_REQUEST_LENGTH; + uint256 internal constant TOKEN_ADDRESS_BYTES_LENTH = 32; uint128 internal constant DESTINATION_GAS_LIMIT = 500_000; uint128 internal constant DESTINATION_MSG_VALUE = 0; mapping(uint32 eid => mapping(bytes32 sender => uint64 nonce)) public inboundNonce; - mapping(uint16 id => bool) public chainToBootstrapped; + mapping(uint32 id => bool) public chainToBootstrapped; event ExocorePrecompileError(address indexed precompile, uint64 nonce); @@ -29,6 +30,7 @@ contract ExocoreGatewayStorage is GatewayStorage { error PrecompileCallFailed(bytes4 selector_, bytes reason); error InvalidRequestLength(Action act, uint256 expectedLength, uint256 actualLength); error DepositRequestShouldNotFail(uint32 srcChainId, uint64 lzNonce); + error RegisterClientChainToExocoreFailed(uint32 clientChainId); uint256[40] private __gap; diff --git a/src/storage/GatewayStorage.sol b/src/storage/GatewayStorage.sol index be2cc6c3..d1b1cc89 100644 --- a/src/storage/GatewayStorage.sol +++ b/src/storage/GatewayStorage.sol @@ -10,11 +10,11 @@ contract GatewayStorage { REQUEST_UNDELEGATE_FROM, REQUEST_DEPOSIT_THEN_DELEGATE_TO, REQUEST_MARK_BOOTSTRAP, - RESPOND, - UPDATE_USERS_BALANCES + REQUEST_REGISTER_TOKENS, + RESPOND } - mapping(Action => bytes4) public _whiteListFunctionSelectors; + mapping(Action => bytes4) internal _whiteListFunctionSelectors; address payable public exocoreValidatorSetAddress; event MessageSent(Action indexed act, bytes32 packetId, uint64 nonce, uint256 nativeFee); diff --git a/test/foundry/ClientChainGateway.t.sol b/test/foundry/ClientChainGateway.t.sol deleted file mode 100644 index a34dc285..00000000 --- a/test/foundry/ClientChainGateway.t.sol +++ /dev/null @@ -1,184 +0,0 @@ -pragma solidity ^0.8.19; - -import "@beacon-oracle/contracts/src/EigenLayerBeaconOracle.sol"; -import "@openzeppelin-contracts/contracts/proxy/beacon/IBeacon.sol"; -import "@openzeppelin-contracts/contracts/proxy/beacon/UpgradeableBeacon.sol"; -import "@openzeppelin-contracts/contracts/proxy/transparent/ProxyAdmin.sol"; -import "@openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; -import "@openzeppelin-contracts/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; - -import "forge-std/Test.sol"; -import "forge-std/console.sol"; - -import "../../src/core/ClientChainGateway.sol"; - -import "../../src/core/ExoCapsule.sol"; -import "../../src/core/ExocoreGateway.sol"; -import {Vault} from "../../src/core/Vault.sol"; - -import "../../src/interfaces/IExoCapsule.sol"; -import "../../src/interfaces/IVault.sol"; -import "../../src/interfaces/precompiles/IDelegation.sol"; -import "../../src/interfaces/precompiles/IDeposit.sol"; -import "../../src/interfaces/precompiles/IWithdrawPrinciple.sol"; -import {EndpointV2Mock} from "../mocks/EndpointV2Mock.sol"; - -import "src/core/BeaconProxyBytecode.sol"; - -contract SetUp is Test { - - Player[] players; - address[] whitelistTokens; - Player exocoreValidatorSet; - Player deployer; - address[] vaults; - ERC20PresetFixedSupply restakeToken; - - ClientChainGateway clientGateway; - ExocoreGateway exocoreGateway; - EndpointV2Mock clientChainLzEndpoint; - EndpointV2Mock exocoreLzEndpoint; - IBeaconChainOracle beaconOracle; - IVault vaultImplementation; - IExoCapsule capsuleImplementation; - IBeacon vaultBeacon; - IBeacon capsuleBeacon; - BeaconProxyBytecode beaconProxyBytecode; - - string operatorAddress = "exo1v4s6vtjpmxwu9rlhqms5urzrc3tc2ae2gnuqhc"; - uint16 exocoreChainId = 2; - uint16 clientChainId = 1; - - struct Player { - uint256 privateKey; - address addr; - } - - event Paused(address account); - event Unpaused(address account); - - error EnforcedPause(); - error ExpectedPause(); - - function setUp() public virtual { - players.push(Player({privateKey: uint256(0x1), addr: vm.addr(uint256(0x1))})); - players.push(Player({privateKey: uint256(0x2), addr: vm.addr(uint256(0x2))})); - players.push(Player({privateKey: uint256(0x3), addr: vm.addr(uint256(0x3))})); - exocoreValidatorSet = Player({privateKey: uint256(0xa), addr: vm.addr(uint256(0xa))}); - deployer = Player({privateKey: uint256(0xb), addr: vm.addr(uint256(0xb))}); - - vm.chainId(clientChainId); - _deploy(); - } - - function _deploy() internal { - vm.startPrank(deployer.addr); - - beaconOracle = IBeaconChainOracle(_deployBeaconOracle()); - - vaultImplementation = new Vault(); - capsuleImplementation = new ExoCapsule(); - - vaultBeacon = new UpgradeableBeacon(address(vaultImplementation)); - capsuleBeacon = new UpgradeableBeacon(address(capsuleImplementation)); - - beaconProxyBytecode = new BeaconProxyBytecode(); - - restakeToken = new ERC20PresetFixedSupply("rest", "rest", 1e16, exocoreValidatorSet.addr); - whitelistTokens.push(address(restakeToken)); - - clientChainLzEndpoint = new EndpointV2Mock(clientChainId); - ProxyAdmin proxyAdmin = new ProxyAdmin(); - ClientChainGateway clientGatewayLogic = new ClientChainGateway( - address(clientChainLzEndpoint), - exocoreChainId, - address(beaconOracle), - address(vaultBeacon), - address(capsuleBeacon), - address(beaconProxyBytecode) - ); - clientGateway = ClientChainGateway( - payable(address(new TransparentUpgradeableProxy(address(clientGatewayLogic), address(proxyAdmin), ""))) - ); - - clientGateway.initialize(payable(exocoreValidatorSet.addr), whitelistTokens); - - vm.stopPrank(); - } - - function _deployBeaconOracle() internal returns (EigenLayerBeaconOracle) { - uint256 GENESIS_BLOCK_TIMESTAMP; - - // mainnet - if (block.chainid == 1) { - GENESIS_BLOCK_TIMESTAMP = 1_606_824_023; - // goerli - } else if (block.chainid == 5) { - GENESIS_BLOCK_TIMESTAMP = 1_616_508_000; - // sepolia - } else if (block.chainid == 11_155_111) { - GENESIS_BLOCK_TIMESTAMP = 1_655_733_600; - // holesky - } else if (block.chainid == 17_000) { - GENESIS_BLOCK_TIMESTAMP = 1_695_902_400; - } else { - revert("Unsupported chainId."); - } - - EigenLayerBeaconOracle oracle = new EigenLayerBeaconOracle(GENESIS_BLOCK_TIMESTAMP); - return oracle; - } - -} - -contract Pausable is SetUp { - - function test_PauseClientChainGateway() public { - vm.expectEmit(true, true, true, true, address(clientGateway)); - emit Paused(exocoreValidatorSet.addr); - vm.prank(exocoreValidatorSet.addr); - clientGateway.pause(); - assertEq(clientGateway.paused(), true); - } - - function test_UnpauseClientChainGateway() public { - vm.startPrank(exocoreValidatorSet.addr); - - vm.expectEmit(true, true, true, true, address(clientGateway)); - emit Paused(exocoreValidatorSet.addr); - clientGateway.pause(); - assertEq(clientGateway.paused(), true); - - vm.expectEmit(true, true, true, true, address(clientGateway)); - emit Unpaused(exocoreValidatorSet.addr); - clientGateway.unpause(); - assertEq(clientGateway.paused(), false); - } - - function test_RevertWhen_UnauthorizedPauser() public { - vm.expectRevert("ClientChainGateway: caller is not Exocore validator set aggregated address"); - vm.startPrank(deployer.addr); - clientGateway.pause(); - } - - function test_RevertWhen_CallDisabledFunctionsWhenPaused() public { - vm.startPrank(exocoreValidatorSet.addr); - clientGateway.pause(); - - vm.expectRevert(EnforcedPause.selector); - clientGateway.claim(address(restakeToken), uint256(1), deployer.addr); - - vm.expectRevert(EnforcedPause.selector); - clientGateway.delegateTo(operatorAddress, address(restakeToken), uint256(1)); - - vm.expectRevert(EnforcedPause.selector); - clientGateway.deposit(address(restakeToken), uint256(1)); - - vm.expectRevert(EnforcedPause.selector); - clientGateway.withdrawPrincipleFromExocore(address(restakeToken), uint256(1)); - - vm.expectRevert(EnforcedPause.selector); - clientGateway.undelegateFrom(operatorAddress, address(restakeToken), uint256(1)); - } - -} diff --git a/test/foundry/Delegation.t.sol b/test/foundry/Delegation.t.sol index 9a82d996..11867ad1 100644 --- a/test/foundry/Delegation.t.sol +++ b/test/foundry/Delegation.t.sol @@ -20,8 +20,10 @@ contract DelegateTest is ExocoreDeployer { uint256 constant DEFAULT_ENDPOINT_CALL_GAS_LIMIT = 200_000; - event NewPacket(uint32, address, bytes32, uint64, bytes); - event MessageSent(GatewayStorage.Action indexed act, bytes32 packetId, uint64 nonce, uint256 nativeFee); + Player delegator; + Player relayer; + + string operatorAddress; event DelegateResult( bool indexed success, address indexed delegator, string indexed delegatee, address token, uint256 amount @@ -46,49 +48,55 @@ contract DelegateTest is ExocoreDeployer { uint256 opAmount ); - function test_Delegation() public { - Player memory delegator = players[0]; - Player memory relayer = players[1]; - string memory operatorAddress = "exo13hasr43vvq8v44xpzh0l6yuym4kca98f87j7ac"; + function setUp() public override { + super.setUp(); + delegator = players[0]; + relayer = players[1]; + + operatorAddress = "exo13hasr43vvq8v44xpzh0l6yuym4kca98f87j7ac"; + } + + function test_Delegation() public { deal(delegator.addr, 1e22); deal(address(exocoreGateway), 1e22); uint256 delegateAmount = 10_000; - _testDelegate(delegator.addr, relayer.addr, operatorAddress, delegateAmount); + // before delegate we should add whitelist tokens + test_AddWhitelistTokens(); + + _testDelegate(delegateAmount); } function test_Undelegation() public { - Player memory delegator = players[0]; - Player memory relayer = players[1]; - string memory operatorAddress = "exo13hasr43vvq8v44xpzh0l6yuym4kca98f87j7ac"; - deal(delegator.addr, 1e22); deal(address(exocoreGateway), 1e22); uint256 delegateAmount = 10_000; uint256 undelegateAmount = 5000; - _testDelegate(delegator.addr, relayer.addr, operatorAddress, delegateAmount); - _testUndelegate(delegator.addr, relayer.addr, operatorAddress, undelegateAmount); + // before undelegate we should add whitelist tokens + test_AddWhitelistTokens(); + + _testDelegate(delegateAmount); + _testUndelegate(undelegateAmount); } - function _testDelegate(address delegator, address relayer, string memory operator, uint256 delegateAmount) - internal - { + function _testDelegate(uint256 delegateAmount) internal { /* ------------------------- delegate workflow test ------------------------- */ // 1. first user call client chain gateway to delegate /// estimate the messaging fee that would be charged from user + uint64 delegateRequestNonce = 2; bytes memory delegateRequestPayload = abi.encodePacked( GatewayStorage.Action.REQUEST_DELEGATE_TO, abi.encodePacked(bytes32(bytes20(address(restakeToken)))), - abi.encodePacked(bytes32(bytes20(delegator))), - bytes(operator), + abi.encodePacked(bytes32(bytes20(delegator.addr))), + bytes(operatorAddress), delegateAmount ); uint256 requestNativeFee = clientGateway.quote(delegateRequestPayload); - bytes32 requestId = generateUID(uint64(1), true); + bytes32 requestId = generateUID(delegateRequestNonce, true); /// layerzero endpoint should emit the message packet including delegate payload. vm.expectEmit(true, true, true, true, address(clientChainLzEndpoint)); @@ -96,34 +104,36 @@ contract DelegateTest is ExocoreDeployer { exocoreChainId, address(clientGateway), address(exocoreGateway).toBytes32(), - uint64(1), + delegateRequestNonce, delegateRequestPayload ); /// clientGateway should emit MessageSent event vm.expectEmit(true, true, true, true, address(clientGateway)); - emit MessageSent(GatewayStorage.Action.REQUEST_DELEGATE_TO, requestId, uint64(1), requestNativeFee); + emit MessageSent(GatewayStorage.Action.REQUEST_DELEGATE_TO, requestId, delegateRequestNonce, requestNativeFee); /// delegator call clientGateway to send delegation request - vm.startPrank(delegator); - clientGateway.delegateTo{value: requestNativeFee}(operator, address(restakeToken), delegateAmount); + vm.startPrank(delegator.addr); + clientGateway.delegateTo{value: requestNativeFee}(operatorAddress, address(restakeToken), delegateAmount); vm.stopPrank(); // 2. second layerzero relayers should watch the request message packet and relay the message to destination // endpoint - bytes memory delegateResponsePayload = abi.encodePacked(GatewayStorage.Action.RESPOND, uint64(1), true); + uint64 delegateResponseNonce = 2; + bytes memory delegateResponsePayload = + abi.encodePacked(GatewayStorage.Action.RESPOND, delegateRequestNonce, true); uint256 responseNativeFee = exocoreGateway.quote(clientChainId, delegateResponsePayload); - bytes32 responseId = generateUID(uint64(1), false); + bytes32 responseId = generateUID(delegateResponseNonce, false); /// DelegationMock contract should receive correct message payload vm.expectEmit(true, true, true, true, DELEGATION_PRECOMPILE_ADDRESS); emit DelegateRequestProcessed( - uint16(clientChainId), - uint64(1), + clientChainId, + delegateRequestNonce, abi.encodePacked(bytes32(bytes20(address(restakeToken)))), - abi.encodePacked(bytes32(bytes20(delegator))), - operator, + abi.encodePacked(bytes32(bytes20(delegator.addr))), + operatorAddress, delegateAmount ); @@ -133,18 +143,18 @@ contract DelegateTest is ExocoreDeployer { clientChainId, address(exocoreGateway), address(clientGateway).toBytes32(), - uint64(1), + delegateResponseNonce, delegateResponsePayload ); /// exocoreGateway should emit MessageSent event after finishing sending response vm.expectEmit(true, true, true, true, address(exocoreGateway)); - emit MessageSent(GatewayStorage.Action.RESPOND, responseId, uint64(1), responseNativeFee); + emit MessageSent(GatewayStorage.Action.RESPOND, responseId, delegateResponseNonce, responseNativeFee); /// relayer call layerzero endpoint to deliver request messages and generate response message - vm.startPrank(relayer); + vm.startPrank(relayer.addr); exocoreLzEndpoint.lzReceive( - Origin(clientChainId, address(clientGateway).toBytes32(), uint64(1)), + Origin(clientChainId, address(clientGateway).toBytes32(), delegateRequestNonce), address(exocoreGateway), requestId, delegateRequestPayload, @@ -154,7 +164,7 @@ contract DelegateTest is ExocoreDeployer { /// assert that DelegationMock contract should have recorded the delegate uint256 actualDelegateAmount = DelegationMock(DELEGATION_PRECOMPILE_ADDRESS).getDelegateAmount( - delegator, operator, clientChainId, address(restakeToken) + delegator.addr, operatorAddress, clientChainId, address(restakeToken) ); assertEq(actualDelegateAmount, delegateAmount); @@ -164,12 +174,12 @@ contract DelegateTest is ExocoreDeployer { /// after relayer relay the response message back to client chain, clientGateway should emit DelegateResult /// event vm.expectEmit(true, true, true, true, address(clientGateway)); - emit DelegateResult(true, delegator, operator, address(restakeToken), delegateAmount); + emit DelegateResult(true, delegator.addr, operatorAddress, address(restakeToken), delegateAmount); /// relayer should watch the response message and relay it back to client chain - vm.startPrank(relayer); + vm.startPrank(relayer.addr); clientChainLzEndpoint.lzReceive( - Origin(exocoreChainId, address(exocoreGateway).toBytes32(), uint64(1)), + Origin(exocoreChainId, address(exocoreGateway).toBytes32(), delegateResponseNonce), address(clientGateway), responseId, delegateResponsePayload, @@ -178,27 +188,26 @@ contract DelegateTest is ExocoreDeployer { vm.stopPrank(); } - function _testUndelegate(address delegator, address relayer, string memory operator, uint256 undelegateAmount) - internal - { + function _testUndelegate(uint256 undelegateAmount) internal { /* ------------------------- undelegate workflow test ------------------------- */ uint256 totalDelegate = DelegationMock(DELEGATION_PRECOMPILE_ADDRESS).getDelegateAmount( - delegator, operator, clientChainId, address(restakeToken) + delegator.addr, operatorAddress, clientChainId, address(restakeToken) ); require(undelegateAmount <= totalDelegate, "undelegate amount overflow"); // 1. first user call client chain gateway to undelegate /// estimate the messaging fee that would be charged from user + uint64 undelegateRequestNonce = 3; bytes memory undelegateRequestPayload = abi.encodePacked( GatewayStorage.Action.REQUEST_UNDELEGATE_FROM, abi.encodePacked(bytes32(bytes20(address(restakeToken)))), - abi.encodePacked(bytes32(bytes20(delegator))), - bytes(operator), + abi.encodePacked(bytes32(bytes20(delegator.addr))), + bytes(operatorAddress), undelegateAmount ); uint256 requestNativeFee = clientGateway.quote(undelegateRequestPayload); - bytes32 requestId = generateUID(2, true); + bytes32 requestId = generateUID(undelegateRequestNonce, true); /// layerzero endpoint should emit the message packet including undelegate payload. vm.expectEmit(true, true, true, true, address(clientChainLzEndpoint)); @@ -206,34 +215,38 @@ contract DelegateTest is ExocoreDeployer { exocoreChainId, address(clientGateway), address(exocoreGateway).toBytes32(), - uint64(2), + undelegateRequestNonce, undelegateRequestPayload ); /// clientGateway should emit MessageSent event vm.expectEmit(true, true, true, true, address(clientGateway)); - emit MessageSent(GatewayStorage.Action.REQUEST_UNDELEGATE_FROM, requestId, uint64(2), requestNativeFee); + emit MessageSent( + GatewayStorage.Action.REQUEST_UNDELEGATE_FROM, requestId, undelegateRequestNonce, requestNativeFee + ); /// delegator call clientGateway to send undelegation request - vm.startPrank(delegator); - clientGateway.undelegateFrom{value: requestNativeFee}(operator, address(restakeToken), undelegateAmount); + vm.startPrank(delegator.addr); + clientGateway.undelegateFrom{value: requestNativeFee}(operatorAddress, address(restakeToken), undelegateAmount); vm.stopPrank(); // 2. second layerzero relayers should watch the request message packet and relay the message to destination // endpoint - bytes memory undelegateResponsePayload = abi.encodePacked(GatewayStorage.Action.RESPOND, uint64(2), true); + uint64 undelegateResponseNonce = 3; + bytes memory undelegateResponsePayload = + abi.encodePacked(GatewayStorage.Action.RESPOND, undelegateRequestNonce, true); uint256 responseNativeFee = exocoreGateway.quote(clientChainId, undelegateResponsePayload); - bytes32 responseId = generateUID(2, false); + bytes32 responseId = generateUID(undelegateResponseNonce, false); /// DelegationMock contract should receive correct message payload vm.expectEmit(true, true, true, true, DELEGATION_PRECOMPILE_ADDRESS); emit UndelegateRequestProcessed( - uint16(clientChainId), - uint64(2), + clientChainId, + undelegateRequestNonce, abi.encodePacked(bytes32(bytes20(address(restakeToken)))), - abi.encodePacked(bytes32(bytes20(delegator))), - operator, + abi.encodePacked(bytes32(bytes20(delegator.addr))), + operatorAddress, undelegateAmount ); @@ -243,28 +256,27 @@ contract DelegateTest is ExocoreDeployer { clientChainId, address(exocoreGateway), address(clientGateway).toBytes32(), - uint64(2), + undelegateResponseNonce, undelegateResponsePayload ); /// exocoreGateway should emit MessageSent event after finishing sending response vm.expectEmit(true, true, true, true, address(exocoreGateway)); - emit MessageSent(GatewayStorage.Action.RESPOND, responseId, uint64(2), responseNativeFee); + emit MessageSent(GatewayStorage.Action.RESPOND, responseId, undelegateResponseNonce, responseNativeFee); /// relayer call layerzero endpoint to deliver request messages and generate response message - vm.startPrank(relayer); + vm.startPrank(relayer.addr); exocoreLzEndpoint.lzReceive( - Origin(clientChainId, address(clientGateway).toBytes32(), uint64(2)), + Origin(clientChainId, address(clientGateway).toBytes32(), undelegateRequestNonce), address(exocoreGateway), requestId, undelegateRequestPayload, bytes("") ); vm.stopPrank(); - /// assert that DelegationMock contract should have recorded the undelegation uint256 actualDelegateAmount = DelegationMock(DELEGATION_PRECOMPILE_ADDRESS).getDelegateAmount( - delegator, operator, clientChainId, address(restakeToken) + delegator.addr, operatorAddress, clientChainId, address(restakeToken) ); assertEq(actualDelegateAmount, totalDelegate - undelegateAmount); @@ -274,12 +286,12 @@ contract DelegateTest is ExocoreDeployer { /// after relayer relay the response message back to client chain, clientGateway should emit UndelegateResult /// event vm.expectEmit(true, true, true, true, address(clientGateway)); - emit UndelegateResult(true, delegator, operator, address(restakeToken), undelegateAmount); + emit UndelegateResult(true, delegator.addr, operatorAddress, address(restakeToken), undelegateAmount); /// relayer should watch the response message and relay it back to client chain - vm.startPrank(relayer); + vm.startPrank(relayer.addr); clientChainLzEndpoint.lzReceive( - Origin(exocoreChainId, address(exocoreGateway).toBytes32(), uint64(2)), + Origin(exocoreChainId, address(exocoreGateway).toBytes32(), undelegateResponseNonce), address(clientGateway), responseId, undelegateResponsePayload, diff --git a/test/foundry/DepositThenDelegateTo.t.sol b/test/foundry/DepositThenDelegateTo.t.sol index 93f3e240..175d8570 100644 --- a/test/foundry/DepositThenDelegateTo.t.sol +++ b/test/foundry/DepositThenDelegateTo.t.sol @@ -4,8 +4,9 @@ import "../../src/core/ExocoreGateway.sol"; import "../../src/interfaces/precompiles/IDelegation.sol"; import "../../src/storage/GatewayStorage.sol"; + +import "../mocks/AssetsMock.sol"; import "../mocks/DelegationMock.sol"; -import {DepositMock} from "../mocks/DepositMock.sol"; import "./ExocoreDeployer.t.sol"; import {OptionsBuilder} from "@layerzero-v2/oapp/contracts/oapp/libs/OptionsBuilder.sol"; @@ -20,10 +21,6 @@ contract DepositThenDelegateToTest is ExocoreDeployer { using AddressCast for address; - // layer zero events - event NewPacket(uint32, address, bytes32, uint64, bytes); - event MessageSent(GatewayStorage.Action indexed act, bytes32 packetId, uint64 nonce, uint256 nativeFee); - // ClientChainGateway emits this when receiving the response event DepositThenDelegateResult( bool indexed delegateSuccess, @@ -51,9 +48,12 @@ contract DepositThenDelegateToTest is ExocoreDeployer { deal(delegator, 1e22); deal(address(exocoreGateway), 1e22); - uint64 lzNonce = 1; + uint64 lzNonce = 2; uint256 delegateAmount = 10_000; + // before all operations we should add whitelist tokens + test_AddWhitelistTokens(); + // ensure there is enough balance vm.startPrank(exocoreValidatorSet.addr); restakeToken.transfer(delegator, delegateAmount); @@ -141,7 +141,7 @@ contract DepositThenDelegateToTest is ExocoreDeployer { clientChainId, address(exocoreGateway), address(clientGateway).toBytes32(), - uint64(1), // outbound nonce not inbound, only equals because it's the first tx + lzNonce, // outbound nonce not inbound, only equals because it's the first tx responsePayload ); @@ -158,7 +158,7 @@ contract DepositThenDelegateToTest is ExocoreDeployer { ); vm.stopPrank(); - uint256 actualDepositAmount = DepositMock(DEPOSIT_PRECOMPILE_ADDRESS).principleBalances( + uint256 actualDepositAmount = AssetsMock(ASSETS_PRECOMPILE_ADDRESS).getPrincipleBalance( clientChainId, // weirdly, the address(x).toBytes32() did not work here. // for reference, the results are diff --git a/test/foundry/DepositWithdrawPrinciple.t.sol b/test/foundry/DepositWithdrawPrinciple.t.sol index 58b48b51..06ccf6da 100644 --- a/test/foundry/DepositWithdrawPrinciple.t.sol +++ b/test/foundry/DepositWithdrawPrinciple.t.sol @@ -22,9 +22,7 @@ contract DepositWithdrawPrincipleTest is ExocoreDeployer { bool indexed success, address indexed token, address indexed withdrawer, uint256 amount ); event Transfer(address indexed from, address indexed to, uint256 amount); - event MessageSent(GatewayStorage.Action indexed act, bytes32 packetId, uint64 nonce, uint256 nativeFee); event MessageProcessed(uint32 _srcChainId, bytes _srcAddress, uint64 _nonce, bytes _payload); - event NewPacket(uint32, address, bytes32, uint64, bytes); event CapsuleCreated(address owner, address capsule); event StakedWithCapsule(address staker, address capsule); @@ -43,8 +41,22 @@ contract DepositWithdrawPrincipleTest is ExocoreDeployer { deal(address(exocoreGateway), 1e22); uint256 depositAmount = 10_000; + uint256 withdrawAmount = 100; uint256 lastlyUpdatedPrincipleBalance; + // before deposit we should add whitelist tokens + test_AddWhitelistTokens(); + + _testLSTDeposit(depositor, depositAmount, lastlyUpdatedPrincipleBalance); + + lastlyUpdatedPrincipleBalance += depositAmount; + + _testLSTWithdraw(depositor, withdrawAmount, lastlyUpdatedPrincipleBalance); + } + + function _testLSTDeposit(Player memory depositor, uint256 depositAmount, uint256 lastlyUpdatedPrincipleBalance) + internal + { // -- deposit workflow test -- vm.startPrank(depositor.addr); @@ -53,6 +65,7 @@ contract DepositWithdrawPrincipleTest is ExocoreDeployer { // first user call client chain gateway to deposit // estimate l0 relay fee that the user should pay + uint64 depositRequestNonce = 2; bytes memory depositRequestPayload = abi.encodePacked( GatewayStorage.Action.REQUEST_DEPOSIT, bytes32(bytes20(address(restakeToken))), @@ -60,7 +73,7 @@ contract DepositWithdrawPrincipleTest is ExocoreDeployer { depositAmount ); uint256 depositRequestNativeFee = clientGateway.quote(depositRequestPayload); - bytes32 depositRequestId = generateUID(1, true); + bytes32 depositRequestId = generateUID(depositRequestNonce, true); // depositor should transfer deposited token to vault vm.expectEmit(true, true, false, true, address(restakeToken)); emit Transfer(depositor.addr, address(vault), depositAmount); @@ -70,12 +83,14 @@ contract DepositWithdrawPrincipleTest is ExocoreDeployer { exocoreChainId, address(clientGateway), address(exocoreGateway).toBytes32(), - uint64(1), + depositRequestNonce, depositRequestPayload ); // client chain gateway should emit MessageSent event vm.expectEmit(true, true, true, true, address(clientGateway)); - emit MessageSent(GatewayStorage.Action.REQUEST_DEPOSIT, depositRequestId, uint64(1), depositRequestNativeFee); + emit MessageSent( + GatewayStorage.Action.REQUEST_DEPOSIT, depositRequestId, depositRequestNonce, depositRequestNativeFee + ); clientGateway.deposit{value: depositRequestNativeFee}(address(restakeToken), depositAmount); // second layerzero relayers should watch the request message packet and relay the message to destination @@ -83,23 +98,26 @@ contract DepositWithdrawPrincipleTest is ExocoreDeployer { // exocore gateway should return response message to exocore network layerzero endpoint vm.expectEmit(true, true, true, true, address(exocoreLzEndpoint)); - lastlyUpdatedPrincipleBalance = depositAmount; + lastlyUpdatedPrincipleBalance += depositAmount; + uint64 depositResponseNonce = 2; bytes memory depositResponsePayload = - abi.encodePacked(GatewayStorage.Action.RESPOND, uint64(1), true, lastlyUpdatedPrincipleBalance); + abi.encodePacked(GatewayStorage.Action.RESPOND, depositRequestNonce, true, lastlyUpdatedPrincipleBalance); uint256 depositResponseNativeFee = exocoreGateway.quote(clientChainId, depositResponsePayload); - bytes32 depositResponseId = generateUID(1, false); + bytes32 depositResponseId = generateUID(depositResponseNonce, false); emit NewPacket( clientChainId, address(exocoreGateway), address(clientGateway).toBytes32(), - uint64(1), + depositResponseNonce, depositResponsePayload ); // exocore gateway should emit MessageSent event vm.expectEmit(true, true, true, true, address(exocoreGateway)); - emit MessageSent(GatewayStorage.Action.RESPOND, depositResponseId, uint64(1), depositResponseNativeFee); + emit MessageSent( + GatewayStorage.Action.RESPOND, depositResponseId, depositResponseNonce, depositResponseNativeFee + ); exocoreLzEndpoint.lzReceive( - Origin(clientChainId, address(clientGateway).toBytes32(), uint64(1)), + Origin(clientChainId, address(clientGateway).toBytes32(), depositRequestNonce), address(exocoreGateway), depositRequestId, depositRequestPayload, @@ -113,35 +131,38 @@ contract DepositWithdrawPrincipleTest is ExocoreDeployer { vm.expectEmit(true, true, true, true, address(clientGateway)); emit DepositResult(true, address(restakeToken), depositor.addr, depositAmount); clientChainLzEndpoint.lzReceive( - Origin(exocoreChainId, address(exocoreGateway).toBytes32(), uint64(1)), + Origin(exocoreChainId, address(exocoreGateway).toBytes32(), depositResponseNonce), address(clientGateway), depositResponseId, depositResponsePayload, bytes("") ); + } + function _testLSTWithdraw(Player memory withdrawer, uint256 withdrawAmount, uint256 lastlyUpdatedPrincipleBalance) + internal + { // -- withdraw principle workflow -- - uint256 withdrawAmount = 100; - // first user call client chain gateway to withdraw // estimate l0 relay fee that the user should pay + uint64 withdrawRequestNonce = 3; bytes memory withdrawRequestPayload = abi.encodePacked( GatewayStorage.Action.REQUEST_WITHDRAW_PRINCIPLE_FROM_EXOCORE, bytes32(bytes20(address(restakeToken))), - bytes32(bytes20(depositor.addr)), + bytes32(bytes20(withdrawer.addr)), withdrawAmount ); uint256 withdrawRequestNativeFee = clientGateway.quote(withdrawRequestPayload); - bytes32 withdrawRequestId = generateUID(2, true); + bytes32 withdrawRequestId = generateUID(withdrawRequestNonce, true); // client chain layerzero endpoint should emit the message packet including withdraw payload. vm.expectEmit(true, true, true, true, address(clientChainLzEndpoint)); emit NewPacket( exocoreChainId, address(clientGateway), address(exocoreGateway).toBytes32(), - uint64(2), + withdrawRequestNonce, withdrawRequestPayload ); // client chain gateway should emit MessageSent event @@ -149,7 +170,7 @@ contract DepositWithdrawPrincipleTest is ExocoreDeployer { emit MessageSent( GatewayStorage.Action.REQUEST_WITHDRAW_PRINCIPLE_FROM_EXOCORE, withdrawRequestId, - uint64(2), + withdrawRequestNonce, withdrawRequestNativeFee ); clientGateway.withdrawPrincipleFromExocore{value: withdrawRequestNativeFee}( @@ -159,25 +180,29 @@ contract DepositWithdrawPrincipleTest is ExocoreDeployer { // second layerzero relayers should watch the request message packet and relay the message to destination // endpoint - // exocore gateway should return response message to exocore network layerzero endpoint - vm.expectEmit(true, true, true, true, address(exocoreLzEndpoint)); + uint64 withdrawResponseNonce = 3; lastlyUpdatedPrincipleBalance -= withdrawAmount; bytes memory withdrawResponsePayload = - abi.encodePacked(GatewayStorage.Action.RESPOND, uint64(2), true, lastlyUpdatedPrincipleBalance); + abi.encodePacked(GatewayStorage.Action.RESPOND, withdrawRequestNonce, true, lastlyUpdatedPrincipleBalance); uint256 withdrawResponseNativeFee = exocoreGateway.quote(clientChainId, withdrawResponsePayload); - bytes32 withdrawResponseId = generateUID(2, false); + bytes32 withdrawResponseId = generateUID(withdrawResponseNonce, false); + + // exocore gateway should return response message to exocore network layerzero endpoint + vm.expectEmit(true, true, true, true, address(exocoreLzEndpoint)); emit NewPacket( clientChainId, address(exocoreGateway), address(clientGateway).toBytes32(), - uint64(2), + withdrawResponseNonce, withdrawResponsePayload ); // exocore gateway should emit MessageSent event vm.expectEmit(true, true, true, true, address(exocoreGateway)); - emit MessageSent(GatewayStorage.Action.RESPOND, withdrawResponseId, uint64(2), withdrawResponseNativeFee); + emit MessageSent( + GatewayStorage.Action.RESPOND, withdrawResponseId, withdrawResponseNonce, withdrawResponseNativeFee + ); exocoreLzEndpoint.lzReceive( - Origin(clientChainId, address(clientGateway).toBytes32(), uint64(2)), + Origin(clientChainId, address(clientGateway).toBytes32(), withdrawRequestNonce), address(exocoreGateway), withdrawRequestId, withdrawRequestPayload, @@ -189,9 +214,9 @@ contract DepositWithdrawPrincipleTest is ExocoreDeployer { // client chain gateway should execute the response hook and emit depositResult event vm.expectEmit(true, true, true, true, address(clientGateway)); - emit WithdrawPrincipleResult(true, address(restakeToken), depositor.addr, withdrawAmount); + emit WithdrawPrincipleResult(true, address(restakeToken), withdrawer.addr, withdrawAmount); clientChainLzEndpoint.lzReceive( - Origin(exocoreChainId, address(exocoreGateway).toBytes32(), uint64(2)), + Origin(exocoreChainId, address(exocoreGateway).toBytes32(), withdrawResponseNonce), address(clientGateway), withdrawResponseId, withdrawResponsePayload, @@ -203,6 +228,8 @@ contract DepositWithdrawPrincipleTest is ExocoreDeployer { Player memory depositor = players[0]; Player memory relayer = players[1]; + uint256 lastlyUpdatedPrincipleBalance; + // transfer some ETH to depositor for staking and paying for gas fee deal(depositor.addr, 1e22); // transfer some gas fee to relayer for paying for onboarding cross-chain message packet @@ -211,25 +238,17 @@ contract DepositWithdrawPrincipleTest is ExocoreDeployer { // sending back response deal(address(exocoreGateway), 1e22); - // before native stake and deposit, we simulate proper block environment states to make proof valid - - /// we set the timestamp of proof to be exactly the timestamp that the validator container get activated on - /// beacon chain - uint256 activationTimestamp = - BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) * SECONDS_PER_EPOCH; - mockProofTimestamp = activationTimestamp; - validatorProof.beaconBlockTimestamp = mockProofTimestamp; + // before deposit we should add whitelist tokens + test_AddWhitelistTokens(); - /// we set current block timestamp to be exactly one slot after the proof generation timestamp - mockCurrentBlockTimestamp = mockProofTimestamp + SECONDS_PER_SLOT; - vm.warp(mockCurrentBlockTimestamp); + _testNativeDeposit(depositor, relayer, lastlyUpdatedPrincipleBalance); + } - /// we mock the call beaconOracle.timestampToBlockRoot to return the expected block root in proof file - vm.mockCall( - address(beaconOracle), - abi.encodeWithSelector(beaconOracle.timestampToBlockRoot.selector), - abi.encode(beaconBlockRoot) - ); + function _testNativeDeposit(Player memory depositor, Player memory relayer, uint256 lastlyUpdatedPrincipleBalance) + internal + { + // before native stake and deposit, we simulate proper block environment states to make proof valid + _simulateBlockEnvironment(); // 1. firstly depositor should stake to beacon chain by depositing 32 ETH to ETHPOS contract ExoCapsule expectedCapsule = ExoCapsule( @@ -269,6 +288,7 @@ contract DepositWithdrawPrincipleTest is ExocoreDeployer { // through layerzero /// client chain layerzero endpoint should emit the message packet including deposit payload. + uint64 depositRequestNonce = 2; uint256 depositAmount = uint256(_getEffectiveBalance(validatorContainer)) * GWEI_TO_WEI; bytes memory depositRequestPayload = abi.encodePacked( GatewayStorage.Action.REQUEST_DEPOSIT, @@ -277,20 +297,22 @@ contract DepositWithdrawPrincipleTest is ExocoreDeployer { depositAmount ); uint256 depositRequestNativeFee = clientGateway.quote(depositRequestPayload); - bytes32 depositRequestId = generateUID(1, true); + bytes32 depositRequestId = generateUID(depositRequestNonce, true); vm.expectEmit(true, true, true, true, address(clientChainLzEndpoint)); emit NewPacket( exocoreChainId, address(clientGateway), address(exocoreGateway).toBytes32(), - uint64(1), + depositRequestNonce, depositRequestPayload ); /// client chain gateway should emit MessageSent event vm.expectEmit(true, true, true, true, address(clientGateway)); - emit MessageSent(GatewayStorage.Action.REQUEST_DEPOSIT, depositRequestId, uint64(1), depositRequestNativeFee); + emit MessageSent( + GatewayStorage.Action.REQUEST_DEPOSIT, depositRequestId, depositRequestNonce, depositRequestNativeFee + ); /// call depositBeaconChainValidator to see if these events are emitted as expected vm.startPrank(depositor.addr); @@ -301,29 +323,32 @@ contract DepositWithdrawPrincipleTest is ExocoreDeployer { // endpoint /// exocore gateway should return response message to exocore network layerzero endpoint - uint256 lastlyUpdatedPrincipleBalance = depositAmount; + uint64 depositResponseNonce = 2; + lastlyUpdatedPrincipleBalance += depositAmount; bytes memory depositResponsePayload = - abi.encodePacked(GatewayStorage.Action.RESPOND, uint64(1), true, lastlyUpdatedPrincipleBalance); + abi.encodePacked(GatewayStorage.Action.RESPOND, depositRequestNonce, true, lastlyUpdatedPrincipleBalance); uint256 depositResponseNativeFee = exocoreGateway.quote(clientChainId, depositResponsePayload); - bytes32 depositResponseId = generateUID(1, false); + bytes32 depositResponseId = generateUID(depositResponseNonce, false); vm.expectEmit(true, true, true, true, address(exocoreLzEndpoint)); emit NewPacket( clientChainId, address(exocoreGateway), address(clientGateway).toBytes32(), - uint64(1), + depositResponseNonce, depositResponsePayload ); /// exocore gateway should emit MessageSent event vm.expectEmit(true, true, true, true, address(exocoreGateway)); - emit MessageSent(GatewayStorage.Action.RESPOND, depositResponseId, uint64(1), depositResponseNativeFee); + emit MessageSent( + GatewayStorage.Action.RESPOND, depositResponseId, depositResponseNonce, depositResponseNativeFee + ); /// relayer catches the request message packet by listening to client chain event and feed it to Exocore network vm.startPrank(relayer.addr); exocoreLzEndpoint.lzReceive( - Origin(clientChainId, address(clientGateway).toBytes32(), uint64(1)), + Origin(clientChainId, address(clientGateway).toBytes32(), depositRequestNonce), address(exocoreGateway), depositRequestId, depositRequestPayload, @@ -341,7 +366,7 @@ contract DepositWithdrawPrincipleTest is ExocoreDeployer { /// relayer catches the response message packet by listening to Exocore event and feed it to client chain vm.startPrank(relayer.addr); clientChainLzEndpoint.lzReceive( - Origin(exocoreChainId, address(exocoreGateway).toBytes32(), uint64(1)), + Origin(exocoreChainId, address(exocoreGateway).toBytes32(), depositResponseNonce), address(clientGateway), depositResponseId, depositResponsePayload, @@ -350,4 +375,24 @@ contract DepositWithdrawPrincipleTest is ExocoreDeployer { vm.stopPrank(); } + function _simulateBlockEnvironment() internal { + /// we set the timestamp of proof to be exactly the timestamp that the validator container get activated on + /// beacon chain + uint256 activationTimestamp = + BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) * SECONDS_PER_EPOCH; + mockProofTimestamp = activationTimestamp; + validatorProof.beaconBlockTimestamp = mockProofTimestamp; + + /// we set current block timestamp to be exactly one slot after the proof generation timestamp + mockCurrentBlockTimestamp = mockProofTimestamp + SECONDS_PER_SLOT; + vm.warp(mockCurrentBlockTimestamp); + + /// we mock the call beaconOracle.timestampToBlockRoot to return the expected block root in proof file + vm.mockCall( + address(beaconOracle), + abi.encodeWithSelector(beaconOracle.timestampToBlockRoot.selector), + abi.encode(beaconBlockRoot) + ); + } + } diff --git a/test/foundry/ExocoreDeployer.t.sol b/test/foundry/ExocoreDeployer.t.sol index 236d4a8f..fb5ccb6f 100644 --- a/test/foundry/ExocoreDeployer.t.sol +++ b/test/foundry/ExocoreDeployer.t.sol @@ -9,6 +9,7 @@ import "@openzeppelin-contracts/contracts/proxy/beacon/UpgradeableBeacon.sol"; import "@openzeppelin-contracts/contracts/proxy/transparent/ProxyAdmin.sol"; import "@openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import "@openzeppelin-contracts/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; +import "@openzeppelin/contracts/utils/Create2.sol"; import "forge-std/Test.sol"; import "forge-std/console.sol"; @@ -18,13 +19,13 @@ import "../../src/core/ClientChainGateway.sol"; import "../../src/core/ExoCapsule.sol"; import "../../src/core/ExocoreGateway.sol"; import {Vault} from "../../src/core/Vault.sol"; +import "src/storage/GatewayStorage.sol"; import {IVault} from "../../src/interfaces/IVault.sol"; +import "../../src/interfaces/precompiles/IAssets.sol"; import "../../src/interfaces/precompiles/IClaimReward.sol"; import "../../src/interfaces/precompiles/IDelegation.sol"; -import "../../src/interfaces/precompiles/IDeposit.sol"; -import "../../src/interfaces/precompiles/IWithdrawPrinciple.sol"; import {NonShortCircuitEndpointV2Mock} from "../mocks/NonShortCircuitEndpointV2Mock.sol"; import "src/core/BeaconProxyBytecode.sol"; @@ -86,12 +87,28 @@ contract ExocoreDeployer is Test { address addr; } + event MessageSent(GatewayStorage.Action indexed act, bytes32 packetId, uint64 nonce, uint256 nativeFee); + event NewPacket(uint32, address, bytes32, uint64, bytes); + event RegisterTokensResult(bool indexed success); + event WhitelistTokenAdded(address _token); + event VaultCreated(address underlyingToken, address vault); + function setUp() public virtual { players.push(Player({privateKey: uint256(0x1), addr: vm.addr(uint256(0x1))})); players.push(Player({privateKey: uint256(0x2), addr: vm.addr(uint256(0x2))})); players.push(Player({privateKey: uint256(0x3), addr: vm.addr(uint256(0x3))})); exocoreValidatorSet = Player({privateKey: uint256(0xa), addr: vm.addr(uint256(0xa))}); + // bind precompile mock contracts code to constant precompile address + bytes memory AssetsMockCode = vm.getDeployedCode("AssetsMock.sol"); + vm.etch(ASSETS_PRECOMPILE_ADDRESS, AssetsMockCode); + + bytes memory DelegationMockCode = vm.getDeployedCode("DelegationMock.sol"); + vm.etch(DELEGATION_PRECOMPILE_ADDRESS, DelegationMockCode); + + bytes memory WithdrawRewardMockCode = vm.getDeployedCode("ClaimRewardMock.sol"); + vm.etch(CLAIM_REWARD_PRECOMPILE_ADDRESS, WithdrawRewardMockCode); + // load beacon chain validator container and proof from json file _loadValidatorContainer(); _loadValidatorProof(); @@ -101,6 +118,104 @@ contract ExocoreDeployer is Test { _deploy(); } + function test_AddWhitelistTokens() public { + // transfer some gas fee to exocore validator set + deal(exocoreValidatorSet.addr, 1e22); + // transfer some gas fee to exocore gateway as it has to pay for the relay fee to layerzero endpoint when + // sending back response + deal(address(exocoreGateway), 1e22); + + whitelistTokens.push(address(restakeToken)); + + // -- add whitelist tokens workflow test -- + + vm.startPrank(exocoreValidatorSet.addr); + + // first user call client chain gateway to add whitelist tokens + + // estimate l0 relay fee that the user should pay + bytes memory registerTokensRequestPayload = abi.encodePacked( + GatewayStorage.Action.REQUEST_REGISTER_TOKENS, uint8(1), bytes32(bytes20(address(restakeToken))) + ); + uint256 registerTokensRequestNativeFee = clientGateway.quote(registerTokensRequestPayload); + bytes32 registerTokensRequestId = generateUID(1, true); + + // client chain layerzero endpoint should emit the message packet including deposit payload. + vm.expectEmit(true, true, true, true, address(clientChainLzEndpoint)); + emit NewPacket( + exocoreChainId, + address(clientGateway), + address(exocoreGateway).toBytes32(), + uint64(1), + registerTokensRequestPayload + ); + // client chain gateway should emit MessageSent event + vm.expectEmit(true, true, true, true, address(clientGateway)); + emit MessageSent( + GatewayStorage.Action.REQUEST_REGISTER_TOKENS, + registerTokensRequestId, + uint64(1), + registerTokensRequestNativeFee + ); + clientGateway.addWhitelistTokens{value: registerTokensRequestNativeFee}(whitelistTokens); + + // second layerzero relayers should watch the request message packet and relay the message to destination + // endpoint + + // exocore gateway should return response message to exocore network layerzero endpoint + vm.expectEmit(true, true, true, true, address(exocoreLzEndpoint)); + bytes memory registerTokensResponsePayload = abi.encodePacked(GatewayStorage.Action.RESPOND, uint64(1), true); + uint256 registerTokensResponseNativeFee = exocoreGateway.quote(clientChainId, registerTokensResponsePayload); + bytes32 registerTokensResponseId = generateUID(1, false); + emit NewPacket( + clientChainId, + address(exocoreGateway), + address(clientGateway).toBytes32(), + uint64(1), + registerTokensResponsePayload + ); + // exocore gateway should emit MessageSent event + vm.expectEmit(true, true, true, true, address(exocoreGateway)); + emit MessageSent( + GatewayStorage.Action.RESPOND, registerTokensResponseId, uint64(1), registerTokensResponseNativeFee + ); + exocoreLzEndpoint.lzReceive( + Origin(clientChainId, address(clientGateway).toBytes32(), uint64(1)), + address(exocoreGateway), + registerTokensRequestId, + registerTokensRequestPayload, + bytes("") + ); + + // third layerzero relayers should watch the response message packet and relay the message to source chain + // endpoint + + address expectedVault = Create2.computeAddress( + bytes32(uint256(uint160(address(restakeToken)))), + keccak256(abi.encodePacked(BEACON_PROXY_BYTECODE, abi.encode(address(vaultBeacon), ""))), + address(clientGateway) + ); + // client chain gateway should execute the response hook and emit depositResult event + vm.expectEmit(true, true, true, true, address(clientGateway)); + emit VaultCreated(address(restakeToken), expectedVault); + emit WhitelistTokenAdded(address(restakeToken)); + emit RegisterTokensResult(true); + clientChainLzEndpoint.lzReceive( + Origin(exocoreChainId, address(exocoreGateway).toBytes32(), uint64(1)), + address(clientGateway), + registerTokensResponseId, + registerTokensResponsePayload, + bytes("") + ); + + // find vault according to uderlying token address + vault = Vault(address(clientGateway.tokenToVault(address(restakeToken)))); + assertEq(address(vault), expectedVault); + assertTrue(clientGateway.isWhitelistedToken(address(restakeToken))); + + vm.stopPrank(); + } + function _loadValidatorContainer() internal { string memory validatorInfo = vm.readFile("test/foundry/test-data/validator_container_proof_302913.json"); @@ -154,7 +269,6 @@ contract ExocoreDeployer is Test { vm.etch(address(ETH_POS), address(ethPOSDepositMock).code); // deploy and initialize client chain contracts - whitelistTokens.push(address(restakeToken)); ProxyAdmin proxyAdmin = new ProxyAdmin(); clientGatewayLogic = new ClientChainGateway( @@ -172,16 +286,13 @@ contract ExocoreDeployer is Test { address(clientGatewayLogic), address(proxyAdmin), abi.encodeWithSelector( - clientGatewayLogic.initialize.selector, payable(exocoreValidatorSet.addr), whitelistTokens + clientGatewayLogic.initialize.selector, payable(exocoreValidatorSet.addr) ) ) ) ) ); - // find vault according to uderlying token address - vault = Vault(address(clientGateway.tokenToVault(address(restakeToken)))); - // deploy Exocore network contracts exocoreGatewayLogic = new ExocoreGateway(address(exocoreLzEndpoint)); exocoreGateway = ExocoreGateway( @@ -214,19 +325,6 @@ contract ExocoreDeployer is Test { clientGateway.setPeer(exocoreChainId, address(exocoreGateway).toBytes32()); exocoreGateway.setPeer(clientChainId, address(clientGateway).toBytes32()); vm.stopPrank(); - - // bind precompile mock contracts code to constant precompile address - bytes memory DepositMockCode = vm.getDeployedCode("DepositMock.sol"); - vm.etch(DEPOSIT_PRECOMPILE_ADDRESS, DepositMockCode); - - bytes memory DelegationMockCode = vm.getDeployedCode("DelegationMock.sol"); - vm.etch(DELEGATION_PRECOMPILE_ADDRESS, DelegationMockCode); - - bytes memory WithdrawPrincipleMockCode = vm.getDeployedCode("WithdrawPrincipleMock.sol"); - vm.etch(WITHDRAW_PRECOMPILE_ADDRESS, WithdrawPrincipleMockCode); - - bytes memory WithdrawRewardMockCode = vm.getDeployedCode("ClaimRewardMock.sol"); - vm.etch(CLAIM_REWARD_PRECOMPILE_ADDRESS, WithdrawRewardMockCode); } function _deployBeaconOracle() internal returns (EigenLayerBeaconOracle) { diff --git a/test/foundry/WithdrawReward.t.sol b/test/foundry/WithdrawReward.t.sol index 6ee534d1..21cd58c3 100644 --- a/test/foundry/WithdrawReward.t.sol +++ b/test/foundry/WithdrawReward.t.sol @@ -16,26 +16,28 @@ contract WithdrawRewardTest is ExocoreDeployer { event DepositResult(bool indexed success, address indexed token, address indexed depositor, uint256 amount); event WithdrawRewardResult(bool indexed success, address indexed token, address indexed withdrawer, uint256 amount); event Transfer(address indexed from, address indexed to, uint256 amount); - event MessageSent(GatewayStorage.Action indexed act, bytes32 packetId, uint64 nonce, uint256 nativeFee); event MessageProcessed(uint16 _srcChainId, bytes _srcAddress, uint64 _nonce, bytes _payload); - event NewPacket(uint32, address, bytes32, uint64, bytes); uint256 constant DEFAULT_ENDPOINT_CALL_GAS_LIMIT = 200_000; function test_WithdrawRewardByLayerZero() public { Player memory withdrawer = players[0]; + Player memory relayer = players[1]; deal(withdrawer.addr, 1e22); deal(address(clientGateway), 1e22); deal(address(exocoreGateway), 1e22); uint256 withdrawAmount = 1000; - vm.startPrank(withdrawer.addr); + + // before withdraw we should add whitelist tokens + test_AddWhitelistTokens(); // -- withdraw reward workflow -- // first user call client chain gateway to withdraw // estimate l0 relay fee that the user should pay + uint64 withdrawRequestNonce = 2; bytes memory withdrawRequestPayload = abi.encodePacked( GatewayStorage.Action.REQUEST_WITHDRAW_REWARD_FROM_EXOCORE, bytes32(bytes20(address(restakeToken))), @@ -43,45 +45,60 @@ contract WithdrawRewardTest is ExocoreDeployer { withdrawAmount ); uint256 requestNativeFee = clientGateway.quote(withdrawRequestPayload); - bytes32 requestId = generateUID(1, true); + bytes32 requestId = generateUID(withdrawRequestNonce, true); // client chain layerzero endpoint should emit the message packet including withdraw payload. vm.expectEmit(true, true, true, true, address(clientChainLzEndpoint)); emit NewPacket( - exocoreChainId, address(clientGateway), address(exocoreGateway).toBytes32(), 1, withdrawRequestPayload + exocoreChainId, + address(clientGateway), + address(exocoreGateway).toBytes32(), + withdrawRequestNonce, + withdrawRequestPayload ); // client chain gateway should emit MessageSent event vm.expectEmit(true, true, true, true, address(clientGateway)); emit MessageSent( - GatewayStorage.Action.REQUEST_WITHDRAW_REWARD_FROM_EXOCORE, requestId, uint64(1), requestNativeFee + GatewayStorage.Action.REQUEST_WITHDRAW_REWARD_FROM_EXOCORE, + requestId, + withdrawRequestNonce, + requestNativeFee ); + + vm.startPrank(withdrawer.addr); clientGateway.withdrawRewardFromExocore{value: requestNativeFee}(address(restakeToken), withdrawAmount); + vm.stopPrank(); // second layerzero relayers should watch the request message packet and relay the message to destination // endpoint // exocore gateway should return response message to exocore network layerzero endpoint - vm.expectEmit(true, true, true, true, address(exocoreLzEndpoint)); + uint64 withdrawResponseNonce = 2; bytes memory withdrawResponsePayload = - abi.encodePacked(GatewayStorage.Action.RESPOND, uint64(1), true, uint256(1234)); + abi.encodePacked(GatewayStorage.Action.RESPOND, withdrawRequestNonce, true, uint256(1234)); uint256 responseNativeFee = exocoreGateway.quote(clientChainId, withdrawResponsePayload); - bytes32 responseId = generateUID(1, false); + bytes32 responseId = generateUID(withdrawResponseNonce, false); + + vm.expectEmit(true, true, true, true, address(exocoreLzEndpoint)); emit NewPacket( clientChainId, address(exocoreGateway), address(clientGateway).toBytes32(), - uint64(1), + withdrawResponseNonce, withdrawResponsePayload ); // exocore gateway should emit MessageSent event vm.expectEmit(true, true, true, true, address(exocoreGateway)); - emit MessageSent(GatewayStorage.Action.RESPOND, responseId, uint64(1), responseNativeFee); + emit MessageSent(GatewayStorage.Action.RESPOND, responseId, withdrawResponseNonce, responseNativeFee); + + vm.startPrank(relayer.addr); exocoreLzEndpoint.lzReceive( - Origin(clientChainId, address(clientGateway).toBytes32(), uint64(1)), + Origin(clientChainId, address(clientGateway).toBytes32(), withdrawRequestNonce), address(exocoreGateway), requestId, withdrawRequestPayload, bytes("") ); + vm.stopPrank(); // third layerzero relayers should watch the response message packet and relay the message to source chain // endpoint @@ -89,13 +106,16 @@ contract WithdrawRewardTest is ExocoreDeployer { // client chain gateway should execute the response hook and emit depositResult event vm.expectEmit(true, true, true, true, address(clientGateway)); emit WithdrawRewardResult(true, address(restakeToken), withdrawer.addr, withdrawAmount); + + vm.startPrank(relayer.addr); clientChainLzEndpoint.lzReceive( - Origin(exocoreChainId, address(exocoreGateway).toBytes32(), uint64(1)), + Origin(exocoreChainId, address(exocoreGateway).toBytes32(), withdrawResponseNonce), address(clientGateway), responseId, withdrawResponsePayload, bytes("") ); + vm.stopPrank(); } } diff --git a/test/foundry/Bootstrap.t.sol b/test/foundry/unit/Bootstrap.t.sol similarity index 96% rename from test/foundry/Bootstrap.t.sol rename to test/foundry/unit/Bootstrap.t.sol index fb6dabf2..4ec37586 100644 --- a/test/foundry/Bootstrap.t.sol +++ b/test/foundry/unit/Bootstrap.t.sol @@ -1,19 +1,19 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import {Bootstrap} from "../../src/core/Bootstrap.sol"; -import {ClientChainGateway} from "../../src/core/ClientChainGateway.sol"; -import {CustomProxyAdmin} from "../../src/core/CustomProxyAdmin.sol"; -import {Vault} from "../../src/core/Vault.sol"; - -import {IOperatorRegistry} from "../../src/interfaces/IOperatorRegistry.sol"; - -import {IVault} from "../../src/interfaces/IVault.sol"; -import {Origin} from "../../src/lzApp/OAppReceiverUpgradeable.sol"; -import {BootstrapStorage} from "../../src/storage/BootstrapStorage.sol"; -import {GatewayStorage} from "../../src/storage/GatewayStorage.sol"; -import {NonShortCircuitEndpointV2Mock} from "../mocks/NonShortCircuitEndpointV2Mock.sol"; +import {Bootstrap} from "src/core/Bootstrap.sol"; +import {ClientChainGateway} from "src/core/ClientChainGateway.sol"; +import {CustomProxyAdmin} from "src/core/CustomProxyAdmin.sol"; +import {Vault} from "src/core/Vault.sol"; + +import {IOperatorRegistry} from "src/interfaces/IOperatorRegistry.sol"; + +import {NonShortCircuitEndpointV2Mock} from "../../mocks/NonShortCircuitEndpointV2Mock.sol"; import {MyToken} from "./MyToken.sol"; +import {IVault} from "src/interfaces/IVault.sol"; +import {Origin} from "src/lzApp/OAppReceiverUpgradeable.sol"; +import {BootstrapStorage} from "src/storage/BootstrapStorage.sol"; +import {GatewayStorage} from "src/storage/GatewayStorage.sol"; import "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/GUID.sol"; @@ -153,9 +153,7 @@ contract BootstrapTest is Test { address(beaconProxyBytecode) ); // we could also use encodeWithSelector and supply .initialize.selector instead. - bytes memory initialization = abi.encodeCall( - clientGatewayLogic.initialize, (payable(exocoreValidatorSet), appendedWhitelistTokensForUpgrade) - ); + bytes memory initialization = abi.encodeCall(clientGatewayLogic.initialize, (payable(exocoreValidatorSet))); bootstrap.setClientChainGatewayLogic(address(clientGatewayLogic), initialization); vm.stopPrank(); } @@ -163,7 +161,9 @@ contract BootstrapTest is Test { function test01_AddWhitelistToken() public returns (MyToken) { vm.startPrank(deployer); MyToken myTokenClone = new MyToken("MyToken", "MYT", 18, addrs, 1000 * 10 ** 18); - bootstrap.addWhitelistToken(address(myTokenClone)); + address[] memory addedWhitelistTokens = new address[](1); + addedWhitelistTokens[0] = address(myTokenClone); + bootstrap.addWhitelistTokens(addedWhitelistTokens); vm.stopPrank(); assertTrue(bootstrap.isWhitelistedToken(address(myTokenClone))); assertTrue(bootstrap.getWhitelistedTokensCount() == 2); @@ -172,8 +172,10 @@ contract BootstrapTest is Test { function test01_AddWhitelistToken_AlreadyExists() public { vm.startPrank(deployer); - vm.expectRevert("BootstrapStorage: token should be not whitelisted before"); - bootstrap.addWhitelistToken(address(myToken)); + vm.expectRevert("Bootstrap: token should be not whitelisted before"); + address[] memory addedWhitelistTokens = new address[](1); + addedWhitelistTokens[0] = address(myToken); + bootstrap.addWhitelistTokens(addedWhitelistTokens); vm.stopPrank(); } @@ -298,7 +300,9 @@ contract BootstrapTest is Test { // now add it to the whitelist vm.startPrank(deployer); - bootstrap.addWhitelistToken(cloneAddress); + address[] memory addedWhitelistTokens = new address[](1); + addedWhitelistTokens[0] = cloneAddress; + bootstrap.addWhitelistTokens(addedWhitelistTokens); vm.stopPrank(); // now try to deposit @@ -553,7 +557,9 @@ contract BootstrapTest is Test { vm.stopPrank(); // only the owner can add the token to the supported list vm.startPrank(deployer); - bootstrap.addWhitelistToken(cloneAddress); + address[] memory addedWhitelistTokens = new address[](1); + addedWhitelistTokens[0] = cloneAddress; + bootstrap.addWhitelistTokens(addedWhitelistTokens); vm.stopPrank(); // finally, check bool isSupported = bootstrap.isWhitelistedToken(cloneAddress); @@ -562,8 +568,10 @@ contract BootstrapTest is Test { function test07_AddWhitelistedToken_AlreadyWhitelisted() public { vm.startPrank(deployer); - vm.expectRevert("BootstrapStorage: token should be not whitelisted before"); - bootstrap.addWhitelistToken(address(myToken)); + vm.expectRevert("Bootstrap: token should be not whitelisted before"); + address[] memory addedWhitelistTokens = new address[](1); + addedWhitelistTokens[0] = address(myToken); + bootstrap.addWhitelistTokens(addedWhitelistTokens); vm.stopPrank(); } @@ -643,7 +651,9 @@ contract BootstrapTest is Test { function test09_DelegateTo_NotEnoughBlance() public { test03_RegisterOperator(); vm.startPrank(deployer); - bootstrap.addWhitelistToken(address(0xa)); + address[] memory addedWhitelistTokens = new address[](1); + addedWhitelistTokens[0] = address(0xa); + bootstrap.addWhitelistTokens(addedWhitelistTokens); vm.stopPrank(); vm.startPrank(addrs[0]); vm.expectRevert(bytes("Bootstrap: insufficient withdrawable balance")); @@ -733,7 +743,9 @@ contract BootstrapTest is Test { function test10_UndelegateFrom_NotEnoughBalance() public { test03_RegisterOperator(); vm.startPrank(deployer); - bootstrap.addWhitelistToken(address(0xa)); + address[] memory addedWhitelistTokens = new address[](1); + addedWhitelistTokens[0] = address(0xa); + bootstrap.addWhitelistTokens(addedWhitelistTokens); vm.stopPrank(); vm.startPrank(addrs[0]); vm.expectRevert(bytes("Bootstrap: insufficient delegated balance")); @@ -1165,20 +1177,6 @@ contract BootstrapTest is Test { bootstrap.setOffsetDuration(offsetDuration + 2); } - function test18_RemoveWhitelistToken() public { - vm.startPrank(deployer); - bootstrap.removeWhitelistToken(address(myToken)); - assertFalse(bootstrap.isWhitelistedToken(address(myToken))); - assertTrue(bootstrap.getWhitelistedTokensCount() == 0); - } - - function test18_RemoveWhitelistToken_DoesNotExist() public { - address fakeToken = address(0xa); - vm.startPrank(deployer); - vm.expectRevert("BootstrapStorage: token is not whitelisted"); - bootstrap.removeWhitelistToken(fakeToken); - } - function test20_WithdrawRewardFromExocore() public { vm.expectRevert(abi.encodeWithSignature("NotYetSupported()")); bootstrap.withdrawRewardFromExocore(address(0x0), 1); diff --git a/test/foundry/unit/ClientChainGateway.t.sol b/test/foundry/unit/ClientChainGateway.t.sol new file mode 100644 index 00000000..3cb89388 --- /dev/null +++ b/test/foundry/unit/ClientChainGateway.t.sol @@ -0,0 +1,366 @@ +pragma solidity ^0.8.19; + +import "@beacon-oracle/contracts/src/EigenLayerBeaconOracle.sol"; + +import "@layerzero-v2/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import "@layerzero-v2/protocol/contracts/libs/AddressCast.sol"; +import "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/GUID.sol"; +import "@openzeppelin-contracts/contracts/proxy/beacon/IBeacon.sol"; +import "@openzeppelin-contracts/contracts/proxy/beacon/UpgradeableBeacon.sol"; +import "@openzeppelin-contracts/contracts/proxy/transparent/ProxyAdmin.sol"; +import "@openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import "@openzeppelin-contracts/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; +import "@openzeppelin-upgradeable/contracts/access/OwnableUpgradeable.sol"; +import "@openzeppelin-upgradeable/contracts/utils/PausableUpgradeable.sol"; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +import "src/core/ClientChainGateway.sol"; + +import "src/core/ExoCapsule.sol"; +import "src/core/ExocoreGateway.sol"; +import {Vault} from "src/core/Vault.sol"; +import "src/storage/GatewayStorage.sol"; + +import {NonShortCircuitEndpointV2Mock} from "../../mocks/NonShortCircuitEndpointV2Mock.sol"; +import "src/interfaces/IExoCapsule.sol"; +import "src/interfaces/IVault.sol"; + +import "src/core/BeaconProxyBytecode.sol"; + +contract SetUp is Test { + + using AddressCast for address; + + struct Player { + uint256 privateKey; + address addr; + } + + // bytes32 token + bytes32 depositor + uint256 amount + uint256 internal constant DEPOSIT_REQUEST_LENGTH = 96; + // bytes32 token + bytes32 delegator + bytes(42) operator + uint256 amount + uint256 internal constant DELEGATE_REQUEST_LENGTH = 138; + // bytes32 token + bytes32 delegator + bytes(42) operator + uint256 amount + uint256 internal constant UNDELEGATE_REQUEST_LENGTH = 138; + // bytes32 token + bytes32 withdrawer + uint256 amount + uint256 internal constant WITHDRAW_PRINCIPLE_REQUEST_LENGTH = 96; + // bytes32 token + bytes32 withdrawer + uint256 amount + uint256 internal constant CLAIM_REWARD_REQUEST_LENGTH = 96; + // bytes32 token + bytes32 delegator + bytes(42) operator + uint256 amount + uint256 internal constant DEPOSIT_THEN_DELEGATE_REQUEST_LENGTH = DELEGATE_REQUEST_LENGTH; + uint256 internal constant TOKEN_ADDRESS_BYTES_LENTH = 32; + + Player[] players; + address[] whitelistTokens; + Player exocoreValidatorSet; + Player deployer; + address[] vaults; + ERC20PresetFixedSupply restakeToken; + + ClientChainGateway clientGateway; + ClientChainGateway clientGatewayLogic; + ExocoreGateway exocoreGateway; + ILayerZeroEndpointV2 clientChainLzEndpoint; + ILayerZeroEndpointV2 exocoreLzEndpoint; + IBeaconChainOracle beaconOracle; + IVault vaultImplementation; + IExoCapsule capsuleImplementation; + IBeacon vaultBeacon; + IBeacon capsuleBeacon; + BeaconProxyBytecode beaconProxyBytecode; + + string operatorAddress = "exo1v4s6vtjpmxwu9rlhqms5urzrc3tc2ae2gnuqhc"; + uint32 exocoreChainId = 2; + uint32 clientChainId = 1; + + event Paused(address account); + event Unpaused(address account); + event MessageSent(GatewayStorage.Action indexed act, bytes32 packetId, uint64 nonce, uint256 nativeFee); + + function setUp() public virtual { + players.push(Player({privateKey: uint256(0x1), addr: vm.addr(uint256(0x1))})); + players.push(Player({privateKey: uint256(0x2), addr: vm.addr(uint256(0x2))})); + players.push(Player({privateKey: uint256(0x3), addr: vm.addr(uint256(0x3))})); + exocoreValidatorSet = Player({privateKey: uint256(0xa), addr: vm.addr(uint256(0xa))}); + deployer = Player({privateKey: uint256(0xb), addr: vm.addr(uint256(0xb))}); + exocoreGateway = ExocoreGateway(payable(address(0xc))); + exocoreLzEndpoint = ILayerZeroEndpointV2(address(0xd)); + + vm.deal(exocoreValidatorSet.addr, 100 ether); + vm.deal(deployer.addr, 100 ether); + + vm.chainId(clientChainId); + _deploy(); + + NonShortCircuitEndpointV2Mock(address(clientChainLzEndpoint)).setDestLzEndpoint( + address(exocoreGateway), address(exocoreLzEndpoint) + ); + + vm.prank(exocoreValidatorSet.addr); + clientGateway.setPeer(exocoreChainId, address(exocoreGateway).toBytes32()); + vm.stopPrank(); + } + + function _deploy() internal { + vm.startPrank(deployer.addr); + + beaconOracle = IBeaconChainOracle(_deployBeaconOracle()); + + vaultImplementation = new Vault(); + capsuleImplementation = new ExoCapsule(); + + vaultBeacon = new UpgradeableBeacon(address(vaultImplementation)); + capsuleBeacon = new UpgradeableBeacon(address(capsuleImplementation)); + + beaconProxyBytecode = new BeaconProxyBytecode(); + + restakeToken = new ERC20PresetFixedSupply("rest", "rest", 1e16, exocoreValidatorSet.addr); + whitelistTokens.push(address(restakeToken)); + + clientChainLzEndpoint = new NonShortCircuitEndpointV2Mock(clientChainId, exocoreValidatorSet.addr); + ProxyAdmin proxyAdmin = new ProxyAdmin(); + clientGatewayLogic = new ClientChainGateway( + address(clientChainLzEndpoint), + exocoreChainId, + address(beaconOracle), + address(vaultBeacon), + address(capsuleBeacon), + address(beaconProxyBytecode) + ); + clientGateway = ClientChainGateway( + payable(address(new TransparentUpgradeableProxy(address(clientGatewayLogic), address(proxyAdmin), ""))) + ); + + clientGateway.initialize(payable(exocoreValidatorSet.addr)); + + vm.stopPrank(); + } + + function _deployBeaconOracle() internal returns (EigenLayerBeaconOracle) { + uint256 GENESIS_BLOCK_TIMESTAMP; + + // mainnet + if (block.chainid == 1) { + GENESIS_BLOCK_TIMESTAMP = 1_606_824_023; + // goerli + } else if (block.chainid == 5) { + GENESIS_BLOCK_TIMESTAMP = 1_616_508_000; + // sepolia + } else if (block.chainid == 11_155_111) { + GENESIS_BLOCK_TIMESTAMP = 1_655_733_600; + // holesky + } else if (block.chainid == 17_000) { + GENESIS_BLOCK_TIMESTAMP = 1_695_902_400; + } else { + revert("Unsupported chainId."); + } + + EigenLayerBeaconOracle oracle = new EigenLayerBeaconOracle(GENESIS_BLOCK_TIMESTAMP); + return oracle; + } + + 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 { + + using stdStorage for StdStorage; + + function setUp() public override { + super.setUp(); + + // we use this hacking way to find the slot of `isWhitelistedToken(address(restakeToken))` and set its value to + // true + bytes32 whitelistedSlot = bytes32( + stdstore.target(address(clientGatewayLogic)).sig("isWhitelistedToken(address)").with_key( + address(restakeToken) + ).find() + ); + vm.store(address(clientGateway), whitelistedSlot, bytes32(uint256(1))); + } + + function test_PauseClientChainGateway() public { + vm.expectEmit(true, true, true, true, address(clientGateway)); + emit Paused(exocoreValidatorSet.addr); + vm.prank(exocoreValidatorSet.addr); + clientGateway.pause(); + assertEq(clientGateway.paused(), true); + } + + function test_UnpauseClientChainGateway() public { + vm.startPrank(exocoreValidatorSet.addr); + + vm.expectEmit(true, true, true, true, address(clientGateway)); + emit PausableUpgradeable.Paused(exocoreValidatorSet.addr); + clientGateway.pause(); + assertEq(clientGateway.paused(), true); + + vm.expectEmit(true, true, true, true, address(clientGateway)); + emit PausableUpgradeable.Unpaused(exocoreValidatorSet.addr); + clientGateway.unpause(); + assertEq(clientGateway.paused(), false); + } + + function test_RevertWhen_UnauthorizedPauser() public { + vm.expectRevert("ClientChainGateway: caller is not Exocore validator set aggregated address"); + vm.startPrank(deployer.addr); + clientGateway.pause(); + } + + function test_RevertWhen_CallDisabledFunctionsWhenPaused() public { + vm.startPrank(exocoreValidatorSet.addr); + clientGateway.pause(); + + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + clientGateway.claim(address(restakeToken), uint256(1), deployer.addr); + + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + clientGateway.delegateTo(operatorAddress, address(restakeToken), uint256(1)); + + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + clientGateway.deposit(address(restakeToken), uint256(1)); + + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + clientGateway.withdrawPrincipleFromExocore(address(restakeToken), uint256(1)); + + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + clientGateway.undelegateFrom(operatorAddress, address(restakeToken), uint256(1)); + } + +} + +contract Initialize is SetUp { + + function test_ExocoreChainIdInitialized() public { + assertEq(clientGateway.EXOCORE_CHAIN_ID(), exocoreChainId); + } + + function test_LzEndpointInitialized() public { + assertFalse(address(clientChainLzEndpoint) == address(0)); + assertEq(address(clientGateway.endpoint()), address(clientChainLzEndpoint)); + } + + function test_VaultBeaconInitialized() public { + assertFalse(address(vaultBeacon) == address(0)); + assertEq(address(clientGateway.VAULT_BEACON()), address(vaultBeacon)); + } + + function test_BeaconProxyByteCodeInitialized() public { + assertFalse(address(beaconProxyBytecode) == address(0)); + assertEq(address(clientGateway.BEACON_PROXY_BYTECODE()), address(beaconProxyBytecode)); + } + + function test_BeaconOracleInitialized() public { + assertFalse(address(beaconOracle) == address(0)); + assertEq(clientGateway.BEACON_ORACLE_ADDRESS(), address(beaconOracle)); + } + + function test_ExoCapsuleBeaconInitialized() public { + assertFalse(address(capsuleBeacon) == address(0)); + assertEq(address(clientGateway.EXO_CAPSULE_BEACON()), address(capsuleBeacon)); + } + + function test_ExocoreValidatoSetAddressInitialized() public { + assertEq(clientGateway.exocoreValidatorSetAddress(), exocoreValidatorSet.addr); + } + + function test_OwnerInitialized() public { + assertEq(clientGateway.owner(), exocoreValidatorSet.addr); + } + + function test_NotPaused() public { + assertFalse(clientGateway.paused()); + } + + function test_Bootstrapped() public { + assertTrue(clientGateway.bootstrapped()); + } + +} + +contract AddWhitelistTokens is SetUp { + + using stdStorage for StdStorage; + + function test_RevertWhen_CallerNotOwner() public { + address[] memory whitelistTokens = new address[](2); + uint256 nativeFee = clientGateway.quote(new bytes(TOKEN_ADDRESS_BYTES_LENTH * whitelistTokens.length + 2)); + + vm.startPrank(deployer.addr); + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, deployer.addr)); + clientGateway.addWhitelistTokens{value: nativeFee}(whitelistTokens); + } + + function test_RevertWhen_Paused() public { + vm.startPrank(exocoreValidatorSet.addr); + clientGateway.pause(); + + address[] memory whitelistTokens = new address[](2); + uint256 nativeFee = clientGateway.quote(new bytes(TOKEN_ADDRESS_BYTES_LENTH * whitelistTokens.length + 2)); + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + clientGateway.addWhitelistTokens{value: nativeFee}(whitelistTokens); + } + + function test_RevertWhen_TokensListTooLong() public { + address[] memory whitelistTokens = new address[](256); + uint256 nativeFee = clientGateway.quote(new bytes(TOKEN_ADDRESS_BYTES_LENTH * whitelistTokens.length + 2)); + + vm.startPrank(exocoreValidatorSet.addr); + vm.expectRevert("ClientChainGateway: tokens length should not execeed 255"); + clientGateway.addWhitelistTokens{value: nativeFee}(whitelistTokens); + } + + function test_RevertWhen_HasZeroAddressToken() public { + address[] memory whitelistTokens = new address[](2); + whitelistTokens[0] = address(restakeToken); + uint256 nativeFee = clientGateway.quote(new bytes(TOKEN_ADDRESS_BYTES_LENTH * whitelistTokens.length + 2)); + + vm.startPrank(exocoreValidatorSet.addr); + vm.expectRevert("ClientChainGateway: zero token address"); + clientGateway.addWhitelistTokens{value: nativeFee}(whitelistTokens); + } + + function test_RevertWhen_HasAlreadyWhitelistedToken() public { + // we use this hacking way to find the slot of `isWhitelistedToken(address(restakeToken))` and set its value to + // true + bytes32 whitelistedSlot = bytes32( + stdstore.target(address(clientGatewayLogic)).sig("isWhitelistedToken(address)").with_key( + address(restakeToken) + ).find() + ); + vm.store(address(clientGateway), whitelistedSlot, bytes32(uint256(1))); + + address[] memory whitelistTokens = new address[](1); + whitelistTokens[0] = address(restakeToken); + uint256 nativeFee = clientGateway.quote(new bytes(TOKEN_ADDRESS_BYTES_LENTH * whitelistTokens.length + 2)); + + vm.startPrank(exocoreValidatorSet.addr); + vm.expectRevert("ClientChainGateway: token should not be whitelisted before"); + clientGateway.addWhitelistTokens{value: nativeFee}(whitelistTokens); + } + + function test_SendMessage() public { + address[] memory whitelistTokens = new address[](1); + whitelistTokens[0] = address(restakeToken); + uint256 nativeFee = clientGateway.quote(new bytes(TOKEN_ADDRESS_BYTES_LENTH * whitelistTokens.length + 2)); + + vm.startPrank(exocoreValidatorSet.addr); + vm.expectEmit(true, true, true, true, address(clientGateway)); + emit MessageSent(GatewayStorage.Action.REQUEST_REGISTER_TOKENS, generateUID(1, true), 1, nativeFee); + clientGateway.addWhitelistTokens{value: nativeFee}(whitelistTokens); + } + +} diff --git a/test/foundry/CustomProxyAdmin.t.sol b/test/foundry/unit/CustomProxyAdmin.t.sol similarity index 97% rename from test/foundry/CustomProxyAdmin.t.sol rename to test/foundry/unit/CustomProxyAdmin.t.sol index 019587a5..dc868c81 100644 --- a/test/foundry/CustomProxyAdmin.t.sol +++ b/test/foundry/unit/CustomProxyAdmin.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import {CustomProxyAdmin} from "../../src/core/CustomProxyAdmin.sol"; -import {ICustomProxyAdmin} from "../../src/interfaces/ICustomProxyAdmin.sol"; +import {CustomProxyAdmin} from "src/core/CustomProxyAdmin.sol"; +import {ICustomProxyAdmin} from "src/interfaces/ICustomProxyAdmin.sol"; import "forge-std/Test.sol"; import "forge-std/console.sol"; diff --git a/test/foundry/ExoCapsule.t.sol b/test/foundry/unit/ExoCapsule.t.sol similarity index 100% rename from test/foundry/ExoCapsule.t.sol rename to test/foundry/unit/ExoCapsule.t.sol diff --git a/test/foundry/ExocoreGateway.t.sol b/test/foundry/unit/ExocoreGateway.t.sol similarity index 89% rename from test/foundry/ExocoreGateway.t.sol rename to test/foundry/unit/ExocoreGateway.t.sol index 8721d5ca..8c0c1ae3 100644 --- a/test/foundry/ExocoreGateway.t.sol +++ b/test/foundry/unit/ExocoreGateway.t.sol @@ -1,10 +1,9 @@ pragma solidity ^0.8.19; -import "../../src/interfaces/precompiles/IClaimReward.sol"; -import "../../src/interfaces/precompiles/IDelegation.sol"; -import "../../src/interfaces/precompiles/IDeposit.sol"; -import "../../src/interfaces/precompiles/IWithdrawPrinciple.sol"; -import {NonShortCircuitEndpointV2Mock} from "../mocks/NonShortCircuitEndpointV2Mock.sol"; +import {NonShortCircuitEndpointV2Mock} from "../../mocks/NonShortCircuitEndpointV2Mock.sol"; +import "src/interfaces/precompiles/IAssets.sol"; +import "src/interfaces/precompiles/IClaimReward.sol"; +import "src/interfaces/precompiles/IDelegation.sol"; import "@layerzero-v2/protocol/contracts/libs/AddressCast.sol"; import "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/GUID.sol"; @@ -60,6 +59,16 @@ contract SetUp is Test { withdrawer = Player({privateKey: uint256(0xc), addr: vm.addr(uint256(0xb))}); clientGateway = ClientChainGateway(payable(address(0xd))); + // bind precompile mock contracts code to constant precompile address + bytes memory AssetsMockCode = vm.getDeployedCode("AssetsMock.sol"); + vm.etch(ASSETS_PRECOMPILE_ADDRESS, AssetsMockCode); + + bytes memory DelegationMockCode = vm.getDeployedCode("DelegationMock.sol"); + vm.etch(DELEGATION_PRECOMPILE_ADDRESS, DelegationMockCode); + + bytes memory WithdrawRewardMockCode = vm.getDeployedCode("ClaimRewardMock.sol"); + vm.etch(CLAIM_REWARD_PRECOMPILE_ADDRESS, WithdrawRewardMockCode); + _deploy(); } @@ -88,19 +97,6 @@ contract SetUp is Test { // transfer some gas fee to exocore gateway as it has to pay for the relay fee to layerzero endpoint when // sending back response deal(address(exocoreGateway), 1e22); - - // bind precompile mock contracts code to constant precompile address - bytes memory DepositMockCode = vm.getDeployedCode("DepositMock.sol"); - vm.etch(DEPOSIT_PRECOMPILE_ADDRESS, DepositMockCode); - - bytes memory DelegationMockCode = vm.getDeployedCode("DelegationMock.sol"); - vm.etch(DELEGATION_PRECOMPILE_ADDRESS, DelegationMockCode); - - bytes memory WithdrawPrincipleMockCode = vm.getDeployedCode("WithdrawPrincipleMock.sol"); - vm.etch(WITHDRAW_PRECOMPILE_ADDRESS, WithdrawPrincipleMockCode); - - bytes memory WithdrawRewardMockCode = vm.getDeployedCode("ClaimRewardMock.sol"); - vm.etch(CLAIM_REWARD_PRECOMPILE_ADDRESS, WithdrawRewardMockCode); } } @@ -169,7 +165,7 @@ contract LzReceive is SetUp { bytes memory msg_ = abi.encodePacked(GatewayStorage.Action.REQUEST_WITHDRAW_PRINCIPLE_FROM_EXOCORE, payload); vm.expectEmit(true, true, true, true, address(exocoreGateway)); - emit ExocorePrecompileError(WITHDRAW_PRECOMPILE_ADDRESS, uint64(1)); + emit ExocorePrecompileError(ASSETS_PRECOMPILE_ADDRESS, uint64(1)); vm.prank(address(exocoreLzEndpoint)); exocoreGateway.lzReceive( diff --git a/test/foundry/MyToken.sol b/test/foundry/unit/MyToken.sol similarity index 100% rename from test/foundry/MyToken.sol rename to test/foundry/unit/MyToken.sol diff --git a/test/mocks/AssetsMock.sol b/test/mocks/AssetsMock.sol new file mode 100644 index 00000000..1365fd0b --- /dev/null +++ b/test/mocks/AssetsMock.sol @@ -0,0 +1,87 @@ +pragma solidity ^0.8.19; + +import {IAssets} from "src/interfaces/precompiles/IAssets.sol"; + +contract AssetsMock is IAssets { + + address constant VIRTUAL_STAKED_ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + mapping(uint32 => mapping(bytes => mapping(bytes => uint256))) public principleBalances; + + uint32[] internal chainIds; + mapping(uint32 chainId => bool registered) isRegisteredChain; + mapping(uint32 chainId => mapping(bytes token => bool registered)) isRegisteredToken; + + function depositTo(uint32 clientChainLzId, bytes memory assetsAddress, bytes memory stakerAddress, uint256 opAmount) + external + returns (bool success, uint256 latestAssetState) + { + require(assetsAddress.length == 32, "invalid asset address"); + require(stakerAddress.length == 32, "invalid staker address"); + if (bytes32(assetsAddress) != bytes32(bytes20(VIRTUAL_STAKED_ETH_ADDRESS))) { + require(isRegisteredToken[clientChainLzId][assetsAddress], "the token is not registered before"); + } + + principleBalances[clientChainLzId][assetsAddress][stakerAddress] += opAmount; + + return (true, principleBalances[clientChainLzId][assetsAddress][stakerAddress]); + } + + function withdrawPrinciple( + uint32 clientChainLzId, + bytes memory assetsAddress, + bytes memory withdrawer, + uint256 opAmount + ) external returns (bool success, uint256 latestAssetState) { + require(assetsAddress.length == 32, "invalid asset address"); + require(withdrawer.length == 32, "invalid staker address"); + if (bytes32(assetsAddress) != bytes32(bytes20(VIRTUAL_STAKED_ETH_ADDRESS))) { + require(isRegisteredToken[clientChainLzId][assetsAddress], "the token is not registered before"); + } + + require(opAmount <= principleBalances[clientChainLzId][assetsAddress][withdrawer], "withdraw amount overflow"); + + principleBalances[clientChainLzId][assetsAddress][withdrawer] -= opAmount; + + return (true, principleBalances[clientChainLzId][assetsAddress][withdrawer]); + } + + function getClientChains() external view returns (bool, uint32[] memory) { + return (true, chainIds); + } + + function registerClientChain(uint32 chainId) external returns (bool) { + require(!isRegisteredChain[chainId], "has already been registered"); + + isRegisteredChain[chainId] = true; + chainIds.push(chainId); + return true; + } + + function registerTokens(uint32 chainId, bytes[] memory tokens) external returns (bool) { + require(isRegisteredChain[chainId], "the chain is not registered before"); + + for (uint256 i; i < tokens.length; i++) { + bytes memory token = tokens[i]; + require(token.length == 32, "token address with invalid length"); + require(!isRegisteredToken[chainId][token], "already registered token"); + + isRegisteredToken[chainId][token] = true; + } + + return true; + } + + function getPrincipleBalance(uint32 clientChainLzId, bytes memory token, bytes memory staker) + public + view + returns (uint256) + { + return principleBalances[clientChainLzId][token][staker]; + } + + function _addressToBytes(address addr) internal pure returns (bytes memory) { + return abi.encodePacked(bytes32(bytes20(addr))); + } + +} diff --git a/test/mocks/ClientChainsMock.sol b/test/mocks/ClientChainsMock.sol deleted file mode 100644 index 00cb41a8..00000000 --- a/test/mocks/ClientChainsMock.sol +++ /dev/null @@ -1,15 +0,0 @@ -pragma solidity ^0.8.19; - -import {IClientChains} from "../../src/interfaces/precompiles/IClientChains.sol"; - -contract ClientChainsMock is IClientChains { - - uint16 clientChainId = 40_161; - - function getClientChains() external view returns (bool, uint16[] memory) { - uint16[] memory res = new uint16[](1); - res[0] = clientChainId; - return (true, res); - } - -} diff --git a/test/mocks/DepositMock.sol b/test/mocks/DepositMock.sol deleted file mode 100644 index 83c97299..00000000 --- a/test/mocks/DepositMock.sol +++ /dev/null @@ -1,35 +0,0 @@ -pragma solidity ^0.8.19; - -import {IDeposit} from "../../src/interfaces/precompiles/IDeposit.sol"; -import "../../src/interfaces/precompiles/IWithdrawPrinciple.sol"; -import "./WithdrawPrincipleMock.sol"; - -contract DepositMock is IDeposit { - - mapping(uint32 => mapping(bytes => mapping(bytes => uint256))) public principleBalances; - - function depositTo(uint32 clientChainLzId, bytes memory assetsAddress, bytes memory stakerAddress, uint256 opAmount) - external - returns (bool success, uint256 latestAssetState) - { - require(assetsAddress.length == 32, "invalid asset address"); - require(stakerAddress.length == 32, "invalid staker address"); - principleBalances[clientChainLzId][assetsAddress][stakerAddress] += opAmount; - WithdrawPrincipleMock(WITHDRAW_PRECOMPILE_ADDRESS).depositTo( - clientChainLzId, assetsAddress, stakerAddress, opAmount - ); - return (true, principleBalances[clientChainLzId][assetsAddress][stakerAddress]); - } - - function withdrawPrinciple( - uint32 clientChainLzId, - bytes memory assetsAddress, - bytes memory withdrawer, - uint256 opAmount - ) external returns (bool success, uint256 latestAssetState) { - require(opAmount <= principleBalances[clientChainLzId][assetsAddress][withdrawer], "withdraw amount overflow"); - principleBalances[clientChainLzId][assetsAddress][withdrawer] -= opAmount; - return (true, principleBalances[clientChainLzId][assetsAddress][withdrawer]); - } - -} diff --git a/test/mocks/DepositWithdrawMock.sol b/test/mocks/DepositWithdrawMock.sol deleted file mode 100644 index 6ac01970..00000000 --- a/test/mocks/DepositWithdrawMock.sol +++ /dev/null @@ -1,43 +0,0 @@ -pragma solidity ^0.8.19; - -import {IDeposit} from "../../src/interfaces/precompiles/IDeposit.sol"; -import {IWithdraw} from "../../src/interfaces/precompiles/IWithdrawPrinciple.sol"; - -contract DepositWithdrawMock is IDeposit, IWithdraw { - - mapping(uint32 => mapping(bytes => mapping(bytes => uint256))) public principleBalances; - - function depositTo(uint32 clientChainLzId, bytes memory assetsAddress, bytes memory stakerAddress, uint256 opAmount) - external - returns (bool success, uint256 latestAssetState) - { - require(assetsAddress.length == 32, "invalid asset address"); - require(stakerAddress.length == 32, "invalid staker address"); - principleBalances[clientChainLzId][assetsAddress][stakerAddress] += opAmount; - - return (true, principleBalances[clientChainLzId][assetsAddress][stakerAddress]); - } - - function withdrawPrinciple( - uint32 clientChainLzId, - bytes memory assetsAddress, - bytes memory withdrawer, - uint256 opAmount - ) external returns (bool success, uint256 latestAssetState) { - require(assetsAddress.length == 32, "invalid asset address"); - require(withdrawer.length == 32, "invalid staker address"); - require(opAmount <= principleBalances[clientChainLzId][assetsAddress][withdrawer], "withdraw amount overflow"); - principleBalances[clientChainLzId][assetsAddress][withdrawer] -= opAmount; - - return (true, principleBalances[clientChainLzId][assetsAddress][withdrawer]); - } - - function getPrincipleBalance(uint32 clientChainLzId, address token, address staker) public view returns (uint256) { - return principleBalances[clientChainLzId][_addressToBytes(token)][_addressToBytes(staker)]; - } - - function _addressToBytes(address addr) internal pure returns (bytes memory) { - return abi.encodePacked(bytes32(bytes20(addr))); - } - -} diff --git a/test/mocks/ExocoreGatewayMock.sol b/test/mocks/ExocoreGatewayMock.sol index e486b834..138df8f0 100644 --- a/test/mocks/ExocoreGatewayMock.sol +++ b/test/mocks/ExocoreGatewayMock.sol @@ -1,17 +1,11 @@ pragma solidity ^0.8.19; -import {BytesLib} from "@layerzero-contracts/util/BytesLib.sol"; - -import {OptionsBuilder} from "@layerzero-v2/oapp/contracts/oapp/libs/OptionsBuilder.sol"; +import {IExocoreGateway} from "src/interfaces/IExocoreGateway.sol"; -import {ILayerZeroReceiver} from "@layerzero-v2/protocol/contracts/interfaces/ILayerZeroReceiver.sol"; -import {ECDSA} from "@openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; -import {OwnableUpgradeable} from "@openzeppelin-upgradeable/contracts/access/OwnableUpgradeable.sol"; -import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; -import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/PausableUpgradeable.sol"; +import {IAssets} from "src/interfaces/precompiles/IAssets.sol"; +import {IClaimReward} from "src/interfaces/precompiles/IClaimReward.sol"; +import {IDelegation} from "src/interfaces/precompiles/IDelegation.sol"; -import {IExocoreGateway} from "src/interfaces/IExocoreGateway.sol"; -import {IClientChains} from "src/interfaces/precompiles/IClientChains.sol"; import { MessagingFee, MessagingReceipt, @@ -19,127 +13,86 @@ import { OAppUpgradeable, Origin } from "src/lzApp/OAppUpgradeable.sol"; +import {ExocoreGatewayStorage} from "src/storage/ExocoreGatewayStorage.sol"; + +import {IOAppCore} from "@layerzero-v2/oapp/contracts/oapp/interfaces/IOAppCore.sol"; +import {OptionsBuilder} from "@layerzero-v2/oapp/contracts/oapp/libs/OptionsBuilder.sol"; +import {ILayerZeroReceiver} from "@layerzero-v2/protocol/contracts/interfaces/ILayerZeroReceiver.sol"; +import {OwnableUpgradeable} from "@openzeppelin-upgradeable/contracts/access/OwnableUpgradeable.sol"; +import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; +import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/PausableUpgradeable.sol"; +import {OAppCoreUpgradeable} from "src/lzApp/OAppCoreUpgradeable.sol"; contract ExocoreGatewayMock is Initializable, - OwnableUpgradeable, PausableUpgradeable, + OwnableUpgradeable, IExocoreGateway, + ExocoreGatewayStorage, OAppUpgradeable { using OptionsBuilder for bytes; - enum Action { - REQUEST_DEPOSIT, - REQUEST_WITHDRAW_PRINCIPLE_FROM_EXOCORE, - REQUEST_WITHDRAW_REWARD_FROM_EXOCORE, - REQUEST_DELEGATE_TO, - REQUEST_UNDELEGATE_FROM, - REQUEST_DEPOSIT_THEN_DELEGATE_TO, - REQUEST_MARK_BOOTSTRAP, - RESPOND, - UPDATE_USERS_BALANCES - } - - mapping(Action => bytes4) public whiteListFunctionSelectors; - address payable public exocoreValidatorSetAddress; - - address immutable DEPOSIT_PRECOMPILE_MOCK_ADDRESS; - address immutable DELEGATION_PRECOMPILE_MOCK_ADDRESS; - address immutable WITHDRAW_PRINCIPLE_PRECOMPILE_MOCK_ADDRESS; - address immutable CLAIM_REWARD_PRECOMPILE_MOCK_ADDRESS; - address immutable CLIENT_CHAINS_PRECOMPILE_MOCK_ADDRESS; - - bytes4 constant DEPOSIT_FUNCTION_SELECTOR = bytes4(keccak256("depositTo(uint32,bytes,bytes,uint256)")); - bytes4 constant DELEGATE_TO_THROUGH_CLIENT_CHAIN_FUNCTION_SELECTOR = - bytes4(keccak256("delegateToThroughClientChain(uint32,uint64,bytes,bytes,bytes,uint256)")); - bytes4 constant UNDELEGATE_FROM_THROUGH_CLIENT_CHAIN_FUNCTION_SELECTOR = - bytes4(keccak256("undelegateFromThroughClientChain(uint32,uint64,bytes,bytes,bytes,uint256)")); - bytes4 constant WITHDRAW_PRINCIPLE_FUNCTION_SELECTOR = - bytes4(keccak256("withdrawPrinciple(uint32,bytes,bytes,uint256)")); - bytes4 constant CLAIM_REWARD_FUNCTION_SELECTOR = bytes4(keccak256("claimReward(uint32,bytes,bytes,uint256)")); - - uint256 constant DEPOSIT_REQUEST_LENGTH = 96; - uint256 constant DELEGATE_REQUEST_LENGTH = 138; - uint256 constant UNDELEGATE_REQUEST_LENGTH = 138; - uint256 constant WITHDRAW_PRINCIPLE_REQUEST_LENGTH = 96; - uint256 constant CLAIM_REWARD_REQUEST_LENGTH = 96; - - uint128 constant DESTINATION_GAS_LIMIT = 500_000; - uint128 constant DESTINATION_MSG_VALUE = 0; - - mapping(uint32 eid => mapping(bytes32 sender => uint64 nonce)) inboundNonce; - mapping(uint16 id => bool) chainToBootstrapped; - - event MessageSent(Action indexed act, bytes32 packetId, uint64 nonce, uint256 nativeFee); + address public immutable ASSETS_PRECOMPILE_ADDRESS; + address public immutable CLAIM_REWARD_PRECOMPILE_ADDRESS; + address public immutable DELEGATION_PRECOMPILE_ADDRESS; - error UnsupportedRequest(Action act); - error RequestExecuteFailed(Action act, uint64 nonce, bytes reason); - error PrecompileCallFailed(bytes4 selector_, bytes reason); - error UnexpectedInboundNonce(uint64 expectedNonce, uint64 actualNonce); - error UnexpectedSourceChain(uint32 unexpectedSrcEndpointId); - error InvalidRequestLength(Action act, uint256 expectedLength, uint256 actualLength); - - uint256[40] private __gap; + IAssets internal immutable ASSETS_CONTRACT; + IClaimReward internal immutable CLAIM_REWARD_CONTRACT; + IDelegation internal immutable DELEGATION_CONTRACT; modifier onlyCalledFromThis() { - require(msg.sender == address(this), "could only be called from this contract itself with low level call"); + require( + msg.sender == address(this), + "ExocoreGateway: can only be called from this contract itself with a low-level call" + ); _; } constructor( - address _endpoint, - address depositPrecompileMockAddress, - address withdrawPrinciplePrecompileMockAddress, - address delegationPrecompileMockAddress, - address ClaimRewardPrecompileMockAddress - ) OAppUpgradeable(_endpoint) { - DEPOSIT_PRECOMPILE_MOCK_ADDRESS = depositPrecompileMockAddress; - DELEGATION_PRECOMPILE_MOCK_ADDRESS = delegationPrecompileMockAddress; - WITHDRAW_PRINCIPLE_PRECOMPILE_MOCK_ADDRESS = withdrawPrinciplePrecompileMockAddress; - CLAIM_REWARD_PRECOMPILE_MOCK_ADDRESS = ClaimRewardPrecompileMockAddress; - CLIENT_CHAINS_PRECOMPILE_MOCK_ADDRESS = address(0); + address endpoint_, + address assetsPrecompileMock, + address ClaimRewardPrecompileMock, + address delegationPrecompileMock + ) OAppUpgradeable(endpoint_) { + require(endpoint_ != address(0), "Endpoint address cannot be zero."); + require(assetsPrecompileMock != address(0), "Assets precompile address cannot be zero."); + require(ClaimRewardPrecompileMock != address(0), "ClaimReward precompile address cannot be zero."); + require(delegationPrecompileMock != address(0), "Delegation precompile address cannot be zero."); + + ASSETS_PRECOMPILE_ADDRESS = assetsPrecompileMock; + CLAIM_REWARD_PRECOMPILE_ADDRESS = ClaimRewardPrecompileMock; + DELEGATION_PRECOMPILE_ADDRESS = delegationPrecompileMock; + + ASSETS_CONTRACT = IAssets(ASSETS_PRECOMPILE_ADDRESS); + CLAIM_REWARD_CONTRACT = IClaimReward(CLAIM_REWARD_PRECOMPILE_ADDRESS); + DELEGATION_CONTRACT = IDelegation(DELEGATION_PRECOMPILE_ADDRESS); _disableInitializers(); } receive() external payable {} - function initialize(address payable _exocoreValidatorSetAddress) external initializer { - require(_exocoreValidatorSetAddress != address(0), "invalid empty exocore validator set address"); + function initialize(address payable exocoreValidatorSetAddress_) external initializer { + require(exocoreValidatorSetAddress_ != address(0), "ExocoreGateway: invalid exocore validator set address"); - exocoreValidatorSetAddress = _exocoreValidatorSetAddress; - - whiteListFunctionSelectors[Action.REQUEST_DEPOSIT] = this.requestDeposit.selector; - whiteListFunctionSelectors[Action.REQUEST_DELEGATE_TO] = this.requestDelegateTo.selector; - whiteListFunctionSelectors[Action.REQUEST_UNDELEGATE_FROM] = this.requestUndelegateFrom.selector; - whiteListFunctionSelectors[Action.REQUEST_WITHDRAW_PRINCIPLE_FROM_EXOCORE] = - this.requestWithdrawPrinciple.selector; - whiteListFunctionSelectors[Action.REQUEST_WITHDRAW_REWARD_FROM_EXOCORE] = this.requestWithdrawReward.selector; + exocoreValidatorSetAddress = exocoreValidatorSetAddress_; + _initializeWhitelistFunctionSelectors(); __Ownable_init_unchained(exocoreValidatorSetAddress); __OAppCore_init_unchained(exocoreValidatorSetAddress); __Pausable_init_unchained(); } - // TODO: call this function automatically, either within the initializer (which requires - // setPeer) or be triggered by Golang after the contract is deployed. - // For manual calls, this function should be called immediately after deployment and - // then never needs to be called again. - function markBootstrapOnAllChains() public { - (bool success, uint16[] memory clientChainIds) = - IClientChains(CLIENT_CHAINS_PRECOMPILE_MOCK_ADDRESS).getClientChains(); - require(success, "ExocoreGateway: failed to get client chain ids"); - - for (uint256 i = 0; i < clientChainIds.length; i++) { - uint16 clientChainId = clientChainIds[i]; - if (!chainToBootstrapped[clientChainId]) { - _sendInterchainMsg(uint32(clientChainId), Action.REQUEST_MARK_BOOTSTRAP, ""); - // TODO: should this be marked only when receiving a response? - chainToBootstrapped[clientChainId] = true; - } - } + function _initializeWhitelistFunctionSelectors() private { + _whiteListFunctionSelectors[Action.REQUEST_DEPOSIT] = this.requestDeposit.selector; + _whiteListFunctionSelectors[Action.REQUEST_DELEGATE_TO] = this.requestDelegateTo.selector; + _whiteListFunctionSelectors[Action.REQUEST_UNDELEGATE_FROM] = this.requestUndelegateFrom.selector; + _whiteListFunctionSelectors[Action.REQUEST_WITHDRAW_PRINCIPLE_FROM_EXOCORE] = + this.requestWithdrawPrinciple.selector; + _whiteListFunctionSelectors[Action.REQUEST_WITHDRAW_REWARD_FROM_EXOCORE] = this.requestWithdrawReward.selector; + _whiteListFunctionSelectors[Action.REQUEST_REGISTER_TOKENS] = this.requestRegisterTokens.selector; } function pause() external { @@ -158,11 +111,66 @@ contract ExocoreGatewayMock is _unpause(); } + // TODO: call this function automatically, either within the initializer (which requires + // setPeer) or be triggered by Golang after the contract is deployed. + // For manual calls, this function should be called immediately after deployment and + // then never needs to be called again. + function markBootstrapOnAllChains() public whenNotPaused { + (bool success, bytes memory result) = + ASSETS_PRECOMPILE_ADDRESS.staticcall(abi.encodeWithSelector(ASSETS_CONTRACT.getClientChains.selector)); + require(success, "ExocoreGateway: failed to get client chain ids"); + (bool ok, uint32[] memory clientChainIds) = abi.decode(result, (bool, uint32[])); + require(ok, "ExocoreGateway: failed to decode client chain ids"); + for (uint256 i = 0; i < clientChainIds.length; i++) { + uint32 clientChainId = clientChainIds[i]; + if (!chainToBootstrapped[clientChainId]) { + _sendInterchainMsg(clientChainId, Action.REQUEST_MARK_BOOTSTRAP, ""); + // TODO: should this be marked only upon receiving a response? + chainToBootstrapped[clientChainId] = true; + } + } + } + + /** + * @notice Sets the peer address (OApp instance) for a corresponding endpoint. This would also + * register the `cientChainId` to Exocore native module if the peer address is first time being set. + * @param clientChainId The endpoint ID for client chain. + * @param clientChainGateway The contract address to be associated with the corresponding endpoint. + * + * @dev Only the owner/admin of the OApp can call this function. + * @dev Indicates that the peer is trusted to send LayerZero messages to this OApp. + * @dev Peer is a bytes32 to accommodate non-evm chains. + */ + function setPeer(uint32 clientChainId, bytes32 clientChainGateway) + public + override(IOAppCore, OAppCoreUpgradeable) + onlyOwner + whenNotPaused + { + _validatePeer(clientChainId, clientChainGateway); + _registerClientChain(clientChainId); + super.setPeer(clientChainId, clientChainGateway); + } + + function _validatePeer(uint32 clientChainId, bytes32 clientChainGateway) internal pure { + require(clientChainId != uint32(0), "ExocoreGateway: zero value is not invalid endpoint id"); + require(clientChainGateway != bytes32(0), "ExocoreGateway: client chain gateway cannot be empty"); + } + + function _registerClientChain(uint32 clientChainId) internal { + if (peers[clientChainId] == bytes32(0)) { + bool success = ASSETS_CONTRACT.registerClientChain(clientChainId); + if (!success) { + revert RegisterClientChainToExocoreFailed(clientChainId); + } + } + } + function _lzReceive(Origin calldata _origin, bytes calldata payload) internal virtual override whenNotPaused { _consumeInboundNonce(_origin.srcEid, _origin.sender, _origin.nonce); Action act = Action(uint8(payload[0])); - bytes4 selector_ = whiteListFunctionSelectors[act]; + bytes4 selector_ = _whiteListFunctionSelectors[act]; if (selector_ == bytes4(0)) { revert UnsupportedRequest(act); } @@ -174,129 +182,133 @@ contract ExocoreGatewayMock is } } - function requestDeposit(uint32 srcChainId, uint64 lzNonce, bytes calldata payload) public onlyCalledFromThis { - if (payload.length != DEPOSIT_REQUEST_LENGTH) { - revert InvalidRequestLength(Action.REQUEST_DEPOSIT, DEPOSIT_REQUEST_LENGTH, payload.length); + function requestRegisterTokens(uint32 srcChainId, uint64 lzNonce, bytes calldata payload) + public + onlyCalledFromThis + { + uint8 count = uint8(payload[0]); + uint256 expectedLength = count * TOKEN_ADDRESS_BYTES_LENTH + 1; + _validatePayloadLength(payload, expectedLength, Action.REQUEST_DEPOSIT); + + bytes[] memory tokens = new bytes[](count); + for (uint256 i; i < count; i++) { + uint256 start = i * TOKEN_ADDRESS_BYTES_LENTH + 1; + uint256 end = start + TOKEN_ADDRESS_BYTES_LENTH; + tokens[i] = payload[start:end]; + } + + try ASSETS_CONTRACT.registerTokens(srcChainId, tokens) returns (bool success) { + _sendInterchainMsg(srcChainId, Action.RESPOND, abi.encodePacked(lzNonce, success)); + } catch { + emit ExocorePrecompileError(ASSETS_PRECOMPILE_ADDRESS, lzNonce); + + _sendInterchainMsg(srcChainId, Action.RESPOND, abi.encodePacked(lzNonce, false)); } + } + + function requestDeposit(uint32 srcChainId, uint64 lzNonce, bytes calldata payload) public onlyCalledFromThis { + _validatePayloadLength(payload, DEPOSIT_REQUEST_LENGTH, Action.REQUEST_DEPOSIT); bytes calldata token = payload[:32]; bytes calldata depositor = payload[32:64]; uint256 amount = uint256(bytes32(payload[64:96])); - (bool success, bytes memory responseOrReason) = DEPOSIT_PRECOMPILE_MOCK_ADDRESS.call( - abi.encodeWithSelector(DEPOSIT_FUNCTION_SELECTOR, srcChainId, token, depositor, amount) - ); - - uint256 lastlyUpdatedPrincipleBalance; - if (success) { - (, lastlyUpdatedPrincipleBalance) = abi.decode(responseOrReason, (bool, uint256)); + (bool success, uint256 updatedBalance) = ASSETS_CONTRACT.depositTo(srcChainId, token, depositor, amount); + if (!success) { + revert DepositRequestShouldNotFail(srcChainId, lzNonce); } - _sendInterchainMsg( - srcChainId, Action.RESPOND, abi.encodePacked(lzNonce, success, lastlyUpdatedPrincipleBalance) - ); + + _sendInterchainMsg(srcChainId, Action.RESPOND, abi.encodePacked(lzNonce, success, updatedBalance)); } function requestWithdrawPrinciple(uint32 srcChainId, uint64 lzNonce, bytes calldata payload) public onlyCalledFromThis { - if (payload.length != WITHDRAW_PRINCIPLE_REQUEST_LENGTH) { - revert InvalidRequestLength( - Action.REQUEST_WITHDRAW_PRINCIPLE_FROM_EXOCORE, WITHDRAW_PRINCIPLE_REQUEST_LENGTH, payload.length - ); - } + _validatePayloadLength( + payload, WITHDRAW_PRINCIPLE_REQUEST_LENGTH, Action.REQUEST_WITHDRAW_PRINCIPLE_FROM_EXOCORE + ); bytes calldata token = payload[:32]; bytes calldata withdrawer = payload[32:64]; uint256 amount = uint256(bytes32(payload[64:96])); - (bool success, bytes memory responseOrReason) = WITHDRAW_PRINCIPLE_PRECOMPILE_MOCK_ADDRESS.call( - abi.encodeWithSelector(WITHDRAW_PRINCIPLE_FUNCTION_SELECTOR, srcChainId, token, withdrawer, amount) - ); + try ASSETS_CONTRACT.withdrawPrinciple(srcChainId, token, withdrawer, amount) returns ( + bool success, uint256 updatedBalance + ) { + _sendInterchainMsg(srcChainId, Action.RESPOND, abi.encodePacked(lzNonce, success, updatedBalance)); + } catch { + emit ExocorePrecompileError(ASSETS_PRECOMPILE_ADDRESS, lzNonce); - uint256 lastlyUpdatedPrincipleBalance; - if (success) { - (, lastlyUpdatedPrincipleBalance) = abi.decode(responseOrReason, (bool, uint256)); + _sendInterchainMsg(srcChainId, Action.RESPOND, abi.encodePacked(lzNonce, false, uint256(0))); } - _sendInterchainMsg( - srcChainId, Action.RESPOND, abi.encodePacked(lzNonce, success, lastlyUpdatedPrincipleBalance) - ); } function requestWithdrawReward(uint32 srcChainId, uint64 lzNonce, bytes calldata payload) public onlyCalledFromThis { - if (payload.length != CLAIM_REWARD_REQUEST_LENGTH) { - revert InvalidRequestLength( - Action.REQUEST_WITHDRAW_REWARD_FROM_EXOCORE, CLAIM_REWARD_REQUEST_LENGTH, payload.length - ); - } + _validatePayloadLength(payload, CLAIM_REWARD_REQUEST_LENGTH, Action.REQUEST_WITHDRAW_REWARD_FROM_EXOCORE); bytes calldata token = payload[:32]; bytes calldata withdrawer = payload[32:64]; uint256 amount = uint256(bytes32(payload[64:96])); - (bool success, bytes memory responseOrReason) = CLAIM_REWARD_PRECOMPILE_MOCK_ADDRESS.call( - abi.encodeWithSelector(CLAIM_REWARD_FUNCTION_SELECTOR, srcChainId, token, withdrawer, amount) - ); + try CLAIM_REWARD_CONTRACT.claimReward(srcChainId, token, withdrawer, amount) returns ( + bool success, uint256 updatedBalance + ) { + _sendInterchainMsg(srcChainId, Action.RESPOND, abi.encodePacked(lzNonce, success, updatedBalance)); + } catch { + emit ExocorePrecompileError(CLAIM_REWARD_PRECOMPILE_ADDRESS, lzNonce); - uint256 lastlyUpdatedRewardBalance; - if (success) { - (, lastlyUpdatedRewardBalance) = abi.decode(responseOrReason, (bool, uint256)); + _sendInterchainMsg(srcChainId, Action.RESPOND, abi.encodePacked(lzNonce, false, uint256(0))); } - _sendInterchainMsg(srcChainId, Action.RESPOND, abi.encodePacked(lzNonce, success, lastlyUpdatedRewardBalance)); } function requestDelegateTo(uint32 srcChainId, uint64 lzNonce, bytes calldata payload) public onlyCalledFromThis { - if (payload.length != DELEGATE_REQUEST_LENGTH) { - revert InvalidRequestLength(Action.REQUEST_DELEGATE_TO, DELEGATE_REQUEST_LENGTH, payload.length); - } + _validatePayloadLength(payload, DELEGATE_REQUEST_LENGTH, Action.REQUEST_DELEGATE_TO); bytes calldata token = payload[:32]; bytes calldata delegator = payload[32:64]; bytes calldata operator = payload[64:106]; uint256 amount = uint256(bytes32(payload[106:138])); - (bool success,) = DELEGATION_PRECOMPILE_MOCK_ADDRESS.call( - abi.encodeWithSelector( - DELEGATE_TO_THROUGH_CLIENT_CHAIN_FUNCTION_SELECTOR, - srcChainId, - lzNonce, - token, - delegator, - operator, - amount - ) - ); - _sendInterchainMsg(srcChainId, Action.RESPOND, abi.encodePacked(lzNonce, success)); + try DELEGATION_CONTRACT.delegateToThroughClientChain(srcChainId, lzNonce, token, delegator, operator, amount) + returns (bool success) { + _sendInterchainMsg(srcChainId, Action.RESPOND, abi.encodePacked(lzNonce, success)); + } catch { + emit ExocorePrecompileError(DELEGATION_PRECOMPILE_ADDRESS, lzNonce); + + _sendInterchainMsg(srcChainId, Action.RESPOND, abi.encodePacked(lzNonce, false)); + } } function requestUndelegateFrom(uint32 srcChainId, uint64 lzNonce, bytes calldata payload) public onlyCalledFromThis { - if (payload.length != UNDELEGATE_REQUEST_LENGTH) { - revert InvalidRequestLength(Action.REQUEST_UNDELEGATE_FROM, UNDELEGATE_REQUEST_LENGTH, payload.length); - } + _validatePayloadLength(payload, UNDELEGATE_REQUEST_LENGTH, Action.REQUEST_UNDELEGATE_FROM); bytes memory token = payload[:32]; bytes memory delegator = payload[32:64]; bytes memory operator = payload[64:106]; uint256 amount = uint256(bytes32(payload[106:138])); - (bool success,) = DELEGATION_PRECOMPILE_MOCK_ADDRESS.call( - abi.encodeWithSelector( - UNDELEGATE_FROM_THROUGH_CLIENT_CHAIN_FUNCTION_SELECTOR, - srcChainId, - lzNonce, - token, - delegator, - operator, - amount - ) - ); - _sendInterchainMsg(srcChainId, Action.RESPOND, abi.encodePacked(lzNonce, success)); + try DELEGATION_CONTRACT.undelegateFromThroughClientChain( + srcChainId, lzNonce, token, delegator, operator, amount + ) returns (bool success) { + _sendInterchainMsg(srcChainId, Action.RESPOND, abi.encodePacked(lzNonce, success)); + } catch { + emit ExocorePrecompileError(DELEGATION_PRECOMPILE_ADDRESS, lzNonce); + + _sendInterchainMsg(srcChainId, Action.RESPOND, abi.encodePacked(lzNonce, false)); + } + } + + function _validatePayloadLength(bytes calldata payload, uint256 expectedLength, Action action) private pure { + if (payload.length != expectedLength) { + revert InvalidRequestLength(action, expectedLength, payload.length); + } } function _sendInterchainMsg(uint32 srcChainId, Action act, bytes memory actionArgs) internal whenNotPaused { diff --git a/test/mocks/PrecompileCallerMock.sol b/test/mocks/PrecompileCallerMock.sol index 37fa5aaa..9df5b4d5 100644 --- a/test/mocks/PrecompileCallerMock.sol +++ b/test/mocks/PrecompileCallerMock.sol @@ -1,6 +1,6 @@ pragma solidity ^0.8.19; -import "../../src/interfaces/precompiles/IDeposit.sol"; +import "../../src/interfaces/precompiles/IAssets.sol"; contract PrecompileCallerMock { @@ -10,10 +10,10 @@ contract PrecompileCallerMock { error PrecompileError(); function deposit(uint256 amount) public { - (bool success, bytes memory response) = DEPOSIT_PRECOMPILE_ADDRESS.call{gas: 216_147}( + (bool success, bytes memory response) = ASSETS_PRECOMPILE_ADDRESS.call{gas: 216_147}( abi.encodeWithSelector( - DEPOSIT_CONTRACT.depositTo.selector, - uint16(101), + ASSETS_CONTRACT.depositTo.selector, + uint32(101), abi.encodePacked(bytes32(bytes20(address(0xdAC17F958D2ee523a2206206994597C13D831ec7)))), abi.encodePacked(bytes32(bytes20(address(0x2)))), amount diff --git a/test/mocks/WithdrawPrincipleMock.sol b/test/mocks/WithdrawPrincipleMock.sol deleted file mode 100644 index fcc7deee..00000000 --- a/test/mocks/WithdrawPrincipleMock.sol +++ /dev/null @@ -1,33 +0,0 @@ -pragma solidity ^0.8.19; - -import "../../src/interfaces/precompiles/IDeposit.sol"; -import {IWithdraw} from "../../src/interfaces/precompiles/IWithdrawPrinciple.sol"; -import "./DepositMock.sol"; - -contract WithdrawPrincipleMock is IWithdraw { - - mapping(uint32 => mapping(bytes => mapping(bytes => uint256))) principleBalances; - - function depositTo(uint32 clientChainLzId, bytes memory assetsAddress, bytes memory stakerAddress, uint256 opAmount) - external - returns (bool, uint256) - { - principleBalances[clientChainLzId][assetsAddress][stakerAddress] += opAmount; - return (true, principleBalances[clientChainLzId][assetsAddress][stakerAddress]); - } - - function withdrawPrinciple( - uint32 clientChainLzId, - bytes memory assetsAddress, - bytes memory withdrawer, - uint256 opAmount - ) external returns (bool success, uint256 latestAssetState) { - require(assetsAddress.length == 32, "invalid asset address"); - require(withdrawer.length == 32, "invalid staker address"); - require(opAmount <= principleBalances[clientChainLzId][assetsAddress][withdrawer], "withdraw amount overflow"); - principleBalances[clientChainLzId][assetsAddress][withdrawer] -= opAmount; - DepositMock(DEPOSIT_PRECOMPILE_ADDRESS).withdrawPrinciple(clientChainLzId, assetsAddress, withdrawer, opAmount); - return (true, principleBalances[clientChainLzId][assetsAddress][withdrawer]); - } - -}