Skip to content

Commit

Permalink
feat: deploy multisig wallets and timelock contract to address centra…
Browse files Browse the repository at this point in the history
…lization related risks (#92)

* feat: use script to deploy multisig wallet on exocore

* forge install: safe-smart-account

v1.3.0-libs.0

* fix: remapping

* feat: add deployment script for exocore multisig

* chore: add utils directory

* feat: add circuit breaker role to customtimelockcontroller

* fix: solhint

* test: add fuzzing test for governance utilizing multisig and timelock

* doc: add governance doc
  • Loading branch information
adu-web3 authored Sep 10, 2024
1 parent 806c050 commit b769c58
Show file tree
Hide file tree
Showing 22 changed files with 618 additions and 16 deletions.
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -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
36 changes: 36 additions & 0 deletions docs/contract-governance.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions lib/safe-smart-account
Submodule safe-smart-account added at 767ef3
3 changes: 2 additions & 1 deletion remappings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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/
solidity-bytes-utils/=lib/solidity-bytes-utils/
@safe-contracts/=lib/safe-smart-account/contracts/
5 changes: 3 additions & 2 deletions script/12_RedeployClientChainGateway.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
2 changes: 1 addition & 1 deletion script/14_CorrectBootstrapErrors.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
72 changes: 72 additions & 0 deletions script/15_DeploySafeMulstisigWallet.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
pragma solidity ^0.8.13;

import {BaseScript} from "./BaseScript.sol";

import "@safe-contracts/GnosisSafeL2.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 {
super.setUp();

exocore = vm.createSelectFork(exocoreRPCURL);
_topUpPlayer(exocore, address(0), exocoreGenesis, deployer.addr, 2 ether);
}

function run() public {
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");

GnosisSafeProxyFactory proxyFactory = GnosisSafeProxyFactory(proxyFactoryAddress);
GnosisSafeL2 safeSingleton = GnosisSafeL2(payable(safeSingletonAddress));

// Set up owners
address[] memory owners = new address[](3);
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 = fallbackHandlerAddress;
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();
}

}
2 changes: 1 addition & 1 deletion script/2_DeployBoth.s.sol
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
5 changes: 3 additions & 2 deletions script/7_DeployBootstrap.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
2 changes: 1 addition & 1 deletion script/BaseScript.sol
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
18 changes: 18 additions & 0 deletions script/deployedMultisigWallets.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"holesky": {
"multisig": "0x9A7b23a0BB29F77BaBB8add484b34c02EfD327BE",
"signers": [
"0x481E020DB4709e6EdDbf8134D41b866c6Fc8555e",
"0x3583fF95f96b356d716881C871aF7Eb55ea34a93",
"0xA1dfab3234f49e02e04E6C56a021F1a497CD0f82"
]
},
"exocore": {
"multisig": "0xF27865277D8Cc608F279B3C09719A9Ceaa25A58f",
"signers": [
"0x481E020DB4709e6EdDbf8134D41b866c6Fc8555e",
"0x3583fF95f96b356d716881C871aF7Eb55ea34a93",
"0xA1dfab3234f49e02e04E6C56a021F1a497CD0f82"
]
}
}
5 changes: 3 additions & 2 deletions script/integration/1_DeployBootstrap.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions script/safe_contracts_on_exocore.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"SimulateTxAccessor": "0xB46E02a8c957892F7a1b7dD019bF7f56cA7830C6",
"GnosisSafeProxyFactory": "0xd92Eb22d59D2736C12ef8e009833b98dB812BC5F",
"DefaultCallbackHandler": "0xA9221e82f099027Da128369d323E249080507b78",
"CompatibilityFallbackHandler": "0x820ed29524601172Fe4aec900Bc48432067CBCDF",
"CreateCall": "0xA2c66e9eD611De51192EEfda6322E3D28b0c380c",
"MultiSend": "0x83Aa234126729346F9e6a33E109244935E521bEC",
"MultiSendCallOnly": "0x32A8c6b3c7D63002E1d230da5D525D6b6391796a",
"SignMessageLib": "0x30fdE0Cc889dEdD87cc11F48798506AbbC7B8c24",
"GnosisSafeL2": "0x9D24ad942d3453F574f3Df9C66504fDE009c14A0",
"GnosisSafe": "0xE28848a95D96dFc200A48f976b32B726253a8e14"
}
2 changes: 1 addition & 1 deletion src/storage/BootstrapStorage.sol
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
File renamed without changes.
File renamed without changes.
37 changes: 37 additions & 0 deletions src/utils/CustomTimelockController.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {TimelockController} from "@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();
}

}
2 changes: 1 addition & 1 deletion test/foundry/ExocoreDeployer.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Loading

0 comments on commit b769c58

Please sign in to comment.