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/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);
}
/**