From 662835debfb8f2c0d578c8573c530431268630b2 Mon Sep 17 00:00:00 2001
From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com>
Date: Mon, 2 Sep 2024 11:42:25 +0000
Subject: [PATCH 1/6] fix(bootstrap): do not fail mark bootstrap
- As outlined in #83, a cross-chain message that can't/won't be retried
must not fail. Hence, all calls to `markBootstrapped` on `Bootstrap`
should not fail. Fixes #89
- Separately, sending multiple LZ messages in one transaction is
convoluted and hence disabled. As a consequence, marking bootstrap on
all chains is no longer supported; instead, the caller must provide
the LZ chain ID. Along similar lines, the caller must provide the
native fee for this transaction.
---
script/11_SetPeers.s.sol | 10 +-
script/14_CorrectBootstrapErrors.s.sol | 9 +-
script/7_DeployBootstrap.s.sol | 36 ++---
script/integration/1_DeployBootstrap.s.sol | 4 +-
src/core/Bootstrap.sol | 42 ++++--
src/core/ExocoreGateway.sol | 37 ++---
src/interfaces/IExocoreGateway.sol | 7 +
src/libraries/Errors.sol | 6 -
src/storage/BootstrapStorage.sol | 9 ++
src/storage/ExocoreGatewayStorage.sol | 4 -
test/foundry/unit/Bootstrap.t.sol | 156 +++++++++++++++++----
test/foundry/unit/ExocoreGateway.t.sol | 45 ++----
test/mocks/ExocoreGatewayMock.sol | 31 ++--
13 files changed, 252 insertions(+), 144 deletions(-)
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..64cdcfea 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,23 +557,24 @@ 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;
}
- if (bootstrapped) {
- revert Errors.BootstrapAlreadyBootstrapped();
- }
- if (clientChainGatewayLogic == address(0)) {
- revert Errors.ZeroAddress();
- }
- ICustomProxyAdmin(customProxyAdmin).changeImplementation(
+ // bootstrapped = true is only actioned by the clientchaingateway after upgrade
+ // so no need to check for that here
+ 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();
+ } catch {
+ // to allow retries, never fail
+ emit BootstrapUpgradeFailed();
+ }
emit Bootstrapped();
}
@@ -584,9 +589,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/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..aee22259 100644
--- a/src/storage/BootstrapStorage.sol
+++ b/src/storage/BootstrapStorage.sol
@@ -227,6 +227,15 @@ 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 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..3f560910 100644
--- a/test/foundry/unit/Bootstrap.t.sol
+++ b/test/foundry/unit/Bootstrap.t.sol
@@ -100,6 +100,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 +126,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 +156,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();
}
@@ -1005,7 +1009,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 +1040,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 +1069,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 +1099,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 +1122,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 +1145,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..552d53fc 100644
--- a/test/mocks/ExocoreGatewayMock.sol
+++ b/test/mocks/ExocoreGatewayMock.sol
@@ -107,20 +107,25 @@ contract ExocoreGatewayMock is
// 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 markBootstrapOnAllChains() public payable whenNotPaused nonReentrant {
+ (bool success, uint32[] memory chainIndices) = ASSETS_CONTRACT.getClientChains();
+ if (!success) {
+ revert Errors.ExocoreGatewayFailedToGetClientChainIds();
}
+ for (uint256 i = 0; i < chainIndices.length; ++i) {
+ _markBootstrap(chainIndices[i]);
+ }
+ }
+
+ 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);
}
/**
From 88d41a3ae7408e9a5c946886f2f7baebb155cc61 Mon Sep 17 00:00:00 2001
From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com>
Date: Mon, 2 Sep 2024 11:49:07 +0000
Subject: [PATCH 2/6] fix: remove reference to markBootstrapOnAllChains
---
docs/architecture.svg | 2 +-
test/mocks/ExocoreGatewayMock.sol | 14 --------------
2 files changed, 1 insertion(+), 15 deletions(-)
diff --git a/docs/architecture.svg b/docs/architecture.svg
index 58e4fbe6..d0d9603f 100644
--- a/docs/architecture.svg
+++ b/docs/architecture.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/test/mocks/ExocoreGatewayMock.sol b/test/mocks/ExocoreGatewayMock.sol
index 552d53fc..643f99ac 100644
--- a/test/mocks/ExocoreGatewayMock.sol
+++ b/test/mocks/ExocoreGatewayMock.sol
@@ -103,20 +103,6 @@ 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 payable whenNotPaused nonReentrant {
- (bool success, uint32[] memory chainIndices) = ASSETS_CONTRACT.getClientChains();
- if (!success) {
- revert Errors.ExocoreGatewayFailedToGetClientChainIds();
- }
- for (uint256 i = 0; i < chainIndices.length; ++i) {
- _markBootstrap(chainIndices[i]);
- }
- }
-
function markBootstrap(uint32 chainIndex) public payable whenNotPaused nonReentrant {
_markBootstrap(chainIndex);
}
From 31fc16388ac18c356446fed5c130fbbb9ee943c2 Mon Sep 17 00:00:00 2001
From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com>
Date: Tue, 3 Sep 2024 06:47:00 +0000
Subject: [PATCH 3/6] test: add unit test for mark bootstrap
In the case where Bootstrap call first fails and then succeeds.
---
src/core/Bootstrap.sol | 7 +++--
src/storage/BootstrapStorage.sol | 5 ++++
test/foundry/unit/Bootstrap.t.sol | 48 ++++++++++++++++++-------------
3 files changed, 38 insertions(+), 22 deletions(-)
diff --git a/src/core/Bootstrap.sol b/src/core/Bootstrap.sol
index 64cdcfea..8c93795a 100644
--- a/src/core/Bootstrap.sol
+++ b/src/core/Bootstrap.sol
@@ -563,7 +563,11 @@ contract Bootstrap is
return;
}
// bootstrapped = true is only actioned by the clientchaingateway after upgrade
- // so no need to check for that here
+ // so no need to check for that here but better to be safe.
+ if (bootstrapped) {
+ emit BootstrappedAlready();
+ return;
+ }
try ICustomProxyAdmin(customProxyAdmin).changeImplementation(
// address(this) is storage address and not logic address. so it is a proxy.
ITransparentUpgradeableProxy(address(this)),
@@ -575,7 +579,6 @@ contract Bootstrap is
// to allow retries, never fail
emit BootstrapUpgradeFailed();
}
- emit Bootstrapped();
}
/// @notice Sets a new client chain gateway logic and its initialization data.
diff --git a/src/storage/BootstrapStorage.sol b/src/storage/BootstrapStorage.sol
index aee22259..c7d6b85f 100644
--- a/src/storage/BootstrapStorage.sol
+++ b/src/storage/BootstrapStorage.sol
@@ -236,6 +236,11 @@ contract BootstrapStorage is GatewayStorage {
/// 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/test/foundry/unit/Bootstrap.t.sol b/test/foundry/unit/Bootstrap.t.sol
index 3f560910..f4129bda 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;
@@ -889,36 +890,35 @@ 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),
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 {
@@ -940,13 +940,21 @@ contract BootstrapTest is Test {
}
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 test13_OperationAllowed() public {
vm.warp(spawnTime - offsetDuration);
vm.startPrank(addrs[0]);
From 51ad701b4e7b0fc7fc90a23883dbe28a75b07c30 Mon Sep 17 00:00:00 2001
From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com>
Date: Tue, 3 Sep 2024 08:19:26 +0000
Subject: [PATCH 4/6] fix: do not fail mark bootstrap to gateway
---
src/core/ClientChainGateway.sol | 4 ++--
src/core/ClientGatewayLzReceiver.sol | 8 ++++++++
test/foundry/unit/Bootstrap.t.sol | 17 ++++++++++++-----
3 files changed, 22 insertions(+), 7 deletions(-)
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/test/foundry/unit/Bootstrap.t.sol b/test/foundry/unit/Bootstrap.t.sol
index f4129bda..577d8d2f 100644
--- a/test/foundry/unit/Bootstrap.t.sol
+++ b/test/foundry/unit/Bootstrap.t.sol
@@ -924,11 +924,8 @@ contract BootstrapTest is Test {
function test12_MarkBootstrapped_AlreadyBootstrapped() public {
test12_MarkBootstrapped();
vm.startPrank(address(clientChainLzEndpoint));
- vm.expectRevert(
- abi.encodeWithSelector(
- GatewayStorage.UnsupportedRequest.selector, GatewayStorage.Action.REQUEST_MARK_BOOTSTRAP
- )
- );
+ vm.expectEmit(address(bootstrap));
+ emit BootstrapStorage.BootstrappedAlready();
bootstrap.lzReceive(
Origin(exocoreChainId, bytes32(bytes20(undeployedExocoreGateway)), uint64(2)),
generateUID(1),
@@ -955,6 +952,16 @@ contract BootstrapTest is Test {
_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);
+ _markBootstrapped(3, true);
+ }
+
function test13_OperationAllowed() public {
vm.warp(spawnTime - offsetDuration);
vm.startPrank(addrs[0]);
From f318b7eef3635f2d51077bc998d1ae39b356bd98 Mon Sep 17 00:00:00 2001
From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com>
Date: Tue, 3 Sep 2024 08:37:55 +0000
Subject: [PATCH 5/6] fix(test): expect event
---
test/foundry/unit/Bootstrap.t.sol | 2 ++
1 file changed, 2 insertions(+)
diff --git a/test/foundry/unit/Bootstrap.t.sol b/test/foundry/unit/Bootstrap.t.sol
index 577d8d2f..57b9004b 100644
--- a/test/foundry/unit/Bootstrap.t.sol
+++ b/test/foundry/unit/Bootstrap.t.sol
@@ -959,6 +959,8 @@ contract BootstrapTest is Test {
_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);
}
From 65debfd0fa59a7aaf90246ec687ecc47bbb67fc5 Mon Sep 17 00:00:00 2001
From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com>
Date: Tue, 3 Sep 2024 08:46:55 +0000
Subject: [PATCH 6/6] refactor: remove repeated code
---
test/foundry/unit/Bootstrap.t.sol | 14 ++++----------
1 file changed, 4 insertions(+), 10 deletions(-)
diff --git a/test/foundry/unit/Bootstrap.t.sol b/test/foundry/unit/Bootstrap.t.sol
index 57b9004b..35955bec 100644
--- a/test/foundry/unit/Bootstrap.t.sol
+++ b/test/foundry/unit/Bootstrap.t.sol
@@ -900,7 +900,7 @@ contract BootstrapTest is Test {
clientChainLzEndpoint.lzReceive(
Origin(exocoreChainId, bytes32(bytes20(undeployedExocoreGateway)), nonce),
address(bootstrap),
- generateUID(1),
+ generateUID(nonce),
abi.encodePacked(GatewayStorage.Action.REQUEST_MARK_BOOTSTRAP, ""),
bytes("")
);
@@ -922,17 +922,11 @@ contract BootstrapTest is Test {
}
function test12_MarkBootstrapped_AlreadyBootstrapped() public {
- test12_MarkBootstrapped();
- vm.startPrank(address(clientChainLzEndpoint));
+ vm.warp(spawnTime + 1);
+ _markBootstrapped(1, true);
vm.expectEmit(address(bootstrap));
emit BootstrapStorage.BootstrappedAlready();
- bootstrap.lzReceive(
- Origin(exocoreChainId, bytes32(bytes20(undeployedExocoreGateway)), uint64(2)),
- generateUID(1),
- abi.encodePacked(GatewayStorage.Action.REQUEST_MARK_BOOTSTRAP, ""),
- address(0),
- bytes("")
- );
+ _markBootstrapped(2, true);
vm.stopPrank();
}