diff --git a/docs/architecture.svg b/docs/architecture.svg index 58e4fbe6..d0d9603f 100644 --- a/docs/architecture.svg +++ b/docs/architecture.svg @@ -1 +1 @@ -IExocoreGatewayILSTRestakingControllerIVaultIExoCapsuleIBaseRestakingControllerILayerZeroReceiverIClientChainsIOAppReceiverIOAppCoreINativeRestakingControllerIBeaconChainOracleIETHPOSDepositITokenWhitelisterIValidatorRegistryPausableUpgradeableOwnableUpgradeableInitializableOAppUpgradeableOAppSenderUpgradeableOAppReceiverUpgradeableGatewayStorageBootstrapStorageClientChainGatewayStorageExocoreGatewayStorageExoCapsuleStorageBaseRestakingControllerclaim(address token, uint256 amount, address recipient)delegateTo(string calldata operator, address token, uint256 amount)undelegateFrom(string calldata operator, address token, uint256 amount)_processRequest(address token, address sender, uint256 amount, Action action, string memory operator)_sendMsgToExocore(Action action, bytes memory actionArgs)LSTRestakingControllerdeposit(address token, uint256 amount)withdrawPrincipleFromExocore(address token, uint256 principleAmount)withdrawRewardFromExocore(address token, uint256 rewardAmount)NativeRestakingControllernativeDeposit(uint256 amount)nativeWithdraw(uint256 amount)nativeDelegateTo(string calldata operator, uint256 amount)nativeUndelegateFrom(string calldata operator, uint256 amount)ExocoreGatewayinitialize(address payable exocoreValidatorSetAddress_)markBootstrapOnAllChains()pause()unpause()_lzReceive(Origin calldata _origin, bytes calldata payload)requestDeposit(uint32 srcChainId, uint64 lzNonce, bytes calldata payload)requestWithdrawPrinciple(uint32 srcChainId, uint64 lzNonce, bytes calldata payload)requestWithdrawReward(uint32 srcChainId, uint64 lzNonce, bytes calldata payload)requestDelegateTo(uint32 srcChainId, uint64 lzNonce, bytes calldata payload)requestUndelegateFrom(uint32 srcChainId, uint64 lzNonce, bytes calldata payload)_sendInterchainMsg(uint32 srcChainId, Action act, bytes memory actionArgs)quote(uint32 srcChainid, bytes memory _message)nextNonce(uint32 srcEid, bytes32 sender)_verifyAndUpdateNonce(uint32 srcEid, bytes32 sender, uint64 nonce)ClientChainGatewayinitialize(address endpoint_)pause()unpause()deposit(address token, uint256 amount)withdraw(address token, uint256 amount)getBalance(address token)transfer(address token, address to, uint256 amount)nativeDeposit(uint256 amount)nativeWithdraw(uint256 amount)nativeDelegateTo(string calldata operator, uint256 amount)nativeUndelegateFrom(string calldata operator, uint256 amount)ClientGatewayLzReceiver_lzReceive(Origin calldata _origin, bytes calldata payload)nextNonce(uint32 srcEid, bytes32 sender)_verifyAndUpdateNonce(uint32 srcEid, bytes32 sender, uint64 nonce)afterReceiveDepositResponse(bytes memory requestPayload, bytes calldata responsePayload)afterReceiveWithdrawPrincipleResponse(bytes memory requestPayload, bytes calldata responsePayload)afterReceiveWithdrawRewardResponse(bytes memory requestPayload, bytes calldata responsePayload)afterReceiveDelegateResponse(bytes memory requestPayload, bytes calldata responsePayload)afterReceiveUndelegateResponse(bytes memory requestPayload, bytes calldata responsePayload)ExoCapsuleinitialize(address gateway_, address capsuleOwner_, address beaconOracle_)verifyDepositProof(bytes32[] calldata validatorContainer, ValidatorContainerProof calldata proof)verifyPartialWithdrawalProof(bytes32[] calldata validatorContainer, ValidatorContainerProof calldata validatorProof, bytes32[] calldata withdrawalContainer, WithdrawalContainerProof calldata withdrawalProof)verifyFullWithdrawalProof(bytes32[] calldata validatorContainer, ValidatorContainerProof calldata validatorProof, bytes32[] calldata withdrawalContainer, WithdrawalContainerProof calldata withdrawalProof)withdraw(uint256 amount, address recipient)updatePrincipleBalance(uint256 lastlyUpdatedPrincipleBalance)updateWithdrawableBalance(uint256 unlockPrincipleAmount)capsuleWithdrawalCredentials() : bytesgetBeaconBlockRoot(uint256 timestamp) : bytes32getRegisteredValidatorByPubkey(bytes32 pubkey) : ValidatorgetRegisteredValidatorByIndex(uint256 index) : Validator_verifyValidatorContainer(bytes32[] calldata validatorContainer, ValidatorContainerProof calldata proof)_verifyWithdrawalContainer(bytes32[] calldata withdrawalContainer, WithdrawalContainerProof calldata proof)_isActivatedAtEpoch(bytes32[] calldata validatorContainer, uint256 atTimestamp) : bool_isStaleProof(Validator storage validator, uint256 proofTimestamp) : bool_hasFullyWithdrawn(bytes32[] calldata validatorContainer) : bool_timestampToEpoch(uint256 timestamp) : uint64Bootstrapinitialize(address owner, uint256 spawnTime_, uint256 offsetDuration_, address payable exocoreValidatorSetAddress_, address[] calldata whitelistTokens_, address customProxyAdmin_)pause()unpause()setSpawnTime(uint256 _spawnTime)setOffsetDuration(uint256 _offsetDuration)addWhitelistToken(address _token)removeWhitelistToken(address _token)registerOperator(string calldata operatorExocoreAddress, string calldata name, Commission memory commission, bytes32 consensusPublicKey)replaceKey(bytes32 newKey)updateRate(uint256 newRate)deposit(address token, uint256 amount)withdrawPrincipleFromExocore(address token, uint256 amount)withdrawRewardFromExocore(address token, uint256 amount)claim(address token, uint256 amount, address recipient)delegateTo(string calldata operator, address token, uint256 amount)undelegateFrom(string calldata operator, address token, uint256 amount)setClientChainGatewayLogic(address _clientChainGatewayLogic, bytes calldata _clientChainInitializationData)getOperatorsCount()getDepositorsCount()getWhitelistedTokensCount()getWhitelistedTokenAtIndex(uint256 index)IClientChainGatewayBootstrapLzReceiver \ No newline at end of file +IExocoreGatewayILSTRestakingControllerIVaultIExoCapsuleIBaseRestakingControllerILayerZeroReceiverIClientChainsIOAppReceiverIOAppCoreINativeRestakingControllerIBeaconChainOracleIETHPOSDepositITokenWhitelisterIValidatorRegistryPausableUpgradeableOwnableUpgradeableInitializableOAppUpgradeableOAppSenderUpgradeableOAppReceiverUpgradeableGatewayStorageBootstrapStorageClientChainGatewayStorageExocoreGatewayStorageExoCapsuleStorageBaseRestakingControllerclaim(address token, uint256 amount, address recipient)delegateTo(string calldata operator, address token, uint256 amount)undelegateFrom(string calldata operator, address token, uint256 amount)_processRequest(address token, address sender, uint256 amount, Action action, string memory operator)_sendMsgToExocore(Action action, bytes memory actionArgs)LSTRestakingControllerdeposit(address token, uint256 amount)withdrawPrincipleFromExocore(address token, uint256 principleAmount)withdrawRewardFromExocore(address token, uint256 rewardAmount)NativeRestakingControllernativeDeposit(uint256 amount)nativeWithdraw(uint256 amount)nativeDelegateTo(string calldata operator, uint256 amount)nativeUndelegateFrom(string calldata operator, uint256 amount)ExocoreGatewayinitialize(address payable exocoreValidatorSetAddress_)markBootstrap(uint32 chainId)pause()unpause()_lzReceive(Origin calldata _origin, bytes calldata payload)requestDeposit(uint32 srcChainId, uint64 lzNonce, bytes calldata payload)requestWithdrawPrinciple(uint32 srcChainId, uint64 lzNonce, bytes calldata payload)requestWithdrawReward(uint32 srcChainId, uint64 lzNonce, bytes calldata payload)requestDelegateTo(uint32 srcChainId, uint64 lzNonce, bytes calldata payload)requestUndelegateFrom(uint32 srcChainId, uint64 lzNonce, bytes calldata payload)_sendInterchainMsg(uint32 srcChainId, Action act, bytes memory actionArgs)quote(uint32 srcChainid, bytes memory _message)nextNonce(uint32 srcEid, bytes32 sender)_verifyAndUpdateNonce(uint32 srcEid, bytes32 sender, uint64 nonce)ClientChainGatewayinitialize(address endpoint_)pause()unpause()deposit(address token, uint256 amount)withdraw(address token, uint256 amount)getBalance(address token)transfer(address token, address to, uint256 amount)nativeDeposit(uint256 amount)nativeWithdraw(uint256 amount)nativeDelegateTo(string calldata operator, uint256 amount)nativeUndelegateFrom(string calldata operator, uint256 amount)ClientGatewayLzReceiver_lzReceive(Origin calldata _origin, bytes calldata payload)nextNonce(uint32 srcEid, bytes32 sender)_verifyAndUpdateNonce(uint32 srcEid, bytes32 sender, uint64 nonce)afterReceiveDepositResponse(bytes memory requestPayload, bytes calldata responsePayload)afterReceiveWithdrawPrincipleResponse(bytes memory requestPayload, bytes calldata responsePayload)afterReceiveWithdrawRewardResponse(bytes memory requestPayload, bytes calldata responsePayload)afterReceiveDelegateResponse(bytes memory requestPayload, bytes calldata responsePayload)afterReceiveUndelegateResponse(bytes memory requestPayload, bytes calldata responsePayload)ExoCapsuleinitialize(address gateway_, address capsuleOwner_, address beaconOracle_)verifyDepositProof(bytes32[] calldata validatorContainer, ValidatorContainerProof calldata proof)verifyPartialWithdrawalProof(bytes32[] calldata validatorContainer, ValidatorContainerProof calldata validatorProof, bytes32[] calldata withdrawalContainer, WithdrawalContainerProof calldata withdrawalProof)verifyFullWithdrawalProof(bytes32[] calldata validatorContainer, ValidatorContainerProof calldata validatorProof, bytes32[] calldata withdrawalContainer, WithdrawalContainerProof calldata withdrawalProof)withdraw(uint256 amount, address recipient)updatePrincipleBalance(uint256 lastlyUpdatedPrincipleBalance)updateWithdrawableBalance(uint256 unlockPrincipleAmount)capsuleWithdrawalCredentials() : bytesgetBeaconBlockRoot(uint256 timestamp) : bytes32getRegisteredValidatorByPubkey(bytes32 pubkey) : ValidatorgetRegisteredValidatorByIndex(uint256 index) : Validator_verifyValidatorContainer(bytes32[] calldata validatorContainer, ValidatorContainerProof calldata proof)_verifyWithdrawalContainer(bytes32[] calldata withdrawalContainer, WithdrawalContainerProof calldata proof)_isActivatedAtEpoch(bytes32[] calldata validatorContainer, uint256 atTimestamp) : bool_isStaleProof(Validator storage validator, uint256 proofTimestamp) : bool_hasFullyWithdrawn(bytes32[] calldata validatorContainer) : bool_timestampToEpoch(uint256 timestamp) : uint64Bootstrapinitialize(address owner, uint256 spawnTime_, uint256 offsetDuration_, address payable exocoreValidatorSetAddress_, address[] calldata whitelistTokens_, address customProxyAdmin_)pause()unpause()setSpawnTime(uint256 _spawnTime)setOffsetDuration(uint256 _offsetDuration)addWhitelistToken(address _token)removeWhitelistToken(address _token)registerOperator(string calldata operatorExocoreAddress, string calldata name, Commission memory commission, bytes32 consensusPublicKey)replaceKey(bytes32 newKey)updateRate(uint256 newRate)deposit(address token, uint256 amount)withdrawPrincipleFromExocore(address token, uint256 amount)withdrawRewardFromExocore(address token, uint256 amount)claim(address token, uint256 amount, address recipient)delegateTo(string calldata operator, address token, uint256 amount)undelegateFrom(string calldata operator, address token, uint256 amount)setClientChainGatewayLogic(address _clientChainGatewayLogic, bytes calldata _clientChainInitializationData)getOperatorsCount()getDepositorsCount()getWhitelistedTokensCount()getWhitelistedTokenAtIndex(uint256 index)IClientChainGatewayBootstrapLzReceiver \ No newline at end of file diff --git a/script/11_SetPeers.s.sol b/script/11_SetPeers.s.sol index de1b7a50..f9024267 100644 --- a/script/11_SetPeers.s.sol +++ b/script/11_SetPeers.s.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.19; import {Bootstrap} from "../src/core/Bootstrap.sol"; import {ExocoreGateway} from "../src/core/ExocoreGateway.sol"; +import {GatewayStorage} from "../src/storage/GatewayStorage.sol"; import {BaseScript} from "./BaseScript.sol"; import "forge-std/Script.sol"; @@ -53,12 +54,9 @@ contract SetPeersAndUpgrade is BaseScript { vm.selectFork(exocore); vm.startBroadcast(exocoreValidatorSet.privateKey); - // fund the gateway - if (exocoreGatewayAddr.balance < 1 ether) { - (bool sent,) = exocoreGatewayAddr.call{value: 1 ether}(""); - require(sent, "Failed to send Ether"); - } - gateway.markBootstrapOnAllChains(); + uint256 nativeFee = + exocoreGateway.quote(clientChainId, abi.encodePacked(GatewayStorage.Action.REQUEST_MARK_BOOTSTRAP, "")); + exocoreGateway.markBootstrap{value: nativeFee}(clientChainId); } } diff --git a/script/14_CorrectBootstrapErrors.s.sol b/script/14_CorrectBootstrapErrors.s.sol index a4ca684c..df419c4a 100644 --- a/script/14_CorrectBootstrapErrors.s.sol +++ b/script/14_CorrectBootstrapErrors.s.sol @@ -29,6 +29,8 @@ contract CorrectBootstrapErrors is BaseScript { address wstETH; address proxyAddress; address proxyAdmin; + address clientGatewayLogic; + bytes initialization; function setUp() public virtual override { // load keys @@ -62,6 +64,9 @@ contract CorrectBootstrapErrors is BaseScript { require(address(vaultImplementation) != address(0), "vault implementation should not be empty"); vaultBeacon = UpgradeableBeacon(stdJson.readAddress(deployed, ".clientChain.vaultBeacon")); require(address(vaultBeacon) != address(0), "vault beacon should not be empty"); + clientGatewayLogic = stdJson.readAddress(deployed, ".clientChain.clientGatewayLogic"); + require(clientGatewayLogic != address(0), "client gateway should not be empty"); + initialization = abi.encodeCall(ClientChainGateway.initialize, (payable(exocoreValidatorSet.addr))); } function run() public { @@ -81,7 +86,9 @@ contract CorrectBootstrapErrors is BaseScript { block.timestamp + 168 hours, 2 seconds, emptyList, - address(proxyAdmin) + address(proxyAdmin), + address(clientGateway), + initialization ) ); proxyAdmin.upgradeAndCall(ITransparentUpgradeableProxy(proxyAddress), address(bootstrapLogic), data); diff --git a/script/7_DeployBootstrap.s.sol b/script/7_DeployBootstrap.s.sol index 775cd342..7ecdbf45 100644 --- a/script/7_DeployBootstrap.s.sol +++ b/script/7_DeployBootstrap.s.sol @@ -60,6 +60,21 @@ contract DeployBootstrapOnly is BaseScript { Bootstrap bootstrapLogic = new Bootstrap( address(clientChainLzEndpoint), exocoreChainId, address(vaultBeacon), address(beaconProxyBytecode) ); + // client chain constructor (upgrade details) + capsuleImplementation = new ExoCapsule(); + capsuleBeacon = new UpgradeableBeacon(address(capsuleImplementation)); + ClientChainGateway clientGatewayLogic = new ClientChainGateway( + address(clientChainLzEndpoint), + exocoreChainId, + address(beaconOracle), + address(vaultBeacon), + address(capsuleBeacon), + address(beaconProxyBytecode) + ); + // then the client chain initialization + address[] memory emptyList; + bytes memory initialization = + abi.encodeWithSelector(clientGatewayLogic.initialize.selector, exocoreValidatorSet.addr, emptyList); // bootstrap implementation Bootstrap bootstrap = Bootstrap( payable( @@ -75,7 +90,9 @@ contract DeployBootstrapOnly is BaseScript { block.timestamp + 168 hours, 2 seconds, whitelistTokens, // vault is auto deployed - address(proxyAdmin) + address(proxyAdmin), + address(clientGatewayLogic), + initialization ) ) ) @@ -86,23 +103,6 @@ contract DeployBootstrapOnly is BaseScript { // initialize proxyAdmin with bootstrap address proxyAdmin.initialize(address(bootstrap)); - // now, focus on the client chain constructor - capsuleImplementation = new ExoCapsule(); - capsuleBeacon = new UpgradeableBeacon(address(capsuleImplementation)); - ClientChainGateway clientGatewayLogic = new ClientChainGateway( - address(clientChainLzEndpoint), - exocoreChainId, - address(beaconOracle), - address(vaultBeacon), - address(capsuleBeacon), - address(beaconProxyBytecode) - ); - // then the client chain initialization - address[] memory emptyList; - bytes memory initialization = - abi.encodeWithSelector(clientGatewayLogic.initialize.selector, exocoreValidatorSet.addr, emptyList); - bootstrap.setClientChainGatewayLogic(address(clientGatewayLogic), initialization); - vm.stopBroadcast(); string memory clientChainContracts = "clientChainContracts"; diff --git a/script/integration/1_DeployBootstrap.s.sol b/script/integration/1_DeployBootstrap.s.sol index 5c659c4e..750f31d0 100644 --- a/script/integration/1_DeployBootstrap.s.sol +++ b/script/integration/1_DeployBootstrap.s.sol @@ -130,7 +130,9 @@ contract DeployContracts is Script { block.timestamp + 3 minutes, 1 seconds, whitelistTokens, - address(proxyAdmin) + address(proxyAdmin), + address(0x1), // these values don't matter for the localnet generate.js test + bytes("123456") ) ) ) diff --git a/src/core/Bootstrap.sol b/src/core/Bootstrap.sol index 3562aa6f..8c93795a 100644 --- a/src/core/Bootstrap.sol +++ b/src/core/Bootstrap.sol @@ -63,7 +63,9 @@ contract Bootstrap is uint256 spawnTime_, uint256 offsetDuration_, address[] calldata whitelistTokens_, - address customProxyAdmin_ + address customProxyAdmin_, + address clientChainGatewayLogic_, + bytes calldata clientChainInitializationData_ ) external initializer { if (owner == address(0)) { revert Errors.ZeroAddress(); @@ -83,6 +85,7 @@ contract Bootstrap is customProxyAdmin = customProxyAdmin_; bootstrapped = false; + _setClientChainGatewayLogic(clientChainGatewayLogic_, clientChainInitializationData_); // msg.sender is not the proxy admin but the transparent proxy itself, and hence, // cannot be used here. we must require a separate owner. since the Exocore validator @@ -546,6 +549,7 @@ contract Bootstrap is /// initialization data must be set. The contract must not have been bootstrapped before. /// Once it is marked bootstrapped, the implementation of the contract is upgraded to the /// client chain gateway logic contract. + /// @dev This call can never fail, since such failures are not handled by ExocoreGateway. function markBootstrapped() public onlyCalledFromThis whenNotPaused { // whenNotPaused is applied so that the upgrade does not proceed without unpausing it. // LZ checks made so far include: @@ -553,24 +557,28 @@ contract Bootstrap is // correct address on remote (peer match) // chainId match // nonce match, which requires that inbound nonce is uint64(1). - // TSS checks are not super clear since they can be set by anyone - // but at this point that does not matter since it is not fully implemented anyway. if (block.timestamp < spawnTime) { - revert Errors.BootstrapNotSpawnTime(); + // technically never possible unless the block producer does some time-based shenanigans. + emit BootstrapNotTimeYet(); + return; } + // bootstrapped = true is only actioned by the clientchaingateway after upgrade + // so no need to check for that here but better to be safe. if (bootstrapped) { - revert Errors.BootstrapAlreadyBootstrapped(); + emit BootstrappedAlready(); + return; } - if (clientChainGatewayLogic == address(0)) { - revert Errors.ZeroAddress(); - } - ICustomProxyAdmin(customProxyAdmin).changeImplementation( + try ICustomProxyAdmin(customProxyAdmin).changeImplementation( // address(this) is storage address and not logic address. so it is a proxy. ITransparentUpgradeableProxy(address(this)), clientChainGatewayLogic, clientChainInitializationData - ); - emit Bootstrapped(); + ) { + emit Bootstrapped(); + } catch { + // to allow retries, never fail + emit BootstrapUpgradeFailed(); + } } /// @notice Sets a new client chain gateway logic and its initialization data. @@ -584,9 +592,22 @@ contract Bootstrap is public onlyOwner { + _setClientChainGatewayLogic(_clientChainGatewayLogic, _clientChainInitializationData); + } + + /// @dev Internal version of `setClientChainGatewayLogic`. + /// @param _clientChainGatewayLogic The address of the new client chain gateway logic + /// contract. + /// @param _clientChainInitializationData The initialization data to be used when setting up + /// the new logic contract. + function _setClientChainGatewayLogic( + address _clientChainGatewayLogic, + bytes calldata _clientChainInitializationData + ) internal { if (_clientChainGatewayLogic == address(0)) { revert Errors.ZeroAddress(); } + // selector is 4 bytes long if (_clientChainInitializationData.length < 4) { revert Errors.BootstrapClientChainDataMalformed(); } diff --git a/src/core/ClientChainGateway.sol b/src/core/ClientChainGateway.sol index c652bded..8ce2db1d 100644 --- a/src/core/ClientChainGateway.sol +++ b/src/core/ClientChainGateway.sol @@ -71,6 +71,8 @@ contract ClientChainGateway is _whiteListFunctionSelectors[Action.REQUEST_ADD_WHITELIST_TOKENS] = this.afterReceiveAddWhitelistTokensRequest.selector; + // overwrite the bootstrap function selector + _whiteListFunctionSelectors[Action.REQUEST_MARK_BOOTSTRAP] = this.afterReceiveMarkBootstrapRequest.selector; bootstrapped = true; @@ -82,8 +84,6 @@ contract ClientChainGateway is /// @dev Clears the bootstrap data. function _clearBootstrapData() internal { - // mandatory to clear! - delete _whiteListFunctionSelectors[Action.REQUEST_MARK_BOOTSTRAP]; // the set below is recommended to clear, so that any possibilities of upgrades // can then be removed. delete customProxyAdmin; diff --git a/src/core/ClientGatewayLzReceiver.sol b/src/core/ClientGatewayLzReceiver.sol index 3834ec2e..792ea1ef 100644 --- a/src/core/ClientGatewayLzReceiver.sol +++ b/src/core/ClientGatewayLzReceiver.sol @@ -327,4 +327,12 @@ abstract contract ClientGatewayLzReceiver is PausableUpgradeable, OAppReceiverUp } } + /// @notice Called after a mark-bootstrap response is received. + /// @dev Since the contract is already bootstrapped (if we are here), there is nothing to do. + /// @dev Failing this, however, will cause a nonce mismatch resulting in a system halt. + /// Hence, we silently ignore this call. + function afterReceiveMarkBootstrapRequest() public onlyCalledFromThis whenNotPaused { + emit BootstrappedAlready(); + } + } diff --git a/src/core/ExocoreGateway.sol b/src/core/ExocoreGateway.sol index 5a0ea9f3..9fbe64e5 100644 --- a/src/core/ExocoreGateway.sol +++ b/src/core/ExocoreGateway.sol @@ -98,29 +98,20 @@ contract ExocoreGateway is _unpause(); } - /// @notice Marks the bootstrap on all chains. - /// @dev This function obtains a list of client chain ids from the precompile, and then - /// sends a `REQUEST_MARK_BOOTSTRAP` to all of them. In response, the Bootstrap contract - /// on those chains should upgrade itself to the ClientChainGateway contract. - /// This function should be the first to be called after the LZ infrastructure is ready. - // 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 nonReentrant { - (bool success, uint32[] memory chainIndices) = ASSETS_CONTRACT.getClientChains(); - if (!success) { - revert Errors.ExocoreGatewayFailedToGetClientChainIds(); - } - for (uint256 i = 0; i < chainIndices.length; ++i) { - uint32 chainIndex = chainIndices[i]; - if (!chainToBootstrapped[chainIndex]) { - _sendInterchainMsg(chainIndex, Action.REQUEST_MARK_BOOTSTRAP, "", true); - // TODO: should this be marked only upon receiving a response? - chainToBootstrapped[chainIndex] = true; - emit BootstrapRequestSent(chainIndex); - } - } + /// @notice Sends a request to mark the bootstrap on a chain. + /// @param chainIndex The index of the chain. + /// @dev This function is useful if the bootstrap failed on a chain and needs to be retried. + function markBootstrap(uint32 chainIndex) public payable whenNotPaused nonReentrant { + _markBootstrap(chainIndex); + } + + /// @dev Internal function to mark the bootstrap on a chain. + /// @param chainIndex The index of the chain. + function _markBootstrap(uint32 chainIndex) internal { + // we don't track that a request was sent to a chain to allow for retrials + // if the transaction fails on the destination chain + _sendInterchainMsg(chainIndex, Action.REQUEST_MARK_BOOTSTRAP, "", false); + emit BootstrapRequestSent(chainIndex); } /// @inheritdoc IExocoreGateway diff --git a/src/interfaces/IExocoreGateway.sol b/src/interfaces/IExocoreGateway.sol index c43e2dbe..76bca735 100644 --- a/src/interfaces/IExocoreGateway.sol +++ b/src/interfaces/IExocoreGateway.sol @@ -58,4 +58,11 @@ interface IExocoreGateway is IOAppReceiver, IOAppCore { string[] calldata metaData ) external payable; + /// @notice Marks the network as bootstrapped, on the client chain. + /// @dev Causes an upgrade of the Bootstrap contract to the ClientChainGateway contract. + /// @dev Only works if LZ infrastructure is set up and SetPeer has been called. + /// @dev This is payable because it requires a fee to be paid to LZ. + /// @param clientChainId The LayerZero chain id of the client chain. + function markBootstrap(uint32 clientChainId) external payable; + } diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol index 6fc91dd5..eec6caee 100644 --- a/src/libraries/Errors.sol +++ b/src/libraries/Errors.sol @@ -94,12 +94,6 @@ library Errors { /// @dev Bootstrap: no ether required for delegation/undelegation error BootstrapNoEtherForDelegation(); - /// @dev Bootstrap: not yet in the bootstrap time - error BootstrapNotSpawnTime(); - - /// @dev Bootstrap: not yet bootstrapped - error BootstrapAlreadyBootstrapped(); - /// @dev Bootstrap: client chain initialization data is malformed error BootstrapClientChainDataMalformed(); diff --git a/src/storage/BootstrapStorage.sol b/src/storage/BootstrapStorage.sol index 7c0f1379..c7d6b85f 100644 --- a/src/storage/BootstrapStorage.sol +++ b/src/storage/BootstrapStorage.sol @@ -227,6 +227,20 @@ contract BootstrapStorage is GatewayStorage { /// trigger this event. event Bootstrapped(); + /// @notice Emitted when a mark bootstrap call is received before the spawn time. + /// @dev This event is triggered when a mark bootstrap call is received before the spawn time. + event BootstrapNotTimeYet(); + + /// @notice Emitted if the bootstrap upgrade to client chain gateway fails. + /// @dev This event is triggered if the upgrade from Bootstrap to Client Chain Gateway fails. It is not an error + /// intentionally to prevent blocking the system. + event BootstrapUpgradeFailed(); + + /// @notice Emitted when the contract is already bootstrapped. + /// @dev This event is triggered when the contract is already bootstrapped and an attempt is made to bootstrap it + /// again. It is not an error intentionally to prevent blocking the system. + event BootstrappedAlready(); + /// @notice Emitted when the client chain gateway logic + implementation are updated. /// @dev This event is triggered whenever the client chain gateway logic and implementation are updated. It may be /// used, before bootstrapping is complete, to upgrade the client chain gateway logic for upgrades or other bugs. diff --git a/src/storage/ExocoreGatewayStorage.sol b/src/storage/ExocoreGatewayStorage.sol index 04560ffc..35415e8f 100644 --- a/src/storage/ExocoreGatewayStorage.sol +++ b/src/storage/ExocoreGatewayStorage.sol @@ -43,10 +43,6 @@ contract ExocoreGatewayStorage is GatewayStorage { /// @dev The msg.value for all the destination chains. uint128 internal constant DESTINATION_MSG_VALUE = 0; - /// @notice A mapping from client chain IDs to whether the chain has been bootstrapped. - /// @dev Used to ensure no repeated bootstrap requests are sent. - mapping(uint32 clienChainId => bool) public chainToBootstrapped; - /// @notice Emitted when a precompile call fails. /// @param precompile Address of the precompile contract. /// @param nonce The LayerZero nonce diff --git a/test/foundry/unit/Bootstrap.t.sol b/test/foundry/unit/Bootstrap.t.sol index c59f6d37..35955bec 100644 --- a/test/foundry/unit/Bootstrap.t.sol +++ b/test/foundry/unit/Bootstrap.t.sol @@ -58,6 +58,7 @@ contract BootstrapTest is Test { address exocoreValidatorSet = vm.addr(uint256(0x8)); address undeployedExocoreGateway = vm.addr(uint256(0x9)); address undeployedExocoreLzEndpoint = vm.addr(uint256(0xb)); + address constant lzActor = address(0x20); IVault vaultImplementation; IExoCapsule capsuleImplementation; @@ -100,6 +101,21 @@ contract BootstrapTest is Test { Bootstrap bootstrapLogic = new Bootstrap( address(clientChainLzEndpoint), exocoreChainId, address(vaultBeacon), address(beaconProxyBytecode) ); + // set up the upgrade params + // deploy capsule implementation contract that has logics called by proxy + capsuleImplementation = new ExoCapsule(); + // deploy the capsule beacon that store the implementation contract address + capsuleBeacon = new UpgradeableBeacon(address(capsuleImplementation)); + ClientChainGateway clientGatewayLogic = new ClientChainGateway( + address(clientChainLzEndpoint), + exocoreChainId, + address(0x1), + address(vaultBeacon), + address(capsuleBeacon), + address(beaconProxyBytecode) + ); + // we could also use encodeWithSelector and supply .initialize.selector instead. + bytes memory initialization = abi.encodeCall(clientGatewayLogic.initialize, (payable(exocoreValidatorSet))); // then the params + proxy spawnTime = block.timestamp + 1 hours; offsetDuration = 30 minutes; @@ -111,7 +127,15 @@ contract BootstrapTest is Test { address(proxyAdmin), abi.encodeCall( bootstrap.initialize, - (deployer, spawnTime, offsetDuration, whitelistTokens, address(proxyAdmin)) + ( + deployer, + spawnTime, + offsetDuration, + whitelistTokens, + address(proxyAdmin), + address(clientGatewayLogic), + initialization + ) ) ) ) @@ -133,25 +157,6 @@ contract BootstrapTest is Test { // now set the gateway address for Exocore. clientChainLzEndpoint.setDestLzEndpoint(undeployedExocoreGateway, undeployedExocoreLzEndpoint); bootstrap.setPeer(exocoreChainId, bytes32(bytes20(undeployedExocoreGateway))); - // lastly set up the upgrade params - - // deploy capsule implementation contract that has logics called by proxy - capsuleImplementation = new ExoCapsule(); - - // deploy the capsule beacon that store the implementation contract address - capsuleBeacon = new UpgradeableBeacon(address(capsuleImplementation)); - - ClientChainGateway clientGatewayLogic = new ClientChainGateway( - address(clientChainLzEndpoint), - exocoreChainId, - address(0x1), - address(vaultBeacon), - address(capsuleBeacon), - address(beaconProxyBytecode) - ); - // we could also use encodeWithSelector and supply .initialize.selector instead. - bytes memory initialization = abi.encodeCall(clientGatewayLogic.initialize, (payable(exocoreValidatorSet))); - bootstrap.setClientChainGatewayLogic(address(clientGatewayLogic), initialization); vm.stopPrank(); } @@ -885,64 +890,74 @@ contract BootstrapTest is Test { } function test12_MarkBootstrapped() public { + // go after spawn time vm.warp(spawnTime + 1); - vm.startPrank(address(0x20)); + _markBootstrapped(1, true); + } + + function _markBootstrapped(uint64 nonce, bool success) internal { + vm.startPrank(lzActor); clientChainLzEndpoint.lzReceive( - Origin(exocoreChainId, bytes32(bytes20(undeployedExocoreGateway)), uint64(1)), + Origin(exocoreChainId, bytes32(bytes20(undeployedExocoreGateway)), nonce), address(bootstrap), - generateUID(1), + generateUID(nonce), abi.encodePacked(GatewayStorage.Action.REQUEST_MARK_BOOTSTRAP, ""), bytes("") ); vm.stopPrank(); - assertTrue(bootstrap.bootstrapped()); - // ensure that it cannot be upgraded ever again. - assertTrue(bootstrap.customProxyAdmin() == address(0)); - assertTrue(proxyAdmin.bootstrapper() == address(0)); - assertTrue(bootstrap.owner() == exocoreValidatorSet); - // getDepositorsCount is no longer a function so can't check the count. - // assertTrue(bootstrap.getDepositorsCount() == 0); + if (success) { + assertTrue(bootstrap.bootstrapped()); + // no more upgrades are possible + assertTrue(bootstrap.customProxyAdmin() == address(0)); + assertTrue(proxyAdmin.bootstrapper() == address(0)); + assertTrue(bootstrap.owner() == exocoreValidatorSet); + } else { + assertFalse(bootstrap.bootstrapped()); + } } function test12_MarkBootstrapped_NotTime() public { - vm.startPrank(address(0x20)); - clientChainLzEndpoint.lzReceive( - Origin(exocoreChainId, bytes32(bytes20(undeployedExocoreGateway)), uint64(1)), - address(bootstrap), - generateUID(1), - abi.encodePacked(GatewayStorage.Action.REQUEST_MARK_BOOTSTRAP, ""), - bytes("") - ); - vm.stopPrank(); - assertFalse(bootstrap.bootstrapped()); + // spawn time is 1 hour later, so this will fail. + _markBootstrapped(1, false); } function test12_MarkBootstrapped_AlreadyBootstrapped() public { - test12_MarkBootstrapped(); - vm.startPrank(address(clientChainLzEndpoint)); - vm.expectRevert( - abi.encodeWithSelector( - GatewayStorage.UnsupportedRequest.selector, GatewayStorage.Action.REQUEST_MARK_BOOTSTRAP - ) - ); - bootstrap.lzReceive( - Origin(exocoreChainId, bytes32(bytes20(undeployedExocoreGateway)), uint64(2)), - generateUID(1), - abi.encodePacked(GatewayStorage.Action.REQUEST_MARK_BOOTSTRAP, ""), - address(0), - bytes("") - ); + vm.warp(spawnTime + 1); + _markBootstrapped(1, true); + vm.expectEmit(address(bootstrap)); + emit BootstrapStorage.BootstrappedAlready(); + _markBootstrapped(2, true); vm.stopPrank(); } function test12_MarkBootstrapped_DirectCall() public { - vm.startPrank(address(0x20)); + // can be any adddress but for clarity use non lz actor + vm.startPrank(address(0x21)); vm.warp(spawnTime + 2); vm.expectRevert(Errors.BootstrapLzReceiverOnlyCalledFromThis.selector); bootstrap.markBootstrapped(); vm.stopPrank(); } + function test12_MarkBootstrapped_FailThenSucceed() public { + vm.warp(spawnTime - 5); + _markBootstrapped(1, false); + vm.warp(spawnTime + 1); + _markBootstrapped(2, true); + } + + function test12_MarkBootstrapped_FailThenSucceed2x() public { + vm.warp(spawnTime - 5); + _markBootstrapped(1, false); + vm.warp(spawnTime + 1); + _markBootstrapped(2, true); + // silently succeeds and does not block the system after bootstrapping + vm.warp(spawnTime + 10); + vm.expectEmit(address(bootstrap)); + emit BootstrapStorage.BootstrappedAlready(); + _markBootstrapped(3, true); + } + function test13_OperationAllowed() public { vm.warp(spawnTime - offsetDuration); vm.startPrank(addrs[0]); @@ -1005,7 +1020,15 @@ contract BootstrapTest is Test { address(proxyAdmin), abi.encodeCall( bootstrap.initialize, - (address(0x0), spawnTime, offsetDuration, whitelistTokens, address(proxyAdmin)) + ( + address(0x0), + spawnTime, + offsetDuration, + whitelistTokens, + address(proxyAdmin), + address(0x1), + bytes("123456") + ) ) ) ) @@ -1028,7 +1051,15 @@ contract BootstrapTest is Test { address(proxyAdmin), abi.encodeCall( bootstrap.initialize, - (deployer, block.timestamp - 10, offsetDuration, whitelistTokens, address(proxyAdmin)) + ( + deployer, + block.timestamp - 10, + offsetDuration, + whitelistTokens, + address(proxyAdmin), + address(0x1), + bytes("123456") + ) ) ) ) @@ -1049,7 +1080,16 @@ contract BootstrapTest is Test { address(bootstrapLogic), address(proxyAdmin), abi.encodeCall( - bootstrap.initialize, (deployer, spawnTime, 0, whitelistTokens, address(proxyAdmin)) + bootstrap.initialize, + ( + deployer, + spawnTime, + 0, + whitelistTokens, + address(proxyAdmin), + address(0x1), + bytes("123456") + ) ) ) ) @@ -1070,7 +1110,10 @@ contract BootstrapTest is Test { new TransparentUpgradeableProxy( address(bootstrapLogic), address(proxyAdmin), - abi.encodeCall(bootstrap.initialize, (deployer, 21, 22, whitelistTokens, address(proxyAdmin))) + abi.encodeCall( + bootstrap.initialize, + (deployer, 21, 22, whitelistTokens, address(proxyAdmin), address(0x1), bytes("123456")) + ) ) ) ) @@ -1090,7 +1133,10 @@ contract BootstrapTest is Test { new TransparentUpgradeableProxy( address(bootstrapLogic), address(proxyAdmin), - abi.encodeCall(bootstrap.initialize, (deployer, 21, 9, whitelistTokens, address(proxyAdmin))) + abi.encodeCall( + bootstrap.initialize, + (deployer, 21, 9, whitelistTokens, address(proxyAdmin), address(0x1), bytes("123456")) + ) ) ) ) @@ -1110,7 +1156,76 @@ contract BootstrapTest is Test { address(bootstrapLogic), address(proxyAdmin), abi.encodeCall( - bootstrap.initialize, (deployer, spawnTime, offsetDuration, whitelistTokens, address(0x0)) + bootstrap.initialize, + ( + deployer, + spawnTime, + offsetDuration, + whitelistTokens, + address(0x0), + address(0x1), + bytes("123456") + ) + ) + ) + ) + ) + ); + } + + function test15_Initialize_GatewayZero() public { + vm.startPrank(deployer); + Bootstrap bootstrapLogic = new Bootstrap( + address(clientChainLzEndpoint), exocoreChainId, address(vaultBeacon), address(beaconProxyBytecode) + ); + vm.expectRevert(Errors.ZeroAddress.selector); + Bootstrap( + payable( + address( + new TransparentUpgradeableProxy( + address(bootstrapLogic), + address(proxyAdmin), + abi.encodeCall( + bootstrap.initialize, + ( + deployer, + spawnTime, + offsetDuration, + whitelistTokens, + address(proxyAdmin), + address(0x0), + bytes("123456") + ) + ) + ) + ) + ) + ); + } + + function test15_Initialize_GatewayLogicZero() public { + vm.startPrank(deployer); + Bootstrap bootstrapLogic = new Bootstrap( + address(clientChainLzEndpoint), exocoreChainId, address(vaultBeacon), address(beaconProxyBytecode) + ); + vm.expectRevert(Errors.BootstrapClientChainDataMalformed.selector); + Bootstrap( + payable( + address( + new TransparentUpgradeableProxy( + address(bootstrapLogic), + address(proxyAdmin), + abi.encodeCall( + bootstrap.initialize, + ( + deployer, + spawnTime, + offsetDuration, + whitelistTokens, + address(proxyAdmin), + address(0x1), + bytes("") + ) ) ) ) diff --git a/test/foundry/unit/ExocoreGateway.t.sol b/test/foundry/unit/ExocoreGateway.t.sol index c6c9c24c..7a27766c 100644 --- a/test/foundry/unit/ExocoreGateway.t.sol +++ b/test/foundry/unit/ExocoreGateway.t.sol @@ -886,50 +886,27 @@ contract AssociateOperatorWithEVMStaker is SetUp { contract MarkBootstrap is SetUp { - uint32 anotherClientChainId = clientChainId; + uint256 nativeFee; - function test_Setup() public { - assertEq(exocoreGateway.chainToBootstrapped(clientChainId), false); + error NoPeer(uint32 chainId); + + function setUp() public virtual override { + super.setUp(); + nativeFee = + exocoreGateway.quote(clientChainId, abi.encodePacked(GatewayStorage.Action.REQUEST_MARK_BOOTSTRAP, "")); } function test_Success() public { vm.startPrank(exocoreValidatorSet.addr); vm.expectEmit(address(exocoreGateway)); emit ExocoreGatewayStorage.BootstrapRequestSent(clientChainId); - exocoreGateway.markBootstrapOnAllChains(); - assertEq(exocoreGateway.chainToBootstrapped(clientChainId), true); + exocoreGateway.markBootstrap{value: nativeFee}(clientChainId); } - function test_Success_Multiple() public { - _registerClientChain(); + function test_Fail() public { vm.startPrank(exocoreValidatorSet.addr); - vm.expectEmit(address(exocoreGateway)); - emit ExocoreGatewayStorage.BootstrapRequestSent(clientChainId); - vm.expectEmit(address(exocoreGateway)); - emit ExocoreGatewayStorage.BootstrapRequestSent(anotherClientChainId); - assertEq(exocoreGateway.chainToBootstrapped(clientChainId), false); - assertEq(exocoreGateway.chainToBootstrapped(anotherClientChainId), false); - exocoreGateway.markBootstrapOnAllChains(); - assertEq(exocoreGateway.chainToBootstrapped(clientChainId), true); - assertEq(exocoreGateway.chainToBootstrapped(anotherClientChainId), true); - } - - function _registerClientChain() internal { - // actual registration of chain - anotherClientChainId += 1; - bytes32 peer = bytes32(uint256(123)); - uint8 addressLength = 20; - string memory name = "AnotherClientChain"; - string memory metaInfo = "EVM compatible client chain"; - string memory signatureType = "secp256k1"; - // but first, set the lz thing up - exocoreLzEndpoint.setDestLzEndpoint(address(123), /* peer */ address(clientLzEndpoint)); - vm.expectEmit(true, true, true, true, address(exocoreGateway)); - emit ExocoreGatewayStorage.ClientChainRegistered(anotherClientChainId); - vm.startPrank(exocoreValidatorSet.addr); - exocoreGateway.registerOrUpdateClientChain( - anotherClientChainId, peer, addressLength, name, metaInfo, signatureType - ); + vm.expectRevert(abi.encodeWithSelector(NoPeer.selector, clientChainId + 1)); + exocoreGateway.markBootstrap{value: nativeFee}(clientChainId + 1); } } diff --git a/test/mocks/ExocoreGatewayMock.sol b/test/mocks/ExocoreGatewayMock.sol index 45b795b7..643f99ac 100644 --- a/test/mocks/ExocoreGatewayMock.sol +++ b/test/mocks/ExocoreGatewayMock.sol @@ -103,24 +103,15 @@ 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 nonReentrant { - (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, "", true); - // TODO: should this be marked only upon receiving a response? - chainToBootstrapped[clientChainId] = true; - } - } + function markBootstrap(uint32 chainIndex) public payable whenNotPaused nonReentrant { + _markBootstrap(chainIndex); + } + + function _markBootstrap(uint32 chainIndex) internal { + // we don't track that a request was sent to a chain to allow for retrials + // if the transaction fails on the destination chain + _sendInterchainMsg(chainIndex, Action.REQUEST_MARK_BOOTSTRAP, "", false); + emit BootstrapRequestSent(chainIndex); } /**