diff --git a/src/core/Bootstrap.sol b/src/core/Bootstrap.sol index 3afce2ad..3562aa6f 100644 --- a/src/core/Bootstrap.sol +++ b/src/core/Bootstrap.sol @@ -195,8 +195,9 @@ contract Bootstrap is whitelistTokens.push(token); isWhitelistedToken[token] = true; + // do not deploy the vault for the virtual token address representing natively staked ETH // deploy the corresponding vault if not deployed before - if (address(tokenToVault[token]) == address(0)) { + if (token != VIRTUAL_STAKED_ETH_ADDRESS && address(tokenToVault[token]) == address(0)) { _deployVault(token); } diff --git a/src/core/ClientGatewayLzReceiver.sol b/src/core/ClientGatewayLzReceiver.sol index 8fa361ed..3834ec2e 100644 --- a/src/core/ClientGatewayLzReceiver.sol +++ b/src/core/ClientGatewayLzReceiver.sol @@ -316,6 +316,7 @@ abstract contract ClientGatewayLzReceiver is PausableUpgradeable, OAppReceiverUp isWhitelistedToken[token] = true; whitelistTokens.push(token); + // do not deploy the vault for the virtual token address representing natively staked ETH // deploy the corresponding vault if not deployed before if (token != VIRTUAL_STAKED_ETH_ADDRESS && address(tokenToVault[token]) == address(0)) { _deployVault(token); diff --git a/src/core/LSTRestakingController.sol b/src/core/LSTRestakingController.sol index f44107b8..79590422 100644 --- a/src/core/LSTRestakingController.sol +++ b/src/core/LSTRestakingController.sol @@ -48,6 +48,10 @@ abstract contract LSTRestakingController is whenNotPaused nonReentrant { + // If we can get the vault, the token cannot be VIRTUAL_STAKED_ETH_ADDRESS, so that staker cannot bypass the + // beacon chain merkle proof check to withdraw natively staked ETH + _getVault(token); + bytes memory actionArgs = abi.encodePacked(bytes32(bytes20(token)), bytes32(bytes20(msg.sender)), principalAmount); bytes memory encodedRequest = abi.encode(token, msg.sender, principalAmount); diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol index c6ad9afb..6fc91dd5 100644 --- a/src/libraries/Errors.sol +++ b/src/libraries/Errors.sol @@ -207,4 +207,7 @@ library Errors { /// @dev Vault: total principal unlock amount is larger than the total deposited amount error VaultTotalUnlockPrincipalExceedsDeposit(); + /// @dev Vault: forbid to deploy vault for the virtual token address representing natively staked ETH + error ForbidToDeployVault(); + } diff --git a/src/storage/BootstrapStorage.sol b/src/storage/BootstrapStorage.sol index 47abc4f5..7c0f1379 100644 --- a/src/storage/BootstrapStorage.sol +++ b/src/storage/BootstrapStorage.sol @@ -5,6 +5,8 @@ import {BeaconProxyBytecode} from "../core/BeaconProxyBytecode.sol"; import {Vault} from "../core/Vault.sol"; import {IValidatorRegistry} from "../interfaces/IValidatorRegistry.sol"; import {IVault} from "../interfaces/IVault.sol"; + +import {Errors} from "../libraries/Errors.sol"; import {GatewayStorage} from "./GatewayStorage.sol"; import {IBeacon} from "@openzeppelin/contracts/proxy/beacon/IBeacon.sol"; import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; @@ -146,6 +148,9 @@ contract BootstrapStorage is GatewayStorage { /// @dev A mapping of validator names to a boolean indicating whether the name has been used. mapping(string name => bool used) public validatorNameInUse; + /// @dev The (virtual) address for staked ETH. + address internal constant VIRTUAL_STAKED_ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + /* -------------------------------------------------------------------------- */ /* Events */ /* -------------------------------------------------------------------------- */ @@ -335,6 +340,10 @@ contract BootstrapStorage is GatewayStorage { // array, so it would not cause collision for encodePacked // slither-disable-next-line encode-packed-collision function _deployVault(address underlyingToken) internal returns (IVault) { + if (underlyingToken == VIRTUAL_STAKED_ETH_ADDRESS) { + revert Errors.ForbidToDeployVault(); + } + Vault vault = Vault( Create2.deploy( 0, diff --git a/src/storage/ClientChainGatewayStorage.sol b/src/storage/ClientChainGatewayStorage.sol index d7c8daf0..5219606e 100644 --- a/src/storage/ClientChainGatewayStorage.sol +++ b/src/storage/ClientChainGatewayStorage.sol @@ -34,9 +34,6 @@ contract ClientChainGatewayStorage is BootstrapStorage { /// @dev The length of the token address in bytes. uint256 internal constant TOKEN_ADDRESS_BYTES_LENGTH = 32; - /// @dev The (virtual) address for staked ETH. - address internal constant VIRTUAL_STAKED_ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; - /// @dev The address of the ETHPOS deposit contract. IETHPOSDeposit internal constant ETH_POS = IETHPOSDeposit(0x00000000219ab540356cBB839Cbe05303d7705Fa); diff --git a/test/foundry/unit/Bootstrap.t.sol b/test/foundry/unit/Bootstrap.t.sol index 4a19509a..c59f6d37 100644 --- a/test/foundry/unit/Bootstrap.t.sol +++ b/test/foundry/unit/Bootstrap.t.sol @@ -65,6 +65,7 @@ contract BootstrapTest is Test { IBeacon capsuleBeacon; BeaconProxyBytecode beaconProxyBytecode; + address internal constant VIRTUAL_STAKED_ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; bytes constant BEACON_PROXY_BYTECODE = hex"608060405260405161090e38038061090e83398101604081905261002291610460565b61002e82826000610035565b505061058a565b61003e83610100565b6040516001600160a01b038416907f1cf3b03a6cf19fa2baba4df148e9dcabedea7f8a5c07840e207e5c089be95d3e90600090a260008251118061007f5750805b156100fb576100f9836001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156100c5573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100e99190610520565b836102a360201b6100291760201c565b505b505050565b610113816102cf60201b6100551760201c565b6101725760405162461bcd60e51b815260206004820152602560248201527f455243313936373a206e657720626561636f6e206973206e6f74206120636f6e6044820152641d1c9858dd60da1b60648201526084015b60405180910390fd5b6101e6816001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156101b3573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101d79190610520565b6102cf60201b6100551760201c565b61024b5760405162461bcd60e51b815260206004820152603060248201527f455243313936373a20626561636f6e20696d706c656d656e746174696f6e206960448201526f1cc81b9bdd08184818dbdb9d1c9858dd60821b6064820152608401610169565b806102827fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d5060001b6102de60201b6100641760201c565b80546001600160a01b0319166001600160a01b039290921691909117905550565b60606102c883836040518060600160405280602781526020016108e7602791396102e1565b9392505050565b6001600160a01b03163b151590565b90565b6060600080856001600160a01b0316856040516102fe919061053b565b600060405180830381855af49150503d8060008114610339576040519150601f19603f3d011682016040523d82523d6000602084013e61033e565b606091505b5090925090506103508683838761035a565b9695505050505050565b606083156103c65782516103bf576001600160a01b0385163b6103bf5760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e74726163740000006044820152606401610169565b50816103d0565b6103d083836103d8565b949350505050565b8151156103e85781518083602001fd5b8060405162461bcd60e51b81526004016101699190610557565b80516001600160a01b038116811461041957600080fd5b919050565b634e487b7160e01b600052604160045260246000fd5b60005b8381101561044f578181015183820152602001610437565b838111156100f95750506000910152565b6000806040838503121561047357600080fd5b61047c83610402565b60208401519092506001600160401b038082111561049957600080fd5b818501915085601f8301126104ad57600080fd5b8151818111156104bf576104bf61041e565b604051601f8201601f19908116603f011681019083821181831017156104e7576104e761041e565b8160405282815288602084870101111561050057600080fd5b610511836020830160208801610434565b80955050505050509250929050565b60006020828403121561053257600080fd5b6102c882610402565b6000825161054d818460208701610434565b9190910192915050565b6020815260008251806020840152610576816040850160208701610434565b601f01601f19169190910160400192915050565b61034e806105996000396000f3fe60806040523661001357610011610017565b005b6100115b610027610022610067565b610100565b565b606061004e83836040518060600160405280602781526020016102f260279139610124565b9392505050565b6001600160a01b03163b151590565b90565b600061009a7fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50546001600160a01b031690565b6001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156100d7573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100fb9190610249565b905090565b3660008037600080366000845af43d6000803e80801561011f573d6000f35b3d6000fd5b6060600080856001600160a01b03168560405161014191906102a2565b600060405180830381855af49150503d806000811461017c576040519150601f19603f3d011682016040523d82523d6000602084013e610181565b606091505b50915091506101928683838761019c565b9695505050505050565b6060831561020d578251610206576001600160a01b0385163b6102065760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e747261637400000060448201526064015b60405180910390fd5b5081610217565b610217838361021f565b949350505050565b81511561022f5781518083602001fd5b8060405162461bcd60e51b81526004016101fd91906102be565b60006020828403121561025b57600080fd5b81516001600160a01b038116811461004e57600080fd5b60005b8381101561028d578181015183820152602001610275565b8381111561029c576000848401525b50505050565b600082516102b4818460208701610272565b9190910192915050565b60208152600082518060208401526102dd816040850160208701610272565b601f01601f1916919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a2646970667358221220d51e81d3bc5ed20a26aeb05dce7e825c503b2061aa78628027300c8d65b9d89a64736f6c634300080c0033416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564"; @@ -175,6 +176,21 @@ contract BootstrapTest is Test { vm.stopPrank(); } + // test that the vault is not deployed for the virtual token address representing natively staked ETH + function test02_VaultNotDeployedForNativeStakedETH() public { + MyToken myTokenClone = new MyToken("MyToken", "MYT", 18, addrs, 1000 * 10 ** 18); + + vm.startPrank(deployer); + address[] memory addedWhitelistTokens = new address[](2); + addedWhitelistTokens[0] = address(myTokenClone); + addedWhitelistTokens[1] = VIRTUAL_STAKED_ETH_ADDRESS; + bootstrap.addWhitelistTokens(addedWhitelistTokens); + vm.stopPrank(); + + assertTrue(address(bootstrap.tokenToVault(address(myToken))) != address(0)); + assertTrue(address(bootstrap.tokenToVault(VIRTUAL_STAKED_ETH_ADDRESS)) == address(0)); + } + function test02_Deposit() public { // Distribute MyToken to addresses vm.startPrank(deployer); diff --git a/test/foundry/unit/ClientChainGateway.t.sol b/test/foundry/unit/ClientChainGateway.t.sol index 500540be..2c9bd7e6 100644 --- a/test/foundry/unit/ClientChainGateway.t.sol +++ b/test/foundry/unit/ClientChainGateway.t.sol @@ -3,6 +3,8 @@ pragma solidity ^0.8.19; import "@beacon-oracle/contracts/src/EigenLayerBeaconOracle.sol"; import "@layerzero-v2/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol"; + +import {Origin} from "@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"; @@ -275,7 +277,7 @@ contract Initialize is SetUp { } -contract withdrawNonBeaconChainETHFromCapsule is SetUp { +contract WithdrawNonBeaconChainETHFromCapsule is SetUp { using stdStorage for StdStorage; @@ -349,3 +351,58 @@ contract withdrawNonBeaconChainETHFromCapsule is SetUp { } } + +contract WithdrawalPrincipalFromExocore is SetUp { + + using stdStorage for StdStorage; + using AddressCast for address; + + address internal constant VIRTUAL_STAKED_ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + uint256 constant WITHDRAWAL_AMOUNT = 1 ether; + + address payable user; + + function setUp() public override { + super.setUp(); + + user = payable(players[0].addr); + vm.deal(user, 10 ether); + + bytes32[] memory tokens = new bytes32[](2); + tokens[0] = bytes32(bytes20(VIRTUAL_STAKED_ETH_ADDRESS)); + tokens[1] = bytes32(bytes20(address(restakeToken))); + + // Simulate adding VIRTUAL_STAKED_ETH_ADDRESS to whitelist via lzReceive + bytes memory message = + abi.encodePacked(GatewayStorage.Action.REQUEST_ADD_WHITELIST_TOKENS, uint8(tokens.length), tokens); + Origin memory origin = Origin({srcEid: exocoreChainId, sender: address(exocoreGateway).toBytes32(), nonce: 1}); + + vm.prank(address(clientChainLzEndpoint)); + clientGateway.lzReceive(origin, bytes32(0), message, address(0), bytes("")); + // assert that VIRTUAL_STAKED_ETH_ADDRESS and restake token is whitelisted + assertTrue(clientGateway.isWhitelistedToken(VIRTUAL_STAKED_ETH_ADDRESS)); + assertTrue(clientGateway.isWhitelistedToken(address(restakeToken))); + } + + function test_revert_withdrawVirtualStakedETH() public { + // Try to withdraw VIRTUAL_STAKED_ETH + vm.prank(user); + vm.expectRevert(BootstrapStorage.VaultNotExist.selector); + clientGateway.withdrawPrincipalFromExocore(VIRTUAL_STAKED_ETH_ADDRESS, WITHDRAWAL_AMOUNT); + } + + function test_revert_withdrawNonWhitelistedToken() public { + address nonWhitelistedToken = address(0x1234); + + vm.prank(players[0].addr); + vm.expectRevert("BootstrapStorage: token is not whitelisted"); + clientGateway.withdrawPrincipalFromExocore(nonWhitelistedToken, WITHDRAWAL_AMOUNT); + } + + function test_revert_withdrawZeroAmount() public { + vm.prank(user); + vm.expectRevert("BootstrapStorage: amount should be greater than zero"); + clientGateway.withdrawPrincipalFromExocore(address(restakeToken), 0); + } + +} diff --git a/test/mocks/NonShortCircuitEndpointV2Mock.sol b/test/mocks/NonShortCircuitEndpointV2Mock.sol index b4c2b661..3da36f56 100644 --- a/test/mocks/NonShortCircuitEndpointV2Mock.sol +++ b/test/mocks/NonShortCircuitEndpointV2Mock.sol @@ -717,8 +717,7 @@ contract NonShortCircuitEndpointV2Mock is ILayerZeroEndpointV2, MessagingContext } } else if (optionType == ExecutorOptions.OPTION_TYPE_ORDERED_EXECUTION) { // ordered = true; - } - else { + } else { revert IExecutorFeeLib.Executor_UnsupportedOptionType(optionType); } }