From ae3325b63e3de61c50bcf43f48f3a352b926a3d3 Mon Sep 17 00:00:00 2001 From: adu Date: Fri, 6 Sep 2024 09:12:19 +0800 Subject: [PATCH 1/9] feat: use script to deploy multisig wallet on exocore --- script/15_DeploySafeMulstisigWallet.s.sol | 0 script/safe_contracts_on_exocore.json | 12 ++++++++++++ 2 files changed, 12 insertions(+) create mode 100644 script/15_DeploySafeMulstisigWallet.s.sol create mode 100644 script/safe_contracts_on_exocore.json diff --git a/script/15_DeploySafeMulstisigWallet.s.sol b/script/15_DeploySafeMulstisigWallet.s.sol new file mode 100644 index 00000000..e69de29b diff --git a/script/safe_contracts_on_exocore.json b/script/safe_contracts_on_exocore.json new file mode 100644 index 00000000..a8847a18 --- /dev/null +++ b/script/safe_contracts_on_exocore.json @@ -0,0 +1,12 @@ +{ + "SimulateTxAccessor": "0xB46E02a8c957892F7a1b7dD019bF7f56cA7830C6", + "GnosisSafeProxyFactory": "0xd92Eb22d59D2736C12ef8e009833b98dB812BC5F", + "DefaultCallbackHandler": "0xA9221e82f099027Da128369d323E249080507b78", + "CompatibilityFallbackHandler": "0x820ed29524601172Fe4aec900Bc48432067CBCDF", + "CreateCall": "0xA2c66e9eD611De51192EEfda6322E3D28b0c380c", + "MultiSend": "0x83Aa234126729346F9e6a33E109244935E521bEC", + "MultiSendCallOnly": "0x32A8c6b3c7D63002E1d230da5D525D6b6391796a", + "SignMessageLib": "0x30fdE0Cc889dEdD87cc11F48798506AbbC7B8c24", + "GnosisSafeL2": "0x9D24ad942d3453F574f3Df9C66504fDE009c14A0", + "GnosisSafe": "0xE28848a95D96dFc200A48f976b32B726253a8e14" +} \ No newline at end of file From d7c501665c32734e4b17860c366767620bc5d4c0 Mon Sep 17 00:00:00 2001 From: adu Date: Fri, 6 Sep 2024 09:12:47 +0800 Subject: [PATCH 2/9] forge install: safe-smart-account v1.3.0-libs.0 --- .gitmodules | 3 +++ lib/safe-smart-account | 1 + 2 files changed, 4 insertions(+) create mode 160000 lib/safe-smart-account diff --git a/.gitmodules b/.gitmodules index db0806c1..332e5ecb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -22,3 +22,6 @@ [submodule "lib/openzeppelin-contracts-upgradeable"] path = lib/openzeppelin-contracts-upgradeable url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable +[submodule "lib/safe-smart-account"] + path = lib/safe-smart-account + url = https://github.com/safe-global/safe-smart-account diff --git a/lib/safe-smart-account b/lib/safe-smart-account new file mode 160000 index 00000000..767ef36b --- /dev/null +++ b/lib/safe-smart-account @@ -0,0 +1 @@ +Subproject commit 767ef36bba88bdbc0c9fe3708a4290cabef4c376 From 33763c425f81cf1e08b56b793b189de4bafed697 Mon Sep 17 00:00:00 2001 From: adu Date: Fri, 6 Sep 2024 09:22:36 +0800 Subject: [PATCH 3/9] fix: remapping --- remappings.txt | 3 +- script/15_DeploySafeMulstisigWallet.s.sol | 53 +++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/remappings.txt b/remappings.txt index 3bb6c03b..2f83cab2 100644 --- a/remappings.txt +++ b/remappings.txt @@ -9,4 +9,5 @@ forge-std/=lib/forge-std/src/ @layerzerolabs/lz-evm-oapp-v2=lib/LayerZero-v2/oapp/ @layerzerolabs/lz-evm-messagelib-v2=lib/LayerZero-v2/messagelib/ @beacon-oracle=lib/eigenlayer-beacon-oracle/ -solidity-bytes-utils/=lib/solidity-bytes-utils/ \ No newline at end of file +solidity-bytes-utils/=lib/solidity-bytes-utils/ +@safe-contracts/=lib/safe-smart-account/contracts/ \ No newline at end of file diff --git a/script/15_DeploySafeMulstisigWallet.s.sol b/script/15_DeploySafeMulstisigWallet.s.sol index e69de29b..6328b4ce 100644 --- a/script/15_DeploySafeMulstisigWallet.s.sol +++ b/script/15_DeploySafeMulstisigWallet.s.sol @@ -0,0 +1,53 @@ +pragma solidity ^0.8.13; + +import "forge-std/Script.sol"; +import "@safe-contracts/proxies/GnosisSafeProxyFactory.sol"; +import "@safe-contracts/GnosisSafe.sol"; + +contract CreateMultisigScript is Script { + function setUp() public {} + + function run() public { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + + // Load deployed contract addresses + GnosisSafeProxyFactory proxyFactory = GnosisSafeProxyFactory(0xd92Eb22d59D2736C12ef8e009833b98dB812BC5F); + GnosisSafe safeSingleton = GnosisSafe(payable(0xE28848a95D96dFc200A48f976b32B726253a8e14)); + + // Set up owners (replace with actual addresses) + address[] memory owners = new address[](3); + owners[0] = address(0x1111111111111111111111111111111111111111); + owners[1] = address(0x2222222222222222222222222222222222222222); + owners[2] = address(0x3333333333333333333333333333333333333333); + + // Set up Safe parameters + uint256 threshold = 2; + address to = address(0); + bytes memory data = ""; + address fallbackHandler = 0x820ed29524601172Fe4aec900Bc48432067CBCDF; // CompatibilityFallbackHandler + address paymentToken = address(0); + uint256 payment = 0; + address payable paymentReceiver = payable(address(0)); + + // Encode initialization data + bytes memory initializer = abi.encodeWithSelector( + GnosisSafe.setup.selector, + owners, + threshold, + to, + data, + fallbackHandler, + paymentToken, + payment, + paymentReceiver + ); + + // Create new Safe proxy + GnosisSafeProxy safeProxy = proxyFactory.createProxy(address(safeSingleton), initializer); + + console.log("New Safe created at:", address(safeProxy)); + + vm.stopBroadcast(); + } +} From 75e130f7801c13af37ee133604c895135b1dcc72 Mon Sep 17 00:00:00 2001 From: adu Date: Mon, 9 Sep 2024 09:28:48 +0800 Subject: [PATCH 4/9] feat: add deployment script for exocore multisig --- script/15_DeploySafeMulstisigWallet.s.sol | 43 +++++++++++++++-------- script/deployedMultisigWallets.json | 18 ++++++++++ 2 files changed, 47 insertions(+), 14 deletions(-) create mode 100644 script/deployedMultisigWallets.json diff --git a/script/15_DeploySafeMulstisigWallet.s.sol b/script/15_DeploySafeMulstisigWallet.s.sol index 6328b4ce..22c1ada3 100644 --- a/script/15_DeploySafeMulstisigWallet.s.sol +++ b/script/15_DeploySafeMulstisigWallet.s.sol @@ -1,31 +1,46 @@ pragma solidity ^0.8.13; -import "forge-std/Script.sol"; +import {BaseScript} from "./BaseScript.sol"; import "@safe-contracts/proxies/GnosisSafeProxyFactory.sol"; -import "@safe-contracts/GnosisSafe.sol"; +import "@safe-contracts/GnosisSafeL2.sol"; +import "forge-std/StdJson.sol"; +import "forge-std/Script.sol"; -contract CreateMultisigScript is Script { - function setUp() public {} +contract CreateMultisigScript is BaseScript { + using stdJson for string; + + function setUp() public override { + super.setUp(); + + exocore = vm.createSelectFork(exocoreRPCURL); + _topUpPlayer(exocore, address(0), exocoreGenesis, deployer.addr, 2 ether); + } function run() public { - uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); - vm.startBroadcast(deployerPrivateKey); + vm.selectFork(exocore); + vm.startBroadcast(deployer.privateKey); + + // Read deployed Safe contracts from JSON file + string memory json = vm.readFile("script/safe_contracts_on_exocore.json"); + + address proxyFactoryAddress = json.readAddress(".GnosisSafeProxyFactory"); + address safeSingletonAddress = json.readAddress(".GnosisSafeL2"); + address fallbackHandlerAddress = json.readAddress(".CompatibilityFallbackHandler"); - // Load deployed contract addresses - GnosisSafeProxyFactory proxyFactory = GnosisSafeProxyFactory(0xd92Eb22d59D2736C12ef8e009833b98dB812BC5F); - GnosisSafe safeSingleton = GnosisSafe(payable(0xE28848a95D96dFc200A48f976b32B726253a8e14)); + GnosisSafeProxyFactory proxyFactory = GnosisSafeProxyFactory(proxyFactoryAddress); + GnosisSafeL2 safeSingleton = GnosisSafeL2(payable(safeSingletonAddress)); - // Set up owners (replace with actual addresses) + // Set up owners address[] memory owners = new address[](3); - owners[0] = address(0x1111111111111111111111111111111111111111); - owners[1] = address(0x2222222222222222222222222222222222222222); - owners[2] = address(0x3333333333333333333333333333333333333333); + owners[0] = deployer.addr; + owners[1] = exocoreValidatorSet.addr; + owners[2] = relayer.addr; // Set up Safe parameters uint256 threshold = 2; address to = address(0); bytes memory data = ""; - address fallbackHandler = 0x820ed29524601172Fe4aec900Bc48432067CBCDF; // CompatibilityFallbackHandler + address fallbackHandler = fallbackHandlerAddress; address paymentToken = address(0); uint256 payment = 0; address payable paymentReceiver = payable(address(0)); diff --git a/script/deployedMultisigWallets.json b/script/deployedMultisigWallets.json new file mode 100644 index 00000000..711dfd94 --- /dev/null +++ b/script/deployedMultisigWallets.json @@ -0,0 +1,18 @@ +{ + "holesky": { + "multisig": "0x9A7b23a0BB29F77BaBB8add484b34c02EfD327BE", + "signers": [ + "0x481E020DB4709e6EdDbf8134D41b866c6Fc8555e", + "0x3583fF95f96b356d716881C871aF7Eb55ea34a93", + "0xA1dfab3234f49e02e04E6C56a021F1a497CD0f82" + ] + }, + "exocore": { + "multisig": "0xF27865277D8Cc608F279B3C09719A9Ceaa25A58f", + "signers": [ + "0x481E020DB4709e6EdDbf8134D41b866c6Fc8555e", + "0x3583fF95f96b356d716881C871aF7Eb55ea34a93", + "0xA1dfab3234f49e02e04E6C56a021F1a497CD0f82" + ] + } +} \ No newline at end of file From 07110635d3e0b6c02d8a7d9f550414c9170e7f3e Mon Sep 17 00:00:00 2001 From: adu Date: Mon, 9 Sep 2024 09:44:34 +0800 Subject: [PATCH 5/9] chore: add utils directory --- script/12_RedeployClientChainGateway.s.sol | 5 +++-- script/14_CorrectBootstrapErrors.s.sol | 2 +- script/15_DeploySafeMulstisigWallet.s.sol | 8 ++++++-- script/2_DeployBoth.s.sol | 2 +- script/7_DeployBootstrap.s.sol | 5 +++-- script/BaseScript.sol | 2 +- script/integration/1_DeployBootstrap.s.sol | 5 +++-- src/storage/BootstrapStorage.sol | 2 +- src/{core => utils}/BeaconProxyBytecode.sol | 0 src/{core => utils}/CustomProxyAdmin.sol | 0 test/foundry/ExocoreDeployer.t.sol | 2 +- test/foundry/unit/Bootstrap.t.sol | 5 +++-- test/foundry/unit/ClientChainGateway.t.sol | 2 +- test/foundry/unit/CustomProxyAdmin.t.sol | 2 +- test/foundry/unit/Multisig.t.sol | 1 + test/foundry/unit/TimelockController.t.sol | 1 + 16 files changed, 27 insertions(+), 17 deletions(-) rename src/{core => utils}/BeaconProxyBytecode.sol (100%) rename src/{core => utils}/CustomProxyAdmin.sol (100%) create mode 100644 test/foundry/unit/Multisig.t.sol create mode 100644 test/foundry/unit/TimelockController.t.sol diff --git a/script/12_RedeployClientChainGateway.s.sol b/script/12_RedeployClientChainGateway.s.sol index e56edab6..5d07ba81 100644 --- a/script/12_RedeployClientChainGateway.s.sol +++ b/script/12_RedeployClientChainGateway.s.sol @@ -3,12 +3,13 @@ pragma solidity ^0.8.19; import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; -import "../src/core/BeaconProxyBytecode.sol"; import {Bootstrap} from "../src/core/Bootstrap.sol"; import {ClientChainGateway} from "../src/core/ClientChainGateway.sol"; -import {CustomProxyAdmin} from "../src/core/CustomProxyAdmin.sol"; + import "../src/core/ExoCapsule.sol"; import {Vault} from "../src/core/Vault.sol"; +import "../src/utils/BeaconProxyBytecode.sol"; +import {CustomProxyAdmin} from "../src/utils/CustomProxyAdmin.sol"; import {BaseScript} from "./BaseScript.sol"; diff --git a/script/14_CorrectBootstrapErrors.s.sol b/script/14_CorrectBootstrapErrors.s.sol index df419c4a..829aaa73 100644 --- a/script/14_CorrectBootstrapErrors.s.sol +++ b/script/14_CorrectBootstrapErrors.s.sol @@ -6,9 +6,9 @@ import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.s import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import {ITransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; -import "../src/core/BeaconProxyBytecode.sol"; import {Bootstrap} from "../src/core/Bootstrap.sol"; import {ClientChainGateway} from "../src/core/ClientChainGateway.sol"; +import "../src/utils/BeaconProxyBytecode.sol"; import "../src/core/ExoCapsule.sol"; import {Vault} from "../src/core/Vault.sol"; diff --git a/script/15_DeploySafeMulstisigWallet.s.sol b/script/15_DeploySafeMulstisigWallet.s.sol index 22c1ada3..a15b9a4c 100644 --- a/script/15_DeploySafeMulstisigWallet.s.sol +++ b/script/15_DeploySafeMulstisigWallet.s.sol @@ -1,12 +1,15 @@ pragma solidity ^0.8.13; import {BaseScript} from "./BaseScript.sol"; -import "@safe-contracts/proxies/GnosisSafeProxyFactory.sol"; + import "@safe-contracts/GnosisSafeL2.sol"; -import "forge-std/StdJson.sol"; +import "@safe-contracts/proxies/GnosisSafeProxyFactory.sol"; + import "forge-std/Script.sol"; +import "forge-std/StdJson.sol"; contract CreateMultisigScript is BaseScript { + using stdJson for string; function setUp() public override { @@ -65,4 +68,5 @@ contract CreateMultisigScript is BaseScript { vm.stopBroadcast(); } + } diff --git a/script/2_DeployBoth.s.sol b/script/2_DeployBoth.s.sol index a7dcafc3..0fddec02 100644 --- a/script/2_DeployBoth.s.sol +++ b/script/2_DeployBoth.s.sol @@ -1,10 +1,10 @@ pragma solidity ^0.8.19; -import "../src/core/BeaconProxyBytecode.sol"; import "../src/core/ClientChainGateway.sol"; import "../src/core/ExoCapsule.sol"; import "../src/core/ExocoreGateway.sol"; import {Vault} from "../src/core/Vault.sol"; +import "../src/utils/BeaconProxyBytecode.sol"; import {ExocoreGatewayMock} from "../test/mocks/ExocoreGatewayMock.sol"; import {BaseScript} from "./BaseScript.sol"; diff --git a/script/7_DeployBootstrap.s.sol b/script/7_DeployBootstrap.s.sol index 7ecdbf45..643a57f2 100644 --- a/script/7_DeployBootstrap.s.sol +++ b/script/7_DeployBootstrap.s.sol @@ -3,12 +3,13 @@ pragma solidity ^0.8.19; import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; -import "../src/core/BeaconProxyBytecode.sol"; import {Bootstrap} from "../src/core/Bootstrap.sol"; import {ClientChainGateway} from "../src/core/ClientChainGateway.sol"; -import {CustomProxyAdmin} from "../src/core/CustomProxyAdmin.sol"; + import "../src/core/ExoCapsule.sol"; import {Vault} from "../src/core/Vault.sol"; +import "../src/utils/BeaconProxyBytecode.sol"; +import {CustomProxyAdmin} from "../src/utils/CustomProxyAdmin.sol"; import {BaseScript} from "./BaseScript.sol"; import {ILayerZeroEndpointV2} from "@layerzero-v2/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol"; diff --git a/script/BaseScript.sol b/script/BaseScript.sol index 2c0af011..8b347528 100644 --- a/script/BaseScript.sol +++ b/script/BaseScript.sol @@ -1,10 +1,10 @@ pragma solidity ^0.8.19; -import "../src/core/BeaconProxyBytecode.sol"; import "../src/interfaces/IClientChainGateway.sol"; import "../src/interfaces/IExoCapsule.sol"; import "../src/interfaces/IExocoreGateway.sol"; import "../src/interfaces/IVault.sol"; +import "../src/utils/BeaconProxyBytecode.sol"; import "../src/interfaces/precompiles/IAssets.sol"; import "../src/interfaces/precompiles/IClaimReward.sol"; diff --git a/script/integration/1_DeployBootstrap.s.sol b/script/integration/1_DeployBootstrap.s.sol index 750f31d0..d0833884 100644 --- a/script/integration/1_DeployBootstrap.s.sol +++ b/script/integration/1_DeployBootstrap.s.sol @@ -10,12 +10,13 @@ import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.so import {EndpointV2Mock} from "../../test/mocks/EndpointV2Mock.sol"; -import "../../src/core/BeaconProxyBytecode.sol"; import {Bootstrap} from "../../src/core/Bootstrap.sol"; -import {CustomProxyAdmin} from "../../src/core/CustomProxyAdmin.sol"; + import {Vault} from "../../src/core/Vault.sol"; import {IValidatorRegistry} from "../../src/interfaces/IValidatorRegistry.sol"; import {IVault} from "../../src/interfaces/IVault.sol"; +import "../../src/utils/BeaconProxyBytecode.sol"; +import {CustomProxyAdmin} from "../../src/utils/CustomProxyAdmin.sol"; import {MyToken} from "../../test/foundry/unit/MyToken.sol"; // Technically this is used for testing but it is marked as a script diff --git a/src/storage/BootstrapStorage.sol b/src/storage/BootstrapStorage.sol index c7d6b85f..2796ce50 100644 --- a/src/storage/BootstrapStorage.sol +++ b/src/storage/BootstrapStorage.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import {BeaconProxyBytecode} from "../core/BeaconProxyBytecode.sol"; import {Vault} from "../core/Vault.sol"; import {IValidatorRegistry} from "../interfaces/IValidatorRegistry.sol"; import {IVault} from "../interfaces/IVault.sol"; +import {BeaconProxyBytecode} from "../utils/BeaconProxyBytecode.sol"; import {Errors} from "../libraries/Errors.sol"; import {GatewayStorage} from "./GatewayStorage.sol"; diff --git a/src/core/BeaconProxyBytecode.sol b/src/utils/BeaconProxyBytecode.sol similarity index 100% rename from src/core/BeaconProxyBytecode.sol rename to src/utils/BeaconProxyBytecode.sol diff --git a/src/core/CustomProxyAdmin.sol b/src/utils/CustomProxyAdmin.sol similarity index 100% rename from src/core/CustomProxyAdmin.sol rename to src/utils/CustomProxyAdmin.sol diff --git a/test/foundry/ExocoreDeployer.t.sol b/test/foundry/ExocoreDeployer.t.sol index 7252a4d0..2d07edf6 100644 --- a/test/foundry/ExocoreDeployer.t.sol +++ b/test/foundry/ExocoreDeployer.t.sol @@ -32,8 +32,8 @@ import "../mocks/ClaimRewardMock.sol"; import "../mocks/DelegationMock.sol"; import {NonShortCircuitEndpointV2Mock} from "../mocks/NonShortCircuitEndpointV2Mock.sol"; -import "src/core/BeaconProxyBytecode.sol"; import "src/core/ExoCapsule.sol"; +import "src/utils/BeaconProxyBytecode.sol"; import "src/libraries/BeaconChainProofs.sol"; import "src/libraries/Endian.sol"; diff --git a/test/foundry/unit/Bootstrap.t.sol b/test/foundry/unit/Bootstrap.t.sol index 35955bec..90b94c58 100644 --- a/test/foundry/unit/Bootstrap.t.sol +++ b/test/foundry/unit/Bootstrap.t.sol @@ -3,8 +3,9 @@ pragma solidity ^0.8.0; import {Bootstrap} from "src/core/Bootstrap.sol"; import {ClientChainGateway} from "src/core/ClientChainGateway.sol"; -import {CustomProxyAdmin} from "src/core/CustomProxyAdmin.sol"; + import {Vault} from "src/core/Vault.sol"; +import {CustomProxyAdmin} from "src/utils/CustomProxyAdmin.sol"; import {IValidatorRegistry} from "src/interfaces/IValidatorRegistry.sol"; @@ -28,9 +29,9 @@ import "forge-std/Test.sol"; import "forge-std/console.sol"; import "src/libraries/Errors.sol"; -import "src/core/BeaconProxyBytecode.sol"; import "src/core/ExoCapsule.sol"; import "src/storage/GatewayStorage.sol"; +import "src/utils/BeaconProxyBytecode.sol"; contract BootstrapTest is Test { diff --git a/test/foundry/unit/ClientChainGateway.t.sol b/test/foundry/unit/ClientChainGateway.t.sol index fb98139d..851eadf3 100644 --- a/test/foundry/unit/ClientChainGateway.t.sol +++ b/test/foundry/unit/ClientChainGateway.t.sol @@ -31,7 +31,7 @@ import {NonShortCircuitEndpointV2Mock} from "../../mocks/NonShortCircuitEndpoint import "src/interfaces/IExoCapsule.sol"; import "src/interfaces/IVault.sol"; -import "src/core/BeaconProxyBytecode.sol"; +import "src/utils/BeaconProxyBytecode.sol"; contract SetUp is Test { diff --git a/test/foundry/unit/CustomProxyAdmin.t.sol b/test/foundry/unit/CustomProxyAdmin.t.sol index 80107fe6..c7e57b34 100644 --- a/test/foundry/unit/CustomProxyAdmin.t.sol +++ b/test/foundry/unit/CustomProxyAdmin.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import {CustomProxyAdmin} from "src/core/CustomProxyAdmin.sol"; import {ICustomProxyAdmin} from "src/interfaces/ICustomProxyAdmin.sol"; +import {CustomProxyAdmin} from "src/utils/CustomProxyAdmin.sol"; import "forge-std/Test.sol"; import "forge-std/console.sol"; diff --git a/test/foundry/unit/Multisig.t.sol b/test/foundry/unit/Multisig.t.sol new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/test/foundry/unit/Multisig.t.sol @@ -0,0 +1 @@ + diff --git a/test/foundry/unit/TimelockController.t.sol b/test/foundry/unit/TimelockController.t.sol new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/test/foundry/unit/TimelockController.t.sol @@ -0,0 +1 @@ + From 58893b8ddd4b685144eff55af91fde30570a88e8 Mon Sep 17 00:00:00 2001 From: adu Date: Mon, 9 Sep 2024 10:22:33 +0800 Subject: [PATCH 6/9] feat: add circuit breaker role to customtimelockcontroller --- src/utils/CustomTimelockController.sol | 37 ++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/utils/CustomTimelockController.sol diff --git a/src/utils/CustomTimelockController.sol b/src/utils/CustomTimelockController.sol new file mode 100644 index 00000000..ae1bc30c --- /dev/null +++ b/src/utils/CustomTimelockController.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/governance/TimelockController.sol"; + +interface IPausable { + + function pause() external; + function unpause() external; + +} + +contract CustomTimelockController is TimelockController { + + bytes32 public constant CIRCUIT_BREAKER_ROLE = keccak256("CIRCUIT_BREAKER_ROLE"); + + constructor( + uint256 minDelay, + address[] memory proposers, + address[] memory executors, + address[] memory circuitBreakers, + address admin + ) TimelockController(minDelay, proposers, executors, admin) { + _setRoleAdmin(CIRCUIT_BREAKER_ROLE, TIMELOCK_ADMIN_ROLE); + + // Grant CIRCUIT_BREAKER_ROLE to the specified circuit breakers + for (uint256 i = 0; i < circuitBreakers.length; i++) { + _setupRole(CIRCUIT_BREAKER_ROLE, circuitBreakers[i]); + } + } + + function pause(address target) external onlyRole(CIRCUIT_BREAKER_ROLE) { + require(target != address(0), "CustomTimelockController: invalid target"); + IPausable(target).pause(); + } + +} From d460487412074c8f6bafbf1eb7489842a5fe21a9 Mon Sep 17 00:00:00 2001 From: adu Date: Mon, 9 Sep 2024 10:53:11 +0800 Subject: [PATCH 7/9] fix: solhint --- src/utils/CustomTimelockController.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/CustomTimelockController.sol b/src/utils/CustomTimelockController.sol index ae1bc30c..292165f5 100644 --- a/src/utils/CustomTimelockController.sol +++ b/src/utils/CustomTimelockController.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import "@openzeppelin/contracts/governance/TimelockController.sol"; +import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol"; interface IPausable { From 039e544ba57c10625048c334d45ed968a0556179 Mon Sep 17 00:00:00 2001 From: adu Date: Mon, 9 Sep 2024 18:21:56 +0800 Subject: [PATCH 8/9] test: add fuzzing test for governance utilizing multisig and timelock --- test/foundry/Governance.t.sol | 418 +++++++++++++++++++++ test/foundry/unit/Multisig.t.sol | 1 - test/foundry/unit/TimelockController.t.sol | 1 - 3 files changed, 418 insertions(+), 2 deletions(-) create mode 100644 test/foundry/Governance.t.sol delete mode 100644 test/foundry/unit/Multisig.t.sol delete mode 100644 test/foundry/unit/TimelockController.t.sol diff --git a/test/foundry/Governance.t.sol b/test/foundry/Governance.t.sol new file mode 100644 index 00000000..bb1fee3b --- /dev/null +++ b/test/foundry/Governance.t.sol @@ -0,0 +1,418 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "@safe-contracts/GnosisSafe.sol"; +import "@safe-contracts/GnosisSafeL2.sol"; +import "@safe-contracts/proxies/GnosisSafeProxyFactory.sol"; +import "forge-std/Test.sol"; +import "src/core/ClientChainGateway.sol"; +import "src/utils/CustomTimelockController.sol"; + +import "@beacon-oracle/contracts/src/EigenLayerBeaconOracle.sol"; + +import "@layerzero-v2/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol"; + +import "@layerzero-v2/protocol/contracts/libs/AddressCast.sol"; + +import "@openzeppelin/contracts/proxy/beacon/IBeacon.sol"; +import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; +import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; + +import "src/core/ClientChainGateway.sol"; +import "src/storage/ClientChainGatewayStorage.sol"; + +import "src/core/ExoCapsule.sol"; +import {Vault} from "src/core/Vault.sol"; + +import {NonShortCircuitEndpointV2Mock} from "../mocks/NonShortCircuitEndpointV2Mock.sol"; +import "src/interfaces/IExoCapsule.sol"; +import "src/interfaces/IVault.sol"; + +import "src/utils/BeaconProxyBytecode.sol"; + +contract GovernanceTest is Test { + + struct Player { + uint256 privateKey; + address addr; + } + + struct Signature { + uint8 v; + bytes32 r; + bytes32 s; + } + + Player signer1; + Player signer2; + Player signer3; + + GnosisSafeL2 public safeImplementation; + GnosisSafeProxyFactory public safeProxyFactory; + GnosisSafeL2 public multisig; + CustomTimelockController public timelock; + + ERC20PresetFixedSupply restakeToken; + + ClientChainGateway clientGateway; + ClientChainGateway clientGatewayLogic; + ILayerZeroEndpointV2 clientChainLzEndpoint; + IBeaconChainOracle beaconOracle; + IVault vaultImplementation; + IExoCapsule capsuleImplementation; + IBeacon vaultBeacon; + IBeacon capsuleBeacon; + BeaconProxyBytecode beaconProxyBytecode; + + uint32 exocoreChainId = 2; + uint32 clientChainId = 1; + + uint256 holeskyFork; + + function setUp() public { + // Fork Holesky testnet + holeskyFork = vm.createSelectFork("https://ethereum-holesky.publicnode.com"); + + // Use already deployed Gnosis Safe contracts on Holesky + safeImplementation = GnosisSafeL2(payable(0x3E5c63644E683549055b9Be8653de26E0B4CD36E)); + safeProxyFactory = GnosisSafeProxyFactory(0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2); + address fallbackHandlerAddress = 0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4; + + // initialise players + signer1 = Player({privateKey: 1, addr: vm.addr(1)}); + signer2 = Player({privateKey: 2, addr: vm.addr(2)}); + signer3 = Player({privateKey: 3, addr: vm.addr(3)}); + + // Deploy 2-of-3 multisig + address[] memory owners = new address[](3); + owners[0] = signer1.addr; + owners[1] = signer2.addr; + owners[2] = signer3.addr; + + bytes memory initializer = abi.encodeWithSelector( + GnosisSafe.setup.selector, + owners, + 2, + address(0), + "", + fallbackHandlerAddress, + address(0), + 0, + payable(address(0)) + ); + + GnosisSafeProxy multisigProxy = + safeProxyFactory.createProxyWithNonce(address(safeImplementation), initializer, 0); + multisig = GnosisSafeL2(payable(address(multisigProxy))); + + // Deploy CustomTimelockController + address[] memory proposers = new address[](1); + address[] memory executors = new address[](1); + address[] memory circuitBreakers = new address[](1); + proposers[0] = address(multisig); + executors[0] = address(multisig); + circuitBreakers[0] = address(multisig); + + timelock = new CustomTimelockController( + 1 days, // minDelay + proposers, + executors, + circuitBreakers, + address(multisig) // admin + ); + + // Deploy and initialize ClientChainGateway + _deployClientChainGateway(address(timelock)); + } + + function _deployClientChainGateway(address owner) internal { + beaconOracle = IBeaconChainOracle(_deployBeaconOracle()); + + vaultImplementation = new Vault(); + capsuleImplementation = new ExoCapsule(); + + vaultBeacon = new UpgradeableBeacon(address(vaultImplementation)); + capsuleBeacon = new UpgradeableBeacon(address(capsuleImplementation)); + + beaconProxyBytecode = new BeaconProxyBytecode(); + + restakeToken = new ERC20PresetFixedSupply("rest", "rest", 1e16, owner); + + clientChainLzEndpoint = new NonShortCircuitEndpointV2Mock(clientChainId, owner); + ProxyAdmin proxyAdmin = new ProxyAdmin(); + clientGatewayLogic = new ClientChainGateway( + address(clientChainLzEndpoint), + exocoreChainId, + address(beaconOracle), + address(vaultBeacon), + address(capsuleBeacon), + address(beaconProxyBytecode) + ); + clientGateway = ClientChainGateway( + payable(address(new TransparentUpgradeableProxy(address(clientGatewayLogic), address(proxyAdmin), ""))) + ); + + clientGateway.initialize(payable(owner)); + } + + function _deployBeaconOracle() internal returns (EigenLayerBeaconOracle) { + uint256 GENESIS_BLOCK_TIMESTAMP; + + // mainnet + if (block.chainid == 1) { + GENESIS_BLOCK_TIMESTAMP = 1_606_824_023; + // goerli + } else if (block.chainid == 5) { + GENESIS_BLOCK_TIMESTAMP = 1_616_508_000; + // sepolia + } else if (block.chainid == 11_155_111) { + GENESIS_BLOCK_TIMESTAMP = 1_655_733_600; + // holesky + } else if (block.chainid == 17_000) { + GENESIS_BLOCK_TIMESTAMP = 1_695_902_400; + } else { + revert("Unsupported chainId."); + } + + EigenLayerBeaconOracle oracle = new EigenLayerBeaconOracle(GENESIS_BLOCK_TIMESTAMP); + return oracle; + } + + function testFuzz_MultisigCanPauseImmediately(uint8 signersMask) public { + vm.assume(signersMask > 0 && signersMask < 8); // Ensure at least one signer and constrain to 3 bits + + // Fork Holesky testnet + vm.selectFork(holeskyFork); + + // Prepare multisig transaction to call pause on timelock + bytes memory pauseData = abi.encodeWithSelector(CustomTimelockController.pause.selector, address(clientGateway)); + + // Use the fuzzed input to determine which signers to include + Player[] memory selectedSigners = selectSigners(signersMask); + + // Sign the transaction + bytes memory signatures = signMultisigTransaction(address(timelock), 0, pauseData, selectedSigners); + + // Execute multisig transaction if we have enough signers + if (selectedSigners.length >= 2) { + multisig.execTransaction( + address(timelock), + 0, + pauseData, + Enum.Operation.Call, + 0, // safeTxGas + 0, // baseGas + 0, // gasPrice + address(0), // gasToken + payable(0), // refundReceiver + signatures + ); + + // Check if gateway is paused + assertTrue(clientGateway.paused(), "Gateway should be paused"); + } else { + // If we don't have enough signers, expect the transaction to revert + vm.expectRevert(); + multisig.execTransaction( + address(timelock), + 0, + pauseData, + Enum.Operation.Call, + 0, // safeTxGas + 0, // baseGas + 0, // gasPrice + address(0), // gasToken + payable(0), // refundReceiver + signatures + ); + + // Check that the gateway is still not paused + assertFalse(clientGateway.paused(), "Gateway should not be paused"); + } + } + + function testFuzz_MultisigNeedsDelayToUnpause( + uint8 pauseSignersMask, + uint8 scheduleSignersMask, + uint8 executeSignersMask + ) public { + vm.assume(pauseSignersMask > 0 && pauseSignersMask < 8); + vm.assume(scheduleSignersMask > 0 && scheduleSignersMask < 8); + vm.assume(executeSignersMask > 0 && executeSignersMask < 8); + + // Fork Holesky testnet + vm.selectFork(holeskyFork); + + // First, pause the gateway + bytes memory pauseData = abi.encodeWithSelector(CustomTimelockController.pause.selector, address(clientGateway)); + Player[] memory pauseSigners = selectSigners(pauseSignersMask); + bytes memory pauseSignatures = signMultisigTransaction(address(timelock), 0, pauseData, pauseSigners); + + if (pauseSigners.length >= 2) { + multisig.execTransaction( + address(timelock), 0, pauseData, Enum.Operation.Call, 0, 0, 0, address(0), payable(0), pauseSignatures + ); + assertTrue(clientGateway.paused(), "Gateway should be paused"); + } else { + vm.expectRevert(); + multisig.execTransaction( + address(timelock), 0, pauseData, Enum.Operation.Call, 0, 0, 0, address(0), payable(0), pauseSignatures + ); + assertFalse(clientGateway.paused(), "Gateway should not be paused"); + return; // Exit the test if we couldn't pause + } + + // Prepare unpause data + bytes memory unpauseData = abi.encodeWithSelector(ClientChainGateway.unpause.selector); + + // Schedule unpause operation + bytes memory scheduleData = abi.encodeWithSelector( + TimelockController.schedule.selector, address(clientGateway), 0, unpauseData, bytes32(0), bytes32(0), 1 days + ); + Player[] memory scheduleSigners = selectSigners(scheduleSignersMask); + bytes memory scheduleSignatures = signMultisigTransaction(address(timelock), 0, scheduleData, scheduleSigners); + + if (scheduleSigners.length >= 2) { + multisig.execTransaction( + address(timelock), + 0, + scheduleData, + Enum.Operation.Call, + 0, + 0, + 0, + address(0), + payable(0), + scheduleSignatures + ); + } else { + vm.expectRevert(); + multisig.execTransaction( + address(timelock), + 0, + scheduleData, + Enum.Operation.Call, + 0, + 0, + 0, + address(0), + payable(0), + scheduleSignatures + ); + return; // Exit the test if we couldn't schedule + } + + // Try to execute immediately (should fail) + bytes memory executeData = abi.encodeWithSelector( + TimelockController.execute.selector, address(clientGateway), 0, unpauseData, bytes32(0), bytes32(0) + ); + Player[] memory executeSigners = selectSigners(executeSignersMask); + bytes memory executeSignatures = signMultisigTransaction(address(timelock), 0, executeData, executeSigners); + + // The transaction should revert because not enough signers or delay not passed + vm.expectRevert(); + multisig.execTransaction( + address(timelock), 0, executeData, Enum.Operation.Call, 0, 0, 0, address(0), payable(0), executeSignatures + ); + + // Wait for delay + vm.warp(block.timestamp + 1 days + 1); + + // Execute unpause operation + if (executeSigners.length >= 2) { + multisig.execTransaction( + address(timelock), + 0, + executeData, + Enum.Operation.Call, + 0, + 0, + 0, + address(0), + payable(0), + executeSignatures + ); + assertFalse(clientGateway.paused(), "Gateway should be unpaused"); + } else { + vm.expectRevert(); + multisig.execTransaction( + address(timelock), + 0, + executeData, + Enum.Operation.Call, + 0, + 0, + 0, + address(0), + payable(0), + executeSignatures + ); + assertTrue(clientGateway.paused(), "Gateway should still be paused"); + } + } + + function selectSigners(uint8 signersMask) internal view returns (Player[] memory) { + Player[] memory selectedSigners = new Player[](3); + uint256 signerCount = 0; + + if (signersMask & 1 != 0) { + selectedSigners[signerCount++] = signer1; + } + if (signersMask & 2 != 0) { + selectedSigners[signerCount++] = signer2; + } + if (signersMask & 4 != 0) { + selectedSigners[signerCount++] = signer3; + } + + // Resize the array to match the actual number of selected signers + assembly { + mstore(selectedSigners, signerCount) + } + + return selectedSigners; + } + + function signMultisigTransaction(address to, uint256 value, bytes memory data, Player[] memory signers) + internal + view + returns (bytes memory) + { + bytes32 txHash = multisig.getTransactionHash( + to, + value, + data, + Enum.Operation.Call, + 0, // safeTxGas + 0, // baseGas + 0, // gasPrice + address(0), // gasToken + payable(0), // refundReceiver + multisig.nonce() + ); + + // Sort signers array based on address + for (uint256 i = 0; i < signers.length - 1; i++) { + for (uint256 j = 0; j < signers.length - i - 1; j++) { + if (signers[j].addr > signers[j + 1].addr) { + Player memory temp = signers[j]; + signers[j] = signers[j + 1]; + signers[j + 1] = temp; + } + } + } + + // Generate sorted signatures + bytes memory signatures; + for (uint256 i = 0; i < signers.length; i++) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signers[i].privateKey, txHash); + signatures = abi.encodePacked(signatures, r, s, v); + } + + // return signatures + return signatures; + } + +} diff --git a/test/foundry/unit/Multisig.t.sol b/test/foundry/unit/Multisig.t.sol deleted file mode 100644 index 8b137891..00000000 --- a/test/foundry/unit/Multisig.t.sol +++ /dev/null @@ -1 +0,0 @@ - diff --git a/test/foundry/unit/TimelockController.t.sol b/test/foundry/unit/TimelockController.t.sol deleted file mode 100644 index 8b137891..00000000 --- a/test/foundry/unit/TimelockController.t.sol +++ /dev/null @@ -1 +0,0 @@ - From 302956e708bed34471717fd942380f1c7fb806b9 Mon Sep 17 00:00:00 2001 From: adu Date: Tue, 10 Sep 2024 09:47:43 +0800 Subject: [PATCH 9/9] doc: add governance doc --- docs/contract-governance.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 docs/contract-governance.md diff --git a/docs/contract-governance.md b/docs/contract-governance.md new file mode 100644 index 00000000..678b03b2 --- /dev/null +++ b/docs/contract-governance.md @@ -0,0 +1,36 @@ +# Governance + +## Overview + +The contract has privileged functions accessible only to the contract owner, excluding most user-facing functionalities. These functions are utilized for: + +- **Configuration**: Setting key contract parameters, such as the whitelist for stakeable assets. +- **Pause/Unpause**: Temporarily halting the contract in emergencies and resuming it once resolved. +- **Enable Messaging**: Activating or deactivating LayerZero messaging capabilities. +- **Upgradeability**: Transitioning the contract to a new implementation. + +The contract owner is effectively the protocol's governor, with governance primarily operating through the owner's proposal and execution of tasks. To facilitate governance and mitigate centralization risks, a two-tier governance structure is implemented for the contract owner: + +1. **CustomTimelockController**: A custom timelock controller contract that owns the business contract, allowing tasks to be proposed and executed through it. This custom controller features a circuit breaker role, enabling emergency contract pauses without waiting for the timelock period. +2. **Multisig**: Utilizing Safe Multisig wallets as the proposer/canceler, executor, circuit breaker, and even the admin of the custom timelock controller contract to avoid single point of failure. + +## `CustomTimelockController` + +`CustomTimelockController` is a custom timelock controller contract that inherits from the OpenZeppelin `TimelockController` contract. It has a special role named circuit breaker, which can be used to pause the contract in case of emergency without waiting for the timelock period(not applied to unpause). The main roles for the `CustomTimelockController` are: + +- **Proposer/Canceler**: Propose a new task or cancel a proposed task. +- **Executor**: Execute a task after the timelock period. +- **Circuit Breaker**: Pause the contract in case of emergency. +- **Admin**: Set the roles for the `CustomTimelockController`. + +## Safe Multisig + +We use the Safe Multisig wallets as the proposer/canceler, executor, circuit breaker and even the admin of the `CustomTimelockController`. For chains like Ethereum, we create the Safe Multisig wallets from deployed Safe proxy factory contract, and set the implementation as the deployed Safe. And for Exocore specifically, we deploy the set of Safe contracts, especially for the `GnosisSafeProxyFactory` and the `GnosisSafe`, `GnosisSafeL2` singletons. The deployed Safe contracts address can be found in the [deployment json file](../script/safe_contracts_on_exocore.json). + +## Governance Test + +We have some fuzzing tests to make sure governance works as expected. Please refer to the [governance test](../test/foundry/Governance.t.sol) for more details. + +## Governance in Production + +When the protocol is ready for production, we will set the Safe Multisig wallets as the proposer/canceler, executor, circuit breaker and even the admin of the `CustomTimelockController`, and manage our contracts through the timelock controller. At that time, we will decide the multisig wallet for each role, the threshold for each multisig wallet, the signers for each multisig wallet, and the timelock period for timelock controller. \ No newline at end of file