diff --git a/script/releases/Env.sol b/script/releases/Env.sol index 7a5410c944..8aa12b0a3b 100644 --- a/script/releases/Env.sol +++ b/script/releases/Env.sol @@ -92,6 +92,10 @@ library Env { return _envAddress("executorMultisig"); } + function beigenExecutorMultisig() internal view returns (address) { + return _envAddress("beigenExecutorMultisig"); + } + function opsMultisig() internal view returns (address) { return _envAddress("operationsMultisig"); } @@ -120,6 +124,10 @@ library Env { return _envAddress("proxyAdmin"); } + function beigenProxyAdmin() internal view returns (address) { + return _envAddress("beigenProxyAdmin"); + } + function ethPOS() internal view returns (IETHPOSDeposit) { return IETHPOSDeposit(_envAddress("ethPOS")); } @@ -128,6 +136,10 @@ library Env { return TimelockController(payable(_envAddress("timelockController"))); } + function beigenTimelockController() internal view returns (TimelockController) { + return TimelockController(payable(_envAddress("beigenTimelockController"))); + } + function multiSendCallOnly() internal view returns (address) { return _envAddress("MultiSendCallOnly"); } @@ -412,14 +424,14 @@ library Env { function beigen( DeployedProxy - ) internal view returns (IBackingEigen) { - return IBackingEigen(_deployedProxy(type(BackingEigen).name)); + ) internal view returns (BackingEigen) { + return BackingEigen(_deployedProxy(type(BackingEigen).name)); } function beigen( DeployedImpl - ) internal view returns (IBackingEigen) { - return IBackingEigen(_deployedImpl(type(BackingEigen).name)); + ) internal view returns (BackingEigen) { + return BackingEigen(_deployedImpl(type(BackingEigen).name)); } /** diff --git a/script/releases/v1.6.0-multichain-deployer/1-deployMultichainDeployer.s.sol b/script/releases/MultisigDeployLib.sol similarity index 52% rename from script/releases/v1.6.0-multichain-deployer/1-deployMultichainDeployer.s.sol rename to script/releases/MultisigDeployLib.sol index e61f0f9d85..e38971d33c 100644 --- a/script/releases/v1.6.0-multichain-deployer/1-deployMultichainDeployer.s.sol +++ b/script/releases/MultisigDeployLib.sol @@ -1,29 +1,10 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.12; -import {EOADeployer} from "zeus-templates/templates/EOADeployer.sol"; -import "forge-std/console.sol"; -import "../../releases/Env.sol"; - -/// @notice Deploy the multichain deployer multisig -/// @dev This script is used to deploy the multichain deployer multisig on the destination chain -/// @dev This script should ONLY be used for mainnet environments. Testnet environments should follow our notion guide -/// TODO: Add a testnet version of this script -/// @dev The SAFE version is 1.4.1 -contract DeployMultichainDeployer is EOADeployer { - using Env for *; - - /// @dev The expected address of the multichain deployer on the destination chain - address public constant MULTICHAIN_DEPLOYER_EXPECTED_ADDRESS = 0xa3053EF25F1F7d9D55a7655372B8a31D0f40eCA9; - - /// @dev Salt for deploying the multichain deployer multisig - uint256 public constant SALT = 0; - - /// @dev Initial threshold for the multichain deployer multisig - uint256 public constant INITIAL_THRESHOLD = 1; - - /// @dev Initial owner of the multichain deployer multisig - address public constant INITIAL_OWNER = 0x792e42f05E87Fb9D8b8F9FdFC598B1de20507964; +/// @notice A library for deploying Safe multisigs +/// @dev Double check the constant addresses are correct on each chain prior to deployment +library MultisigDeployLib { + using MultisigDeployLib for *; /// @dev Safe proxy factory, this should be the same for all chains /// @dev See https://github.com/safe-global/safe-smart-account/blob/main/CHANGELOG.md#version-141 for more details @@ -35,76 +16,65 @@ contract DeployMultichainDeployer is EOADeployer { /// @dev L2 Singletons still need to be passed into the L1 deployment /// @dev `SAFE_TO_L2_SETUP` does a no-op if the chain is mainnet /// @dev See: https://github.com/safe-global/safe-smart-account/blob/0095f1aa113255d97b476e625760514cc7d10982/contracts/libraries/SafeToL2Setup.sol#L59-L69 + /// @dev Even mainnet testnet chains (eg. sepolia, hoodi) should use the SAFE_L2_SINGLETON address public constant SAFE_L2_SINGLETON = 0x29fcB43b46531BcA003ddC8FCB67FFE91900C762; address public constant SAFE_TO_L2_SETUP = 0xBD89A1CE4DDe368FFAB0eC35506eEcE0b1fFdc54; + bytes public constant SETUP_TO_L2_DATA = abi.encodeWithSignature("setupToL2(address)", SAFE_L2_SINGLETON); - function _runAsEOA() internal override { - vm.startBroadcast(); - - address multichainDeployerMultisig = ISafeProxyFactory(SAFE_PROXY_FACTORY).createProxyWithNonce({ - _singleton: SAFE_SINGLETON, - initializer: getInitializationData(), - saltNonce: SALT - }); - - vm.stopBroadcast(); - - // Update config - zUpdate("multichainDeployerMultisig", multichainDeployerMultisig); - } - - function testScript() public virtual { - // If the multichain deployer multisig is already deployed, we need to add the contracts to the env - if (_isDeployerMultisigDeployed()) { - _addContractsToEnv(); - } else { - // Otherwise, we need to deploy the multichain deployer multisig - super.runAsEOA(); - } - - // Check that the multichain deployer multisig is deployed at the expected address - assertEq(Env.multichainDeployerMultisig(), MULTICHAIN_DEPLOYER_EXPECTED_ADDRESS); - - // Check the threshold is 1 - assertEq(IMultisig(address(Env.multichainDeployerMultisig())).getThreshold(), 1); - - // Check owners - address[] memory owners = IMultisig(address(Env.multichainDeployerMultisig())).getOwners(); - assertEq(owners.length, 1, "Expected 1 owner"); - assertEq(owners[0], INITIAL_OWNER, "Expected initial owner"); - - // Check the version is 1.4.1 - assertEq(IMultisig(address(Env.multichainDeployerMultisig())).VERSION(), "1.4.1"); + /** + * @notice Deploys a Safe multisig + * @param initialOwners The initial owners of the multisig + * @param initialThreshold The initial threshold of the multisig + * @param salt The salt to use for the deployment + * @return The address of the deployed multisig + */ + function deployMultisig( + address[] memory initialOwners, + uint256 initialThreshold, + uint256 salt + ) internal returns (address) { + return deployMultisigWithPaymentReceiver(initialOwners, initialThreshold, salt, address(0)); } - function getInitializationData() internal pure returns (bytes memory) { - // Setup initial owners - address[] memory initialOwners = new address[](1); - initialOwners[0] = INITIAL_OWNER; - - // Setup the multisig - return abi.encodeWithSelector( + /// @notice Used to deploy a multisig with a payment receiver. This is used by multichain deployer on testnet chains. + function deployMultisigWithPaymentReceiver( + address[] memory initialOwners, + uint256 initialThreshold, + uint256 salt, + address paymentReceiver + ) internal returns (address) { + bytes memory initializerData = abi.encodeWithSelector( IMultisig.setup.selector, initialOwners, /* signers */ - INITIAL_THRESHOLD, /* threshold */ + initialThreshold, /* threshold */ SAFE_TO_L2_SETUP, /* to (used in setupModules) */ - abi.encodeWithSignature("setupToL2(address)", SAFE_L2_SINGLETON), /* data (used in setupModules) */ + SETUP_TO_L2_DATA, /* data (used in setupModules) */ SAFE_FALLBACK_HANDLER, /* fallbackHandler */ address(0), /* paymentToken */ 0, /* payment */ - payable(address(0)) /* paymentReceiver */ + payable(paymentReceiver) /* paymentReceiver */ ); + + address deployedMultisig = + ISafeProxyFactory(SAFE_PROXY_FACTORY).createProxyWithNonce(SAFE_SINGLETON, initializerData, salt); + require(deployedMultisig != address(0), "something wrong in multisig deployment, zero address returned"); + return deployedMultisig; + } + + function getThreshold( + address multisig + ) internal view returns (uint256) { + return IMultisig(multisig).getThreshold(); } - function _isDeployerMultisigDeployed() internal view returns (bool) { - return MULTICHAIN_DEPLOYER_EXPECTED_ADDRESS.code.length > 0; + function getOwners( + address multisig + ) internal view returns (address[] memory) { + return IMultisig(multisig).getOwners(); } - /// @dev Add contracts to the env - /// @dev This function should only be called if the multichain deployer multisig is already deployed - function _addContractsToEnv() internal { - require(MULTICHAIN_DEPLOYER_EXPECTED_ADDRESS.code.length > 0, "Multichain deployer multisig not deployed"); - zUpdate("multichainDeployerMultisig", MULTICHAIN_DEPLOYER_EXPECTED_ADDRESS); + function isOwner(address multisig, address owner) internal view returns (bool) { + return IMultisig(multisig).isOwner(owner); } } diff --git a/script/releases/Scripts.md b/script/releases/Scripts.md new file mode 100644 index 0000000000..31935fcf77 --- /dev/null +++ b/script/releases/Scripts.md @@ -0,0 +1,51 @@ +# EigenLayer Release Scripts + +This document describes the various release scripts available in the `script/releases/` directory. + +## Multichain Multisig Deployer Scripts + +### 1. multichain-deployer-mainnet +Deploys a multichain deployer multisig to mainnet environments (e.g., Base, Ethereum Mainnet). +- Initializes the multisig to be 3/7 +- 6 of the 7 signers come from the ops multisig + +### 2. multichain-deployer-testnet-preprod +Deploys a multichain deployer multisig to testnet and preprod environments. +- Configured as a 1/n multisig +- Has 0xDA as the signer + +## Destination Chain Initialization Scripts + +### 3. v1.6.0-destination-genesis-mainnet +Deploys foundational contracts for a destination chain +- Proxy Admin +- Ops Multisig +- Pauser Multisig + +### 4. v1.6.0-destination-governance-mainnet +Deploys governance infrastructure for a destination chain +- Timelock Controller (with 1 day delay) +- Protocol Council Multisig +- Community Multisig +- Executor Multisig +- The protocol council and community multisigs are initialized to be the mainnet ops multisig signers with 3/n quorum + +### 5. v1.6.0-destination-governance-testnet +Same as the mainnet governance deployment above, but with testnet-specific configurations: +- Timelock delay set to 1 second +- The owner of the multisig is the 0xDA address +- Threshold is 1 + +## Protocol Deployment Scripts + +### 6. v1.6.0-protocol-from-scratch +Deploys the entire EigenLayer protocol from scratch to v1.6.0, including: +- All contracts up to slashing +- Governance infrastructure +- Token contracts +*Note: This should not be used on destination chains, only the below should be used.* This script is useful to initiate a net new *full core protocol deployment* on a testnet chain. + +### 7. v1.7.0-v1.8.0-multichain-hourglass-combined +Upgrades the protocol to support: +- Multichain functionality +- Hourglass features diff --git a/script/releases/TOML.md b/script/releases/TOML.md deleted file mode 100644 index add78d3609..0000000000 --- a/script/releases/TOML.md +++ /dev/null @@ -1,246 +0,0 @@ -## `parseToml` - -### Signature - -```solidity -// Return the value(s) that correspond to 'key' -vm.parseToml(string memory toml, string memory key) -// Return the entire TOML file -vm.parseToml(string memory toml); -``` - -### Description - -These cheatcodes are used to parse TOML files in the form of strings after converting to JSON. Usually, it's coupled with `vm.readFile()` which returns an entire file in the form of a string. - -You can use `stdToml` from `forge-std`, as a helper library for better UX. - -The cheatcode accepts either a `key` to search for a specific value in the TOML, or no key to return the entire TOML. It returns the value as an abi-encoded `bytes` array. That means that you will have to `abi.decode()` to the appropriate type for it to function properly, else it will `revert`. - -### JSONpath Key - -`parseToml` uses a syntax called JSONpath to form arbitrary keys for arbitrary JSON files. The same syntax (or rather a dialect) is used by the tool [`jq`](https://stedolan.github.io/jq/). - -To read more about the syntax, you can visit the [README](https://crates.io/crates/jsonpath-rust) of the rust library that we use under the hood to implement the feature. That way you can be certain that you are using the correct dialect of jsonPath. - -### Encoding Rules - -We use the terms `string`, `integer`, `float`, `boolean`, `array`, `datetime`, `inline-table` as they are defined in the [TOML spec](https://www.w3schools.io/file/toml-datatypes/). - -We use the terms `number`, `string`, `object`, `array`, `boolean`, `null` as they are defined in the [JSON spec](https://www.w3schools.com/js/js_json_datatypes.asp). - -**TOML Encoding Rules** - -- `float` is limited to 32 bits (i.e. `+1.5`). It is recommended to use strings to prevent precision loss -- `integer` is limited to 64 bits (i.e. `9223372036854775807`). It is recommended to use strings to encode large values -- Array values cannot have mixed types (i.e. `[256, "b"]`, only `[256, 512]` or `["a", "b"]`) -- `datetime` is encoded as a `string` upon conversion -- `float` is encoded as a `number` upon conversion -- `integer` is encoded as a `number` upon conversion -- `inline-table` (or `table`) is encoded as `object` upon conversion -- `null` is encoded as a `"null"` string - -**JSON Encoding Rules** - -- `null` is encoded as `bytes32(0)` or `""` -- Numbers >= 0 are encoded as `uint256` -- Negative numbers are encoded as `int256` -- Floating point numbers with decimal digits are not allowed -- Floating point numbers using the scientific notation can be `uint256` or `int256` depending on the value -- A string that can be decoded into a type of `H160` and starts with `0x` is encoded as an `address`. In other words, if it can be decoded into an address, it's probably an address -- A string that starts with `0x` is encoded as `bytes32` if it has a length of `66` or else to `bytes` -- A string that is neither an `address`, a `bytes32` or `bytes`, is encoded as a `string` -- An array is encoded as a dynamic array of the type of its first element -- An object (`{}`) is encoded as a `tuple` - -### Type Coercion - -As described above, `parseToml` needs to deduce the type of TOML value and that has some inherent limitations. For that reason, there is a sub-family of `parseToml*` cheatcodes that coerce the type of the returned value. - -For example `vm.parseTomlUint(toml, key)` will coerce the value to a `uint256`. That means that it can parse all the following values and return them as a `uint256`. That includes a number as type `number`, a stringified number as a `string` and of course it's hex representation. - -```toml -hexUint = "0x12C980" -stringUint = "115792089237316195423570985008687907853269984665640564039457584007913129639935" -numberUint = 9223372036854775807 # TOML is limited to 64-bit integers -``` - -Similarly, there are cheatcodes for all types (including `bytes` and `bytes32`) and their arrays (`vm.parseTomlUintArray`). - -### Decoding TOML tables into Solidity structs - -TOML tables are converted to JSON objects. JSON objects are encoded as tuples, and can be decoded via tuples or structs. That means that you can define a `struct` in Solidity and it will decode the entire JSON object into that `struct`. - -For example: - -The following TOML: - -```toml -a = 43 -b = "sigma" -``` - -will be converted into the following JSON: - -```json -{ - "a": 43, - "b": "sigma" -} -``` - -will be decoded into: - -```solidity -struct Json { - uint256 a; - string b; -} -``` - -As the values are returned as an abi-encoded tuple, the exact name of the attributes of the struct don't need to match the names of the keys in the JSON. The above json file could also be decoded as: - -```solidity -struct Json { - uint256 apple; - string pineapple; -} -``` - -What matters is the alphabetical order. As the JSON object is an unordered data structure but the tuple is an ordered one, we had to somehow give order to the JSON. The easiest way was to order the keys by alphabetical order. That means that in order to decode the JSON object correctly, you will need to define attributes of the struct with **types** that correspond to the values of the alphabetical order of the keys of the JSON. - -- The struct is interpreted serially. That means that the tuple's first item will be decoded based on the first item of the struct definition (no alphabetical order). -- The JSON will parsed alphabetically, not serially. -- Note that this parsing uses Rust's BTreeMap crate under the hood, meaning that uppercase and lowercase strings are treated differently. Uppercase characters _precede_ lowercase in this lexicographical ordering, ie "Zebra" would precede "apple". - -Thus, the first (in alphabetical order) value of the JSON, will be abi-encoded and then tried to be abi-decoded, based on the type of the first attribute of the `struct`. - -The above TOML would not be able to be decoded with the struct below: - -```solidity -struct Json { - uint256 b; - uint256 a; -} -``` - -The reason is that it would try to decode the string `"sigma"` as a uint. To be exact, it would be decoded, but it would result to a wrong number, since it would interpret the bytes incorrectly. - -Another example, given the following TOML: - -```toml -name = "Fresh Fruit" - -[[apples]] -sweetness = 7 -sourness = 3 -color = "Red" - -[[apples]] -sweetness = 5 -sourness = 5 -color = "Green" - -[[apples]] -sweetness = 9 -sourness = 1 -color = "Yellow" -``` - -will be converted into the following JSON: - -```json -{ - "apples": [ - { - "sweetness": 7, - "sourness": 3, - "color": "Red" - }, - { - "sweetness": 5, - "sourness": 5, - "color": "Green" - }, - { - "sweetness": 9, - "sourness": 1, - "color": "Yellow" - } - ], - "name": "Fresh Fruit" -} -``` - -And Solidity structs defined as follows: - -```solidity -struct Apple { - string color; - uint8 sourness; - uint8 sweetness; -} - -struct FruitStall { - Apple[] apples; - string name; -} -``` - -One would decode the TOML as follows: - -```solidity -string memory root = vm.projectRoot(); -string memory path = string.concat(root, "/src/test/fixtures/fruitstall.toml"); -string memory toml = vm.readFile(path); -bytes memory data = vm.parseToml(toml); -FruitStall memory fruitstall = abi.decode(data, (FruitStall)); - -// Logs: Welcome to Fresh Fruit -console2.log("Welcome to", fruitstall.name); - -for (uint256 i = 0; i < fruitstall.apples.length; i++) { - Apple memory apple = fruitstall.apples[i]; - - // Logs: - // Color: Red, Sourness: 3, Sweetness: 7 - // Color: Green, Sourness: 5, Sweetness: 5 - // Color: Yellow, Sourness: 1, Sweetness: 9 - console2.log( - "Color: %s, Sourness: %d, Sweetness: %d", - apple.color, - apple.sourness, - apple.sweetness - ); -} -``` - -### How to use StdToml - -1. Import the library `import {stdToml} from "forge-std/StdToml.sol";` -2. Define its usage with `string`: `using stdToml for string;` -3. If you want to parse simple values (numbers, address, etc.) use the helper functions -4. If you want to parse entire TOML tables: - 1. Define the `struct` in Solidity. Make sure to follow the alphabetical order -- it's hard to debug - 2. Use the `parseRaw()` helper function to return abi-encoded `bytes` and then decode them to your struct - -```solidity -string memory root = vm.projectRoot(); -string memory path = string.concat(root, "/src/test/fixtures/config.toml"); -string memory toml = vm.readFile(path); -bytes memory data = toml.parseRaw("."); -Config memory config = abi.decode(data, (Config)) -``` - -### Troubleshooting - -#### Cannot read file - -> FAIL. Reason: The path `` is not allowed to be accessed for read operations - -If you receive this error, make sure that you enable read permissions in `foundry.toml` using the [`fs_permissions` key](/reference/cheatcodes/fs.mdx) - -### References - -- Helper Library: [stdToml.sol](https://github.com/foundry-rs/forge-std/blob/master/src/StdToml.sol) -- [File Cheatcodes](/reference/cheatcodes/fs.mdx): cheatcodes for working with files diff --git a/script/releases/v0.0.0-multichain-deployer-mainnet/1-deployMultichainDeployer.s.sol b/script/releases/v0.0.0-multichain-deployer-mainnet/1-deployMultichainDeployer.s.sol new file mode 100644 index 0000000000..736a597f26 --- /dev/null +++ b/script/releases/v0.0.0-multichain-deployer-mainnet/1-deployMultichainDeployer.s.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import {EOADeployer} from "zeus-templates/templates/EOADeployer.sol"; +import {MultisigDeployLib, IMultisig} from "script/releases/MultisigDeployLib.sol"; +import "forge-std/console.sol"; +import "../../releases/Env.sol"; + +/// @notice Deploy the multichain deployer multisig +/// @dev This script is used to deploy the multichain deployer multisig on the destination chain +/// @dev This script should ONLY be used for mainnet environments. Testnet environments should follow our notion guide +/// @dev Use v1.6.0-multichain-deployer-testnet-preprod for testnet environments +/// @dev The SAFE version is 1.4.1 +contract DeployMultichainDeployer is EOADeployer { + using Env for *; + + /// @dev The expected address of the multichain deployer on the destination chain + address public constant MULTICHAIN_DEPLOYER_EXPECTED_ADDRESS = 0xa3053EF25F1F7d9D55a7655372B8a31D0f40eCA9; + + /// @dev Salt for deploying the multichain deployer multisig + uint256 public constant SALT = 0; + + /// @dev Initial threshold for the multichain deployer multisig + uint256 public constant INITIAL_THRESHOLD = 1; + + /// @dev Initial owner of the multichain deployer multisig + address public constant INITIAL_OWNER = 0x792e42f05E87Fb9D8b8F9FdFC598B1de20507964; + + function _runAsEOA() internal override { + vm.startBroadcast(); + + address[] memory initialOwners = new address[](1); + initialOwners[0] = INITIAL_OWNER; + + address multichainDeployerMultisig = MultisigDeployLib.deployMultisig({ + initialOwners: initialOwners, + initialThreshold: INITIAL_THRESHOLD, + salt: SALT + }); + + vm.stopBroadcast(); + + // Update config + zUpdate("multichainDeployerMultisig", multichainDeployerMultisig); + } + + function testScript() public virtual { + // If the multichain deployer multisig is already deployed, we need to add the contracts to the env + if (_isDeployerMultisigDeployed()) { + _addContractsToEnv(); + } else { + // Otherwise, we need to deploy the multichain deployer multisig + super.runAsEOA(); + } + + // Check that the multichain deployer multisig is deployed at the expected address + assertEq(Env.multichainDeployerMultisig(), MULTICHAIN_DEPLOYER_EXPECTED_ADDRESS); + + // Check the threshold is 1 + assertEq(IMultisig(address(Env.multichainDeployerMultisig())).getThreshold(), 1); + + // Check owners + address[] memory owners = IMultisig(address(Env.multichainDeployerMultisig())).getOwners(); + assertEq(owners.length, 1, "Expected 1 owner"); + assertEq(owners[0], INITIAL_OWNER, "Expected initial owner"); + + // Check the version is 1.4.1 + assertEq(IMultisig(address(Env.multichainDeployerMultisig())).VERSION(), "1.4.1"); + } + + function _isDeployerMultisigDeployed() internal view returns (bool) { + return MULTICHAIN_DEPLOYER_EXPECTED_ADDRESS.code.length > 0; + } + + /// @dev Add contracts to the env + /// @dev This function should only be called if the multichain deployer multisig is already deployed + function _addContractsToEnv() internal { + require(MULTICHAIN_DEPLOYER_EXPECTED_ADDRESS.code.length > 0, "Multichain deployer multisig not deployed"); + zUpdate("multichainDeployerMultisig", MULTICHAIN_DEPLOYER_EXPECTED_ADDRESS); + } +} diff --git a/script/releases/v1.6.0-multichain-deployer/2-transferDeployerOwnership.s.sol b/script/releases/v0.0.0-multichain-deployer-mainnet/2-transferDeployerOwnership.s.sol similarity index 97% rename from script/releases/v1.6.0-multichain-deployer/2-transferDeployerOwnership.s.sol rename to script/releases/v0.0.0-multichain-deployer-mainnet/2-transferDeployerOwnership.s.sol index 18460038b0..8215a633b4 100644 --- a/script/releases/v1.6.0-multichain-deployer/2-transferDeployerOwnership.s.sol +++ b/script/releases/v0.0.0-multichain-deployer-mainnet/2-transferDeployerOwnership.s.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.12; import "./1-deployMultichainDeployer.s.sol"; import {MultisigBuilder} from "zeus-templates/templates/MultisigBuilder.sol"; +import {IMultisig} from "script/releases/MultisigDeployLib.sol"; import "../../releases/Env.sol"; // For TOML parsing diff --git a/script/releases/v1.6.0-multichain-deployer/README.md b/script/releases/v0.0.0-multichain-deployer-mainnet/README.md similarity index 100% rename from script/releases/v1.6.0-multichain-deployer/README.md rename to script/releases/v0.0.0-multichain-deployer-mainnet/README.md diff --git a/script/releases/v1.6.0-multichain-deployer/owners.toml b/script/releases/v0.0.0-multichain-deployer-mainnet/owners.toml similarity index 100% rename from script/releases/v1.6.0-multichain-deployer/owners.toml rename to script/releases/v0.0.0-multichain-deployer-mainnet/owners.toml diff --git a/script/releases/v1.6.0-multichain-deployer/upgrade.json b/script/releases/v0.0.0-multichain-deployer-mainnet/upgrade.json similarity index 100% rename from script/releases/v1.6.0-multichain-deployer/upgrade.json rename to script/releases/v0.0.0-multichain-deployer-mainnet/upgrade.json diff --git a/script/releases/v0.0.0-multichain-deployer-testnet-preprod/1-deployMultichainDeployer.s.sol b/script/releases/v0.0.0-multichain-deployer-testnet-preprod/1-deployMultichainDeployer.s.sol new file mode 100644 index 0000000000..fad6fc920a --- /dev/null +++ b/script/releases/v0.0.0-multichain-deployer-testnet-preprod/1-deployMultichainDeployer.s.sol @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import {EOADeployer} from "zeus-templates/templates/EOADeployer.sol"; +import {MultisigDeployLib, IMultisig} from "script/releases/MultisigDeployLib.sol"; +import "forge-std/console.sol"; +import "../../releases/Env.sol"; + +/// @notice Deploy the multichain deployer multisig +/// @dev This script is used to deploy the multichain deployer multisig on the destination chain +/// @dev This script should ONLY be used for testnet environments +/// @dev The SAFE version is 1.4.1 +contract DeployMultichainDeployer is EOADeployer { + using Env for *; + + /// @notice The type of environment we are deploying to + enum EnvType { + NULL, + PREPROD, + TESTNET + } + + /// @dev The environment type + EnvType public envType = EnvType.NULL; + + /// @dev Setup the environment type + modifier setupEnv() { + if (Env._strEq(Env.env(), "preprod") || Env._strEq(Env.env(), "preprod-hoodi")) { + envType = EnvType.PREPROD; + } else if ( + Env._strEq(Env.env(), "testnet-sepolia") || Env._strEq(Env.env(), "testnet-base-sepolia") + || Env._strEq(Env.env(), "testnet-hoodi") + ) { + envType = EnvType.TESTNET; + } else { + revert("Invalid environment"); + } + _; + } + + /// @dev The expected address of the multichain deployer on the destination chain + address public constant MULTICHAIN_DEPLOYER_EXPECTED_ADDRESS_TESTNET = 0xA591635DE4C254BD3fa9C9Db9000eA6488344C28; + address public constant MULTICHAIN_DEPLOYER_EXPECTED_ADDRESS_PREPROD = 0x011aF0ABDBcffBF879de4E1F1F8e346bDCAc0653; + + /// @dev Salt for deploying the multichain deployer multisig + uint256 public constant SALT_TESTNET = 5; + uint256 public constant SALT_PREPROD = 6; + + /// @dev Initial threshold for the multichain deployer multisig - same for testnet and preprod + uint256 public constant INITIAL_THRESHOLD = 1; + + /// @dev Initial owner of the multichain deployer multisig - same for testnet and preprod + address public constant INITIAL_OWNER = 0xDA29BB71669f46F2a779b4b62f03644A84eE3479; + + /// @dev Payment receiver for the multichain deployer multisig - same for testnet and preprod + /// @dev These addresses were originally deployed via the UI, which included a payment receiver + address public constant PAYMENT_RECEIVER = 0x5afe7A11E7000000000000000000000000000000; + + function _runAsEOA() internal override setupEnv { + vm.startBroadcast(); + + address[] memory initialOwners = new address[](1); + initialOwners[0] = INITIAL_OWNER; + + address multichainDeployerMultisig = MultisigDeployLib.deployMultisigWithPaymentReceiver({ + initialOwners: initialOwners, + initialThreshold: INITIAL_THRESHOLD, + salt: _getSalt(), + paymentReceiver: PAYMENT_RECEIVER + }); + + vm.stopBroadcast(); + + // Update config + zUpdate("multichainDeployerMultisig", multichainDeployerMultisig); + } + + function testScript() public virtual setupEnv { + // If the multichain deployer multisig is already deployed, we need to add the contracts to the env + if (_isDeployerMultisigDeployed()) { + _addContractsToEnv(); + } else { + // Otherwise, we need to deploy the multichain deployer multisig + super.runAsEOA(); + } + + // Check that the multichain deployer multisig is deployed at the expected address + assertEq(Env.multichainDeployerMultisig(), _getExpectedDeployerMultisig()); + + // Check the threshold is 1 + assertEq(IMultisig(address(Env.multichainDeployerMultisig())).getThreshold(), 1); + + // Check owners + address[] memory owners = IMultisig(address(Env.multichainDeployerMultisig())).getOwners(); + assertEq(owners.length, 1, "Expected 1 owner"); + assertEq(owners[0], INITIAL_OWNER, "Expected initial owner"); + + // Check the version is 1.4.1 + assertEq(IMultisig(address(Env.multichainDeployerMultisig())).VERSION(), "1.4.1"); + } + + /// @dev Check if the multichain deployer multisig is deployed for a given environment type + function _isDeployerMultisigDeployed() internal view returns (bool) { + address deployer = _getExpectedDeployerMultisig(); + return deployer.code.length > 0; + } + + /// @dev Add contracts to the env + /// @dev This function should only be called if the multichain deployer multisig is already deployed + function _addContractsToEnv() internal { + address deployer = _getExpectedDeployerMultisig(); + require(_isDeployerMultisigDeployed(), "Multichain deployer multisig not deployed"); + zUpdate("multichainDeployerMultisig", deployer); + } + + /// @dev Get the salt for a given environment type + function _getSalt() internal view returns (uint256) { + if (envType == EnvType.PREPROD) { + return SALT_PREPROD; + } else if (envType == EnvType.TESTNET) { + return SALT_TESTNET; + } + revert("Invalid environment type"); + } + + function _getExpectedDeployerMultisig() internal view returns (address) { + if (envType == EnvType.PREPROD) { + return MULTICHAIN_DEPLOYER_EXPECTED_ADDRESS_PREPROD; + } else if (envType == EnvType.TESTNET) { + return MULTICHAIN_DEPLOYER_EXPECTED_ADDRESS_TESTNET; + } + revert("Invalid environment type"); + } +} diff --git a/script/releases/v0.0.0-multichain-deployer-testnet-preprod/upgrade.json b/script/releases/v0.0.0-multichain-deployer-testnet-preprod/upgrade.json new file mode 100644 index 0000000000..449996ec78 --- /dev/null +++ b/script/releases/v0.0.0-multichain-deployer-testnet-preprod/upgrade.json @@ -0,0 +1,11 @@ +{ + "name": "multichain-deployer-testnet-preprod", + "from": ">=0.0.0", + "to": "0.0.0", + "phases": [ + { + "type": "eoa", + "filename": "1-deployMultichainDeployer.s.sol" + } + ] +} \ No newline at end of file diff --git a/script/releases/v1.6.0-destination-genesis/1-deployContracts.s.sol b/script/releases/v1.6.0-destination-genesis-mainnet/1-deployContracts.s.sol similarity index 58% rename from script/releases/v1.6.0-destination-genesis/1-deployContracts.s.sol rename to script/releases/v1.6.0-destination-genesis-mainnet/1-deployContracts.s.sol index b407714c28..f98f3a843d 100644 --- a/script/releases/v1.6.0-destination-genesis/1-deployContracts.s.sol +++ b/script/releases/v1.6.0-destination-genesis-mainnet/1-deployContracts.s.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.12; import {EOADeployer} from "zeus-templates/templates/EOADeployer.sol"; +import {MultisigDeployLib, IMultisig} from "script/releases/MultisigDeployLib.sol"; import "../Env.sol"; // For TOML parsing @@ -30,12 +31,18 @@ contract DeployDestinationGenesis is EOADeployer { vm.startBroadcast(); // Deploy opsMultisig - address opsMultisig = - deployMultisig({initialOwners: opsMultisigInitialOwners, initialThreshold: 3, salt: ++salt}); + address opsMultisig = MultisigDeployLib.deployMultisig({ + initialOwners: opsMultisigInitialOwners, + initialThreshold: 3, + salt: ++salt + }); // Deploy pauserMultisig - address pauserMultisig = - deployMultisig({initialOwners: pauserMultisigInitialOwners, initialThreshold: 1, salt: ++salt}); + address pauserMultisig = MultisigDeployLib.deployMultisig({ + initialOwners: pauserMultisigInitialOwners, + initialThreshold: 1, + salt: ++salt + }); // Deploy pauserRegistry address[] memory pausers = new address[](2); @@ -74,8 +81,8 @@ contract DeployDestinationGenesis is EOADeployer { assertNotEq(Env.pauserMultisig(), address(0)); // Check the threshold of each multisig - assertEq(Multisig(address(Env.opsMultisig())).getThreshold(), 3); - assertEq(Multisig(address(Env.pauserMultisig())).getThreshold(), 1); + assertEq(IMultisig(address(Env.opsMultisig())).getThreshold(), 3); + assertEq(IMultisig(address(Env.pauserMultisig())).getThreshold(), 1); // Assert the owners of each multisig address[] memory opsMultisigOwners = @@ -83,21 +90,21 @@ contract DeployDestinationGenesis is EOADeployer { address[] memory pauserMultisigOwners = _getMultisigOwner("script/releases/v1.6.0-destination-genesis/pauserOwners.toml"); for (uint256 i = 0; i < opsMultisigOwners.length; i++) { - assertTrue(Multisig(address(Env.opsMultisig())).isOwner(opsMultisigOwners[i])); + assertTrue(IMultisig(address(Env.opsMultisig())).isOwner(opsMultisigOwners[i])); } for (uint256 i = 0; i < pauserMultisigOwners.length; i++) { - assertTrue(Multisig(address(Env.pauserMultisig())).isOwner(pauserMultisigOwners[i])); + assertTrue(IMultisig(address(Env.pauserMultisig())).isOwner(pauserMultisigOwners[i])); } // Assert owner counts are correct assertEq( opsMultisigOwners.length, - Multisig(address(Env.opsMultisig())).getOwners().length, + IMultisig(address(Env.opsMultisig())).getOwners().length, "opsMultisigOwners length mismatch" ); assertEq( pauserMultisigOwners.length, - Multisig(address(Env.pauserMultisig())).getOwners().length, + IMultisig(address(Env.pauserMultisig())).getOwners().length, "pauserMultisigOwners length mismatch" ); @@ -107,41 +114,6 @@ contract DeployDestinationGenesis is EOADeployer { assertEq(Env.impl.pauserRegistry().unpauser(), Env.opsMultisig()); } - function deployMultisig( - address[] memory initialOwners, - uint256 initialThreshold, - uint256 salt - ) internal returns (address) { - // addresses taken from https://github.com/safe-global/safe-smart-account/blob/main/CHANGELOG.md#expected-addresses-with-deterministic-deployment-proxy-default - // NOTE: double check these addresses are correct on each chain - address safeFactory = 0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67; - address safeSingleton = 0x29fcB43b46531BcA003ddC8FCB67FFE91900C762; // Gnosis safe L2 singleton - address safeFallbackHandler = 0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99; - - bytes memory emptyData; - - bytes memory initializerData = abi.encodeWithSignature( - "setup(address[],uint256,address,bytes,address,address,uint256,address)", - initialOwners, /* signers */ - initialThreshold, /* threshold */ - address(0), /* to (used in setupModules) */ - emptyData, /* data (used in setupModules) */ - safeFallbackHandler, - address(0), /* paymentToken */ - 0, /* payment */ - payable(address(0)) /* paymentReceiver */ - ); - - bytes memory calldataToFactory = - abi.encodeWithSignature("createProxyWithNonce(address,bytes,uint256)", safeSingleton, initializerData, salt); - - (bool success, bytes memory returndata) = safeFactory.call(calldataToFactory); - require(success, "multisig deployment failed"); - address deployedMultisig = abi.decode(returndata, (address)); - require(deployedMultisig != address(0), "something wrong in multisig deployment, zero address returned"); - return deployedMultisig; - } - function _getMultisigOwner( string memory path ) internal view returns (address[] memory) { @@ -156,11 +128,3 @@ contract DeployDestinationGenesis is EOADeployer { return owners; } } - -interface Multisig { - function getThreshold() external view returns (uint256); - function getOwners() external view returns (address[] memory); - function isOwner( - address owner - ) external view returns (bool); -} diff --git a/script/releases/v1.6.0-destination-genesis/opsOwners.toml b/script/releases/v1.6.0-destination-genesis-mainnet/opsOwners.toml similarity index 100% rename from script/releases/v1.6.0-destination-genesis/opsOwners.toml rename to script/releases/v1.6.0-destination-genesis-mainnet/opsOwners.toml diff --git a/script/releases/v1.6.0-destination-genesis/pauserOwners.toml b/script/releases/v1.6.0-destination-genesis-mainnet/pauserOwners.toml similarity index 100% rename from script/releases/v1.6.0-destination-genesis/pauserOwners.toml rename to script/releases/v1.6.0-destination-genesis-mainnet/pauserOwners.toml diff --git a/script/releases/v1.6.0-destination-genesis/upgrade.json b/script/releases/v1.6.0-destination-genesis-mainnet/upgrade.json similarity index 100% rename from script/releases/v1.6.0-destination-genesis/upgrade.json rename to script/releases/v1.6.0-destination-genesis-mainnet/upgrade.json diff --git a/script/releases/v1.6.0-destination-governance-mainnet/1-deployGovernance.s.sol b/script/releases/v1.6.0-destination-governance-mainnet/1-deployGovernance.s.sol index 2dcfe1e71c..7943a20694 100644 --- a/script/releases/v1.6.0-destination-governance-mainnet/1-deployGovernance.s.sol +++ b/script/releases/v1.6.0-destination-governance-mainnet/1-deployGovernance.s.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.12; import {EOADeployer} from "zeus-templates/templates/EOADeployer.sol"; +import {MultisigDeployLib, IMultisig} from "script/releases/MultisigDeployLib.sol"; import "../Env.sol"; import "@openzeppelin/contracts/governance/TimelockController.sol"; @@ -42,8 +43,8 @@ contract DeployGovernance is EOADeployer { runAsEOA(); // Assert that the multisigs have the proper owners - protocolCouncilMultisig and communityMultisig have the same owners & threshold - checkMultisig(Multisig(Env.protocolCouncilMultisig())); - checkMultisig(Multisig(Env.communityMultisig())); + checkMultisig(IMultisig(Env.protocolCouncilMultisig())); + checkMultisig(IMultisig(Env.communityMultisig())); // Assert that the executorMultisig has the proper configuration checkExecutorMultisig(); @@ -79,7 +80,7 @@ contract DeployGovernance is EOADeployer { address[] memory initialOwners = _getMultisigOwner(); // 1. Deploy protocolCouncilMultisig - address protocolCouncilMultisig = deployMultisig({ + address protocolCouncilMultisig = MultisigDeployLib.deployMultisig({ initialOwners: initialOwners, initialThreshold: PROTOCOL_COUNCIL_AND_COMMUNITY_THRESHOLD, salt: ++salt @@ -87,7 +88,7 @@ contract DeployGovernance is EOADeployer { zUpdate("protocolCouncilMultisig", protocolCouncilMultisig); // 2. Deploy communityMultisig - address communityMultisig = deployMultisig({ + address communityMultisig = MultisigDeployLib.deployMultisig({ initialOwners: initialOwners, initialThreshold: PROTOCOL_COUNCIL_AND_COMMUNITY_THRESHOLD, salt: ++salt @@ -102,46 +103,14 @@ contract DeployGovernance is EOADeployer { owners_executorMultisig[0] = address(Env.timelockController()); owners_executorMultisig[1] = Env.communityMultisig(); - address executorMultisig = - deployMultisig({initialOwners: owners_executorMultisig, initialThreshold: EXECUTOR_THRESHOLD, salt: ++salt}); + address executorMultisig = MultisigDeployLib.deployMultisig({ + initialOwners: owners_executorMultisig, + initialThreshold: EXECUTOR_THRESHOLD, + salt: ++salt + }); zUpdate("executorMultisig", executorMultisig); } - function deployMultisig( - address[] memory initialOwners, - uint256 initialThreshold, - uint256 salt - ) internal returns (address) { - // addresses taken from https://github.com/safe-global/safe-smart-account/blob/main/CHANGELOG.md#expected-addresses-with-deterministic-deployment-proxy-default - // NOTE: double check these addresses are correct on each chain - address safeFactory = 0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67; - address safeSingleton = 0x29fcB43b46531BcA003ddC8FCB67FFE91900C762; // Gnosis safe L2 singleton - address safeFallbackHandler = 0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99; - - bytes memory emptyData; - - bytes memory initializerData = abi.encodeWithSignature( - "setup(address[],uint256,address,bytes,address,address,uint256,address)", - initialOwners, /* signers */ - initialThreshold, /* threshold */ - address(0), /* to (used in setupModules) */ - emptyData, /* data (used in setupModules) */ - safeFallbackHandler, - address(0), /* paymentToken */ - 0, /* payment */ - payable(address(0)) /* paymentReceiver */ - ); - - bytes memory calldataToFactory = - abi.encodeWithSignature("createProxyWithNonce(address,bytes,uint256)", safeSingleton, initializerData, salt); - - (bool success, bytes memory returndata) = safeFactory.call(calldataToFactory); - require(success, "multisig deployment failed"); - address deployedMultisig = abi.decode(returndata, (address)); - require(deployedMultisig != address(0), "something wrong in multisig deployment, zero address returned"); - return deployedMultisig; - } - function configureTimelockController() public { // Get the timelock controller TimelockController timelockController = Env.timelockController(); @@ -227,7 +196,7 @@ contract DeployGovernance is EOADeployer { /// @dev Used to check the configuration of the protocolCouncilMultisig and communityMultisig function checkMultisig( - Multisig multisig + IMultisig multisig ) public view { // Check threshold assertEq(multisig.getThreshold(), PROTOCOL_COUNCIL_AND_COMMUNITY_THRESHOLD); @@ -244,18 +213,18 @@ contract DeployGovernance is EOADeployer { function checkExecutorMultisig() public view { // Check threshold - assertEq(Multisig(Env.executorMultisig()).getThreshold(), EXECUTOR_THRESHOLD); + assertEq(IMultisig(Env.executorMultisig()).getThreshold(), EXECUTOR_THRESHOLD); // Check owner count - assertEq(Multisig(Env.executorMultisig()).getOwners().length, 2, "executorMultisig owner count mismatch"); + assertEq(IMultisig(Env.executorMultisig()).getOwners().length, 2, "executorMultisig owner count mismatch"); // Check owners assertTrue( - Multisig(Env.executorMultisig()).isOwner(address(Env.timelockController())), + IMultisig(Env.executorMultisig()).isOwner(address(Env.timelockController())), "timelockController not in executorMultisig" ); assertTrue( - Multisig(Env.executorMultisig()).isOwner(Env.communityMultisig()), + IMultisig(Env.executorMultisig()).isOwner(Env.communityMultisig()), "communityMultisig not in executorMultisig" ); } @@ -328,11 +297,3 @@ contract DeployGovernance is EOADeployer { return toml.readAddressArray(".owners"); } } - -interface Multisig { - function getThreshold() external view returns (uint256); - function getOwners() external view returns (address[] memory); - function isOwner( - address owner - ) external view returns (bool); -} diff --git a/script/releases/v1.6.0-destination-governance-mainnet/2-validateOwnership.s.sol b/script/releases/v1.6.0-destination-governance-mainnet/2-validateOwnership.s.sol index 24237be067..6fd82792a5 100644 --- a/script/releases/v1.6.0-destination-governance-mainnet/2-validateOwnership.s.sol +++ b/script/releases/v1.6.0-destination-governance-mainnet/2-validateOwnership.s.sol @@ -15,7 +15,7 @@ contract ValidateOwnership is MultisigBuilder, DeployGovernance { if (!Env._strEq(Env.env(), "base")) { return; } - + ProxyAdmin proxyAdmin = ProxyAdmin(address(Env.proxyAdmin())); proxyAdmin.transferOwnership(Env.executorMultisig()); } diff --git a/script/releases/v1.6.0-destination-governance-testnet/1-deployGovernance.s.sol b/script/releases/v1.6.0-destination-governance-testnet/1-deployGovernance.s.sol index 1dd4f96ce2..5056cc8595 100644 --- a/script/releases/v1.6.0-destination-governance-testnet/1-deployGovernance.s.sol +++ b/script/releases/v1.6.0-destination-governance-testnet/1-deployGovernance.s.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.12; import {EOADeployer} from "zeus-templates/templates/EOADeployer.sol"; +import {MultisigDeployLib, IMultisig} from "script/releases/MultisigDeployLib.sol"; import "../Env.sol"; import "@openzeppelin/contracts/governance/TimelockController.sol"; @@ -39,8 +40,8 @@ contract DeployGovernance is EOADeployer { runAsEOA(); // Assert that the multisigs have the proper owners - protocolCouncilMultisig and communityMultisig have the same owners & threshold - checkMultisig(Multisig(Env.protocolCouncilMultisig())); - checkMultisig(Multisig(Env.communityMultisig())); + checkMultisig(IMultisig(Env.protocolCouncilMultisig())); + checkMultisig(IMultisig(Env.communityMultisig())); // Assert that the executorMultisig has the proper configuration checkExecutorMultisig(); @@ -77,13 +78,19 @@ contract DeployGovernance is EOADeployer { initialOwners[0] = TESTNET_OWNER; // 1. Deploy protocolCouncilMultisig - address protocolCouncilMultisig = - deployMultisig({initialOwners: initialOwners, initialThreshold: TESTNET_THRESHOLD, salt: ++salt}); + address protocolCouncilMultisig = MultisigDeployLib.deployMultisig({ + initialOwners: initialOwners, + initialThreshold: TESTNET_THRESHOLD, + salt: ++salt + }); zUpdate("protocolCouncilMultisig", protocolCouncilMultisig); // 2. Deploy communityMultisig - address communityMultisig = - deployMultisig({initialOwners: initialOwners, initialThreshold: TESTNET_THRESHOLD, salt: ++salt}); + address communityMultisig = MultisigDeployLib.deployMultisig({ + initialOwners: initialOwners, + initialThreshold: TESTNET_THRESHOLD, + salt: ++salt + }); zUpdate("communityMultisig", communityMultisig); // 3. Deploy primary executorMultisig @@ -94,46 +101,14 @@ contract DeployGovernance is EOADeployer { owners_executorMultisig[0] = address(Env.timelockController()); owners_executorMultisig[1] = Env.communityMultisig(); - address executorMultisig = - deployMultisig({initialOwners: owners_executorMultisig, initialThreshold: TESTNET_THRESHOLD, salt: ++salt}); + address executorMultisig = MultisigDeployLib.deployMultisig({ + initialOwners: owners_executorMultisig, + initialThreshold: TESTNET_THRESHOLD, + salt: ++salt + }); zUpdate("executorMultisig", executorMultisig); } - function deployMultisig( - address[] memory initialOwners, - uint256 initialThreshold, - uint256 salt - ) internal returns (address) { - // addresses taken from https://github.com/safe-global/safe-smart-account/blob/main/CHANGELOG.md#expected-addresses-with-deterministic-deployment-proxy-default - // NOTE: double check these addresses are correct on each chain - address safeFactory = 0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67; - address safeSingleton = 0x29fcB43b46531BcA003ddC8FCB67FFE91900C762; // Gnosis safe L2 singleton - address safeFallbackHandler = 0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99; - - bytes memory emptyData; - - bytes memory initializerData = abi.encodeWithSignature( - "setup(address[],uint256,address,bytes,address,address,uint256,address)", - initialOwners, /* signers */ - initialThreshold, /* threshold */ - address(0), /* to (used in setupModules) */ - emptyData, /* data (used in setupModules) */ - safeFallbackHandler, - address(0), /* paymentToken */ - 0, /* payment */ - payable(address(0)) /* paymentReceiver */ - ); - - bytes memory calldataToFactory = - abi.encodeWithSignature("createProxyWithNonce(address,bytes,uint256)", safeSingleton, initializerData, salt); - - (bool success, bytes memory returndata) = safeFactory.call(calldataToFactory); - require(success, "multisig deployment failed"); - address deployedMultisig = abi.decode(returndata, (address)); - require(deployedMultisig != address(0), "something wrong in multisig deployment, zero address returned"); - return deployedMultisig; - } - function configureTimelockController() public { // Get the timelock controller TimelockController timelockController = Env.timelockController(); @@ -219,7 +194,7 @@ contract DeployGovernance is EOADeployer { /// @dev Used to check the configuration of the protocolCouncilMultisig and communityMultisig function checkMultisig( - Multisig multisig + IMultisig multisig ) public view { // Check threshold for testnet assertEq(multisig.getThreshold(), TESTNET_THRESHOLD); @@ -238,18 +213,18 @@ contract DeployGovernance is EOADeployer { function checkExecutorMultisig() public view { // Check threshold - assertEq(Multisig(Env.executorMultisig()).getThreshold(), TESTNET_THRESHOLD); + assertEq(IMultisig(Env.executorMultisig()).getThreshold(), TESTNET_THRESHOLD); // Check owner count - assertEq(Multisig(Env.executorMultisig()).getOwners().length, 2, "executorMultisig owner count mismatch"); + assertEq(IMultisig(Env.executorMultisig()).getOwners().length, 2, "executorMultisig owner count mismatch"); // Check owners assertTrue( - Multisig(Env.executorMultisig()).isOwner(address(Env.timelockController())), + IMultisig(Env.executorMultisig()).isOwner(address(Env.timelockController())), "timelockController not in executorMultisig" ); assertTrue( - Multisig(Env.executorMultisig()).isOwner(Env.communityMultisig()), + IMultisig(Env.executorMultisig()).isOwner(Env.communityMultisig()), "communityMultisig not in executorMultisig" ); } @@ -311,11 +286,3 @@ contract DeployGovernance is EOADeployer { assertEq(timelockController.getMinDelay(), 1); } } - -interface Multisig { - function getThreshold() external view returns (uint256); - function getOwners() external view returns (address[] memory); - function isOwner( - address owner - ) external view returns (bool); -} diff --git a/script/releases/v1.6.0-protocol-from-scratch/1-deployGovernance.s.sol b/script/releases/v1.6.0-protocol-from-scratch/1-deployGovernance.s.sol new file mode 100644 index 0000000000..f8d4de3563 --- /dev/null +++ b/script/releases/v1.6.0-protocol-from-scratch/1-deployGovernance.s.sol @@ -0,0 +1,330 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import {EOADeployer} from "zeus-templates/templates/EOADeployer.sol"; +import {MultisigDeployLib} from "../MultisigDeployLib.sol"; +import "../Env.sol"; + +import "@openzeppelin/contracts/governance/TimelockController.sol"; + +/// @notice This script is used to deploy the governance contracts on a testnet environment. +/// @dev Before running this script, please ensure that you have deployed the MultichainDeployer multisig +/// This script deploys the following contracts/msigs: +/// - TimelockController +/// - protocolCouncilMultisig +/// - communityMultisig +/// - executorMultisig +/// - operationsMultisig +contract DeployGovernance is EOADeployer { + using Env for *; + + // Testnet constants + address public constant TESTNET_OWNER = 0xDA29BB71669f46F2a779b4b62f03644A84eE3479; + uint256 public constant TESTNET_THRESHOLD = 1; + + function _runAsEOA() internal virtual override { + vm.startBroadcast(); + + deployTimelockControllers(); + deployProtocolMultisigs(); + configureTimelockController(Env.timelockController()); + configureTimelockController(Env.beigenTimelockController()); + + vm.stopBroadcast(); + } + + function testScript() public virtual { + runAsEOA(); + + // Assert that the multisigs have the proper owners - protocolCouncilMultisig, communityMultisig, and operationsMultisig have the same owners & threshold + checkMultisig(Env.protocolCouncilMultisig()); + checkMultisig(Env.communityMultisig()); + checkMultisig(Env.opsMultisig()); + + // Assert that the executorMultisig and beigenExecutorMultisig have the proper configuration + checkExecutorMultisig(); + checkBeigenExecutorMultisig(); + + // Assert that the timelock controller is configured correctly + checkTimelockControllerConfig(Env.timelockController()); + checkTimelockControllerConfig(Env.beigenTimelockController()); + } + + // set up initially with deployer as a proposer & executor, to be renounced prior to finalizing deployment + function deployTimelockControllers() public { + address[] memory proposers = new address[](1); + proposers[0] = msg.sender; + + address[] memory executors = new address[](1); + executors[0] = msg.sender; + + TimelockController timelockController = new TimelockController({ + minDelay: 0, // no delay for setup + proposers: proposers, + executors: executors, + admin: address(0) + }); + + TimelockController beigenTimelockController = new TimelockController({ + minDelay: 0, // no delay for setup + proposers: proposers, + executors: executors, + admin: address(0) + }); + + zUpdate("timelockController", address(timelockController)); + zUpdate("beigenTimelockController", address(beigenTimelockController)); + } + + /// + function deployProtocolMultisigs() public { + // pseudorandom number for salt + uint256 salt = uint256(keccak256(abi.encode(block.chainid, block.timestamp))); // Pseudo-random salt + + // Set the initial owner of the multisig to the testnet owner + address[] memory initialOwners = new address[](1); + initialOwners[0] = TESTNET_OWNER; + + // 1. Deploy protocolCouncilMultisig + address protocolCouncilMultisig = MultisigDeployLib.deployMultisig({ + initialOwners: initialOwners, + initialThreshold: TESTNET_THRESHOLD, + salt: ++salt + }); + zUpdate("protocolCouncilMultisig", protocolCouncilMultisig); + + // 2. Deploy communityMultisig + address communityMultisig = MultisigDeployLib.deployMultisig({ + initialOwners: initialOwners, + initialThreshold: TESTNET_THRESHOLD, + salt: ++salt + }); + zUpdate("communityMultisig", communityMultisig); + + // 3. Deploy primary executorMultisig + require( + address(Env.timelockController()) != address(0), "must deploy timelockController before executorMultisig" + ); + address[] memory owners_executorMultisig = new address[](2); + owners_executorMultisig[0] = address(Env.timelockController()); + owners_executorMultisig[1] = Env.communityMultisig(); + + address executorMultisig = MultisigDeployLib.deployMultisig({ + initialOwners: owners_executorMultisig, + initialThreshold: TESTNET_THRESHOLD, + salt: ++salt + }); + zUpdate("executorMultisig", executorMultisig); + + // 4. Deploy beigenExecutorMultisig + require( + address(Env.beigenTimelockController()) != address(0), + "must deploy beigenTimelockController before beigenExecutorMultisig" + ); + address[] memory owners_beigenExecutorMultisig = new address[](2); + owners_beigenExecutorMultisig[0] = address(Env.beigenTimelockController()); + owners_beigenExecutorMultisig[1] = Env.communityMultisig(); + address beigenExecutorMultisig = MultisigDeployLib.deployMultisig({ + initialOwners: owners_beigenExecutorMultisig, + initialThreshold: TESTNET_THRESHOLD, + salt: ++salt + }); + zUpdate("beigenExecutorMultisig", beigenExecutorMultisig); + + // 5. Deploy operationsMultisig + address operationsMultisig = MultisigDeployLib.deployMultisig({ + initialOwners: initialOwners, + initialThreshold: TESTNET_THRESHOLD, + salt: ++salt + }); + zUpdate("operationsMultisig", operationsMultisig); + } + + function configureTimelockController( + TimelockController timelockController + ) public { + // We have 10 actions to perform on the timelock controller + uint256 tx_array_length = 10; + address[] memory targets = new address[](tx_array_length); + for (uint256 i = 0; i < targets.length; ++i) { + targets[i] = address(timelockController); + } + uint256[] memory values = new uint256[](tx_array_length); + bytes[] memory payloads = new bytes[](tx_array_length); + + // 1. remove sender as canceller + payloads[0] = + abi.encodeWithSelector(AccessControl.revokeRole.selector, timelockController.CANCELLER_ROLE(), msg.sender); + // 2. remove sender as executor + payloads[1] = + abi.encodeWithSelector(AccessControl.revokeRole.selector, timelockController.EXECUTOR_ROLE(), msg.sender); + // 3. remove sender as proposer + payloads[2] = + abi.encodeWithSelector(AccessControl.revokeRole.selector, timelockController.PROPOSER_ROLE(), msg.sender); + // 4. remove sender as admin + payloads[3] = abi.encodeWithSelector( + AccessControl.revokeRole.selector, timelockController.TIMELOCK_ADMIN_ROLE(), msg.sender + ); + + // 5. add operationsMultisig as canceller + payloads[4] = abi.encodeWithSelector( + AccessControl.grantRole.selector, timelockController.CANCELLER_ROLE(), Env.opsMultisig() + ); + // 6. add operationsMultisig as proposer + payloads[5] = abi.encodeWithSelector( + AccessControl.grantRole.selector, timelockController.PROPOSER_ROLE(), Env.opsMultisig() + ); + + // 7. add protocolCouncilMultisig as proposer + payloads[6] = abi.encodeWithSelector( + AccessControl.grantRole.selector, timelockController.PROPOSER_ROLE(), Env.protocolCouncilMultisig() + ); + // 8. add protocolCouncilMultisig as executor + payloads[7] = abi.encodeWithSelector( + AccessControl.grantRole.selector, timelockController.EXECUTOR_ROLE(), Env.protocolCouncilMultisig() + ); + + // 9. add communityMultisig as admin + payloads[8] = abi.encodeWithSelector( + AccessControl.grantRole.selector, timelockController.TIMELOCK_ADMIN_ROLE(), Env.communityMultisig() + ); + + // For testnet, the delay is 1 second + uint256 delayToSet = 1; + + require(delayToSet != 0, "delay not calculated"); + // 10. set min delay to appropriate length + payloads[9] = abi.encodeWithSelector(timelockController.updateDelay.selector, delayToSet); + + // schedule the batch + timelockController.scheduleBatch( + targets, + values, + payloads, + bytes32(0), // no predecessor needed + bytes32(0), // no salt + 0 // 0 enforced delay + ); + + // execute the batch + timelockController.executeBatch( + targets, + values, + payloads, + bytes32(0), // no predecessor needed + bytes32(0) // no salt + ); + } + + /** + * + * CHECKING FUNCTIONS + * + */ + + /// @dev Used to check the configuration of the protocolCouncilMultisig and communityMultisig + function checkMultisig( + address multisig + ) public view { + // Check threshold for testnet + assertEq(MultisigDeployLib.getThreshold(multisig), TESTNET_THRESHOLD); + + // Check owners + address[] memory expectedOwners = new address[](1); + expectedOwners[0] = TESTNET_OWNER; + + for (uint256 i = 0; i < expectedOwners.length; i++) { + assertTrue(MultisigDeployLib.isOwner(multisig, expectedOwners[i]), "owner mismatch"); + } + + // Assert that the owner counts are correct + assertEq(expectedOwners.length, MultisigDeployLib.getOwners(multisig).length, "owner count mismatch"); + } + + function checkExecutorMultisig() public view { + // Check threshold + assertEq(MultisigDeployLib.getThreshold(Env.executorMultisig()), TESTNET_THRESHOLD); + + // Check owner count + assertEq(MultisigDeployLib.getOwners(Env.executorMultisig()).length, 2, "executorMultisig owner count mismatch"); + + // Check owners + assertTrue( + MultisigDeployLib.isOwner(Env.executorMultisig(), address(Env.timelockController())), + "timelockController not in executorMultisig" + ); + assertTrue( + MultisigDeployLib.isOwner(Env.executorMultisig(), Env.communityMultisig()), + "communityMultisig not in executorMultisig" + ); + } + + function checkBeigenExecutorMultisig() public view { + // Check threshold + assertEq(MultisigDeployLib.getThreshold(Env.beigenExecutorMultisig()), TESTNET_THRESHOLD); + + // Check owner count + assertEq( + MultisigDeployLib.getOwners(Env.beigenExecutorMultisig()).length, + 2, + "beigenExecutorMultisig owner count mismatch" + ); + + // Check owners + assertTrue( + MultisigDeployLib.isOwner(Env.beigenExecutorMultisig(), address(Env.beigenTimelockController())), + "timelockController not in beigenExecutorMultisig" + ); + assertTrue( + MultisigDeployLib.isOwner(Env.beigenExecutorMultisig(), Env.communityMultisig()), + "communityMultisig not in beigenExecutorMultisig" + ); + } + + function checkTimelockControllerConfig( + TimelockController timelockController + ) public view { + // check for proposer + executor rights on Protocol Council multisig + require( + timelockController.hasRole(timelockController.PROPOSER_ROLE(), Env.protocolCouncilMultisig()), + "protocolCouncilMultisig does not have PROPOSER_ROLE on timelockController" + ); + require( + timelockController.hasRole(timelockController.CANCELLER_ROLE(), Env.opsMultisig()), + "operationsMultisig does not have CANCELLER_ROLE on timelockController" + ); + + // check that community multisig has admin rights + require( + timelockController.hasRole(timelockController.TIMELOCK_ADMIN_ROLE(), Env.communityMultisig()), + "communityMultisig does not have TIMELOCK_ADMIN_ROLE on timelockController" + ); + + // check for self-administration + require( + timelockController.hasRole(timelockController.TIMELOCK_ADMIN_ROLE(), address(timelockController)), + "timelockController does not have TIMELOCK_ADMIN_ROLE on itself" + ); + + // check that deployer has no rights + require( + !timelockController.hasRole(timelockController.TIMELOCK_ADMIN_ROLE(), msg.sender), + "deployer erroenously retains TIMELOCK_ADMIN_ROLE on timelockController" + ); + require( + !timelockController.hasRole(timelockController.PROPOSER_ROLE(), msg.sender), + "deployer erroenously retains PROPOSER_ROLE on timelockController" + ); + require( + !timelockController.hasRole(timelockController.EXECUTOR_ROLE(), msg.sender), + "deployer erroenously retains EXECUTOR_ROLE on timelockController" + ); + require( + !timelockController.hasRole(timelockController.CANCELLER_ROLE(), msg.sender), + "deployer erroenously retains CANCELLER_ROLE on timelockController" + ); + + // Check the delay for testnet (1 second) + assertEq(timelockController.getMinDelay(), 1); + } +} diff --git a/script/releases/v1.6.0-protocol-from-scratch/2-deployPauser.s.sol b/script/releases/v1.6.0-protocol-from-scratch/2-deployPauser.s.sol new file mode 100644 index 0000000000..fbc7e03b3c --- /dev/null +++ b/script/releases/v1.6.0-protocol-from-scratch/2-deployPauser.s.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import {DeployGovernance} from "./1-deployGovernance.s.sol"; +import {MultisigDeployLib} from "../MultisigDeployLib.sol"; +import "../Env.sol"; + +/// @dev This script is used to deploy the operations contracts on a testnet environment. +/// This script deploys the following contracts/msigs: +/// - pauserMultisig +/// - pauserRegistry +/// - proxyAdmin +contract DeployPauser is DeployGovernance { + using Env for *; + + function _runAsEOA() internal virtual override { + // Setup safe deploy parameters + uint256 salt = uint256(keccak256(abi.encode(block.chainid, block.timestamp + 1))); // Pseudo-random salt; We add 1 to the timestamp to ensure the salt is different from the governance script + address[] memory initialOwners = new address[](1); + initialOwners[0] = TESTNET_OWNER; + + vm.startBroadcast(); + + // Deploy pauserMultisig + address pauserMultisig = MultisigDeployLib.deployMultisig({ + initialOwners: initialOwners, + initialThreshold: TESTNET_THRESHOLD, + salt: ++salt + }); + + // Deploy pauserRegistry + address[] memory pausers = new address[](2); + pausers[0] = Env.opsMultisig(); + pausers[0] = Env.executorMultisig(); + pausers[1] = pauserMultisig; + + deployImpl({ + name: type(PauserRegistry).name, + deployedTo: address(new PauserRegistry({_pausers: pausers, _unpauser: Env.executorMultisig()})) + }); + + vm.stopBroadcast(); + + // Update config + zUpdate("pauserMultisig", pauserMultisig); + } + + function testScript() public virtual override { + // Run the deploy governance script, since we need the executor multisig to be deployed + DeployGovernance._runAsEOA(); + + // Run the deploy operations script + runAsEOA(); + + // Check the pauser multisig + assertNotEq(Env.pauserMultisig(), address(0)); + assertEq(MultisigDeployLib.getThreshold(Env.pauserMultisig()), TESTNET_THRESHOLD); + + // Assert the owners of each multisig + address[] memory expectedOwners = new address[](1); + expectedOwners[0] = TESTNET_OWNER; + for (uint256 i = 0; i < expectedOwners.length; i++) { + assertTrue(MultisigDeployLib.isOwner(Env.pauserMultisig(), expectedOwners[i])); + } + assertEq( + expectedOwners.length, + MultisigDeployLib.getOwners(Env.pauserMultisig()).length, + "pauserMultisigOwners length mismatch" + ); + + // Assert that the pauserRegistry is non-zero, and that the pausers are set correctly + assertTrue(Env.impl.pauserRegistry().isPauser(Env.opsMultisig())); + assertTrue(Env.impl.pauserRegistry().isPauser(Env.pauserMultisig())); + assertTrue(Env.impl.pauserRegistry().isPauser(Env.executorMultisig())); + assertEq(Env.impl.pauserRegistry().unpauser(), Env.executorMultisig()); + } +} diff --git a/script/releases/v1.6.0-protocol-from-scratch/3-deployToken.s.sol b/script/releases/v1.6.0-protocol-from-scratch/3-deployToken.s.sol new file mode 100644 index 0000000000..6053e61213 --- /dev/null +++ b/script/releases/v1.6.0-protocol-from-scratch/3-deployToken.s.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import {DeployGovernance} from "./1-deployGovernance.s.sol"; +import {DeployPauser} from "./2-deployPauser.s.sol"; +import "../Env.sol"; + +/// @dev This script is used to deploy the token contracts on a testnet environment. +contract DeployToken is DeployPauser { + using Env for *; + + function _runAsEOA() internal virtual override { + vm.startBroadcast(); + + // 0. Deploy the empty contract + address emptyContract = address(new EmptyContract()); + + // 1. Deploy the bEIGEN and normal proxy admins + ProxyAdmin beigenProxyAdmin = new ProxyAdmin(); + ProxyAdmin proxyAdmin = new ProxyAdmin(); + zUpdate("beigenProxyAdmin", address(beigenProxyAdmin)); + zUpdate("proxyAdmin", address(proxyAdmin)); + + // 2. Deploy the bEIGEN and Eigen proxies + deployProxy({ + name: type(BackingEigen).name, + deployedTo: address( + new TransparentUpgradeableProxy({ + _logic: address(emptyContract), + admin_: address(beigenProxyAdmin), + _data: "" + }) + ) + }); + + deployProxy({ + name: type(Eigen).name, + deployedTo: address( + new TransparentUpgradeableProxy({_logic: address(emptyContract), admin_: address(proxyAdmin), _data: ""}) + ) + }); + + // 3. Deploy the bEIGEN and Eigen implementations + deployImpl({ + name: type(BackingEigen).name, + deployedTo: address(new BackingEigen({_EIGEN: IERC20(address(Env.proxy.eigen()))})) + }); + deployImpl({ + name: type(Eigen).name, + deployedTo: address(new Eigen({_bEIGEN: IERC20(address(Env.proxy.beigen())), _version: Env.deployVersion()})) + }); + + // 4. Upgrade the EIGEN Proxy to point to the implementation + address initialOwner = msg.sender; + address[] memory minters; + uint256[] memory mintingAllowances; + uint256[] memory mintAllowedAfters; + proxyAdmin.upgradeAndCall({ + proxy: ITransparentUpgradeableProxy(address(Env.proxy.eigen())), + implementation: address(Env.impl.eigen()), + data: abi.encodeWithSelector( + Eigen.initialize.selector, initialOwner, minters, mintingAllowances, mintAllowedAfters + ) + }); + + // Disable transfer restrictions and transfer ownership + Env.proxy.eigen().disableTransferRestrictions(); + Env.proxy.eigen().transferOwnership(Env.executorMultisig()); + + // 5. Upgrade the bEIGEN Proxy to point to the implementation + beigenProxyAdmin.upgradeAndCall({ + proxy: ITransparentUpgradeableProxy(address(Env.proxy.beigen())), + implementation: address(Env.impl.beigen()), + data: abi.encodeWithSelector(BackingEigen.initialize.selector, initialOwner) + }); + + // Set minter, disable transfer restrictions and transfer ownership + Env.proxy.beigen().setIsMinter({minterAddress: TESTNET_OWNER, newStatus: true}); + Env.proxy.beigen().disableTransferRestrictions(); + Env.proxy.beigen().transferOwnership(Env.executorMultisig()); + + // 6. Transfer ownership of the proxy admins to the executor multisig + proxyAdmin.transferOwnership(Env.executorMultisig()); + beigenProxyAdmin.transferOwnership(Env.beigenExecutorMultisig()); + + vm.stopBroadcast(); + } + + function testScript() public virtual override { + // Deploy older contracts. We have to manually set the EOA mode so we don't revert + _mode = OperationalMode.EOA; + DeployGovernance._runAsEOA(); + DeployPauser._runAsEOA(); + + // Run the deploy token script + runAsEOA(); + Eigen eigen = Env.proxy.eigen(); + BackingEigen beigen = Env.proxy.beigen(); + + // Check proxyAdmin owner + assertEq(ProxyAdmin(address(Env.proxyAdmin())).owner(), Env.executorMultisig()); + assertEq(ProxyAdmin(address(Env.beigenProxyAdmin())).owner(), Env.beigenExecutorMultisig()); + + // Check the proxy impl and proxy admin for EIGEn + assertEq(Env._getProxyImpl(address(eigen)), address(Env.impl.eigen())); + assertEq(Env._getProxyAdmin(address(eigen)), address(Env.proxyAdmin())); + + // Check the proxy impl and proxy admin for bEIGEN - we can't use the Env helpers here because the proxy admin is a different contract + ProxyAdmin beigenProxyAdmin = ProxyAdmin(Env.beigenProxyAdmin()); + assertEq( + beigenProxyAdmin.getProxyImplementation(ITransparentUpgradeableProxy(address(beigen))), + address(Env.impl.beigen()) + ); + assertEq( + beigenProxyAdmin.getProxyAdmin(ITransparentUpgradeableProxy(address(beigen))), address(beigenProxyAdmin) + ); + + // Assert that transfer restrictions are disabled + assertEq(beigen.transferRestrictionsDisabledAfter(), 0); + assertEq(eigen.transferRestrictionsDisabledAfter(), 0); + + // Assert that the owner of the contracts is the executor multisig + assertEq(Ownable(address(beigen)).owner(), Env.executorMultisig()); + assertEq(Ownable(address(eigen)).owner(), Env.executorMultisig()); + + // Assert that the minter of bEIGEN is the testnet owner + assertEq(beigen.isMinter(TESTNET_OWNER), true); + + // Check EIGEN immutables + storage + assertEq(address(eigen.bEIGEN()), address(beigen)); + assertTrue(Env._strEq(Env.proxy.eigen().version(), Env.deployVersion())); + + // Check bEIGEN immutables + storage + assertEq(address(beigen.EIGEN()), address(eigen)); + } +} diff --git a/script/releases/v1.6.0-protocol-from-scratch/4-deployCore.s.sol b/script/releases/v1.6.0-protocol-from-scratch/4-deployCore.s.sol new file mode 100644 index 0000000000..41716f67d0 --- /dev/null +++ b/script/releases/v1.6.0-protocol-from-scratch/4-deployCore.s.sol @@ -0,0 +1,482 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import {DeployToken} from "./3-deployToken.s.sol"; +import {DeployPauser} from "./2-deployPauser.s.sol"; +import {DeployGovernance} from "./1-deployGovernance.s.sol"; +import "../Env.sol"; + +/// This script deploys the following contracts/proxies: +/// - DelegationManager +/// - StrategyManager +/// - AllocationManager +/// - RewardsCoordinator +/// - AVSDirectory +/// - EigenPodManager +/// - StrategyFactory +/// - ReleaseManager +/// - PermissionController +/// - KeyRegistrar +/// - EigenStrategy +/// - EigenPod - Implementation & Beacon +/// - StrategyBase - Implementation & Beacon +contract DeployCore is DeployToken { + using Env for *; + + EmptyContract emptyContract; + + function deployBlankProxy( + string memory name + ) private returns (address) { + return deployProxy({ + name: name, + deployedTo: address(new TransparentUpgradeableProxy(address(emptyContract), Env.proxyAdmin(), "")) + }); + } + + function _runAsEOA() internal virtual override { + vm.startBroadcast(); + + // 0. Deploy the empty contract + emptyContract = new EmptyContract(); + + // 1. Deploy empty proxies for the core contracts + deployBlankProxy({name: type(DelegationManager).name}); + + deployBlankProxy({name: type(StrategyManager).name}); + + deployBlankProxy({name: type(AllocationManager).name}); + + deployBlankProxy({name: type(RewardsCoordinator).name}); + + deployBlankProxy({name: type(AVSDirectory).name}); + + deployBlankProxy({name: type(EigenPodManager).name}); + + deployBlankProxy({name: type(StrategyFactory).name}); + + deployBlankProxy({name: type(EigenStrategy).name}); + + deployBlankProxy({name: type(PermissionController).name}); + + deployBlankProxy({name: type(KeyRegistrar).name}); + + deployBlankProxy({name: type(ReleaseManager).name}); + + // 2. Deploy the beacons and their associated implementations + deployImpl({ + name: type(EigenPod).name, + deployedTo: address(new EigenPod(Env.ethPOS(), Env.proxy.eigenPodManager(), Env.deployVersion())) + }); + + deployBeacon({ + name: type(EigenPod).name, + deployedTo: address(new UpgradeableBeacon(address(Env.impl.eigenPod()))) + }); + + deployImpl({ + name: type(StrategyBase).name, + deployedTo: address( + new StrategyBase({ + _strategyManager: Env.proxy.strategyManager(), + _pauserRegistry: Env.impl.pauserRegistry(), + _version: Env.deployVersion() + }) + ) + }); + + deployBeacon({ + name: type(StrategyBase).name, + deployedTo: address(new UpgradeableBeacon(address(Env.impl.strategyBase()))) + }); + + // 3. Deploy the non-beacon implementation contracts + deployImpl({ + name: type(DelegationManager).name, + deployedTo: address( + new DelegationManager( + Env.proxy.strategyManager(), + Env.proxy.eigenPodManager(), + Env.proxy.allocationManager(), + Env.impl.pauserRegistry(), + Env.proxy.permissionController(), + Env.MIN_WITHDRAWAL_DELAY(), + Env.deployVersion() + ) + ) + }); + + deployImpl({ + name: type(StrategyManager).name, + deployedTo: address( + new StrategyManager( + Env.proxy.allocationManager(), + Env.proxy.delegationManager(), + Env.impl.pauserRegistry(), + Env.deployVersion() + ) + ) + }); + + deployImpl({ + name: type(AllocationManager).name, + deployedTo: address( + new AllocationManager( + Env.proxy.delegationManager(), + Env.proxy.eigenStrategy(), + Env.impl.pauserRegistry(), + Env.proxy.permissionController(), + Env.MIN_WITHDRAWAL_DELAY(), + Env.ALLOCATION_CONFIGURATION_DELAY(), + Env.deployVersion() + ) + ) + }); + + deployImpl({ + name: type(RewardsCoordinator).name, + deployedTo: address( + new RewardsCoordinator( + IRewardsCoordinatorTypes.RewardsCoordinatorConstructorParams({ + delegationManager: Env.proxy.delegationManager(), + strategyManager: Env.proxy.strategyManager(), + allocationManager: Env.proxy.allocationManager(), + pauserRegistry: Env.impl.pauserRegistry(), + permissionController: Env.proxy.permissionController(), + CALCULATION_INTERVAL_SECONDS: Env.CALCULATION_INTERVAL_SECONDS(), + MAX_REWARDS_DURATION: Env.MAX_REWARDS_DURATION(), + MAX_RETROACTIVE_LENGTH: Env.MAX_RETROACTIVE_LENGTH(), + MAX_FUTURE_LENGTH: Env.MAX_FUTURE_LENGTH(), + GENESIS_REWARDS_TIMESTAMP: Env.GENESIS_REWARDS_TIMESTAMP(), + version: Env.deployVersion() + }) + ) + ) + }); + + deployImpl({ + name: type(AVSDirectory).name, + deployedTo: address( + new AVSDirectory(Env.proxy.delegationManager(), Env.impl.pauserRegistry(), Env.deployVersion()) + ) + }); + + deployImpl({ + name: type(EigenPodManager).name, + deployedTo: address( + new EigenPodManager( + Env.ethPOS(), + Env.beacon.eigenPod(), + Env.proxy.delegationManager(), + Env.impl.pauserRegistry(), + Env.deployVersion() + ) + ) + }); + + deployImpl({ + name: type(StrategyFactory).name, + deployedTo: address( + new StrategyFactory(Env.proxy.strategyManager(), Env.impl.pauserRegistry(), Env.deployVersion()) + ) + }); + + deployImpl({ + name: type(EigenStrategy).name, + deployedTo: address( + new EigenStrategy({ + _strategyManager: Env.proxy.strategyManager(), + _pauserRegistry: Env.impl.pauserRegistry(), + _version: Env.deployVersion() + }) + ) + }); + + deployImpl({ + name: type(PermissionController).name, + deployedTo: address(new PermissionController(Env.deployVersion())) + }); + + deployImpl({ + name: type(KeyRegistrar).name, + deployedTo: address( + new KeyRegistrar(Env.proxy.permissionController(), Env.proxy.allocationManager(), Env.deployVersion()) + ) + }); + + deployImpl({ + name: type(ReleaseManager).name, + deployedTo: address(new ReleaseManager(Env.proxy.permissionController(), Env.deployVersion())) + }); + + // 4. Transfer ownership of beacons to the executor multisig + Env.beacon.eigenPod().transferOwnership(Env.executorMultisig()); + Env.beacon.strategyBase().transferOwnership(Env.executorMultisig()); + + vm.stopBroadcast(); + } + + function testScript() public virtual override { + // Deploy older contracts. We have to manually set the EOA mode so we don't revert + _mode = OperationalMode.EOA; + DeployGovernance._runAsEOA(); + DeployPauser._runAsEOA(); + DeployToken._runAsEOA(); + + // Run the deploy proxies script + runAsEOA(); + + _validateProxyAdmins(); + _validateImplConstructors(); + _validateImplsInitialized(); + _validateBeacons(); + _validateVersion(); + } + + /// @dev Ensure each deployed TUP/beacon is owned by the proxyAdmin/executorMultisig + function _validateProxyAdmins() internal view { + address pa = Env.proxyAdmin(); + + assertTrue( + Env._getProxyAdmin(address(Env.proxy.delegationManager())) == pa, "delegationManager proxyAdmin incorrect" + ); + + assertTrue( + Env._getProxyAdmin(address(Env.proxy.strategyManager())) == pa, "strategyManager proxyAdmin incorrect" + ); + + assertTrue( + Env._getProxyAdmin(address(Env.proxy.allocationManager())) == pa, "allocationManager proxyAdmin incorrect" + ); + + assertTrue( + Env._getProxyAdmin(address(Env.proxy.rewardsCoordinator())) == pa, "rewardsCoordinator proxyAdmin incorrect" + ); + + assertTrue(Env._getProxyAdmin(address(Env.proxy.avsDirectory())) == pa, "avsDirectory proxyAdmin incorrect"); + + assertTrue(Env._getProxyAdmin(address(Env.proxy.releaseManager())) == pa, "releaseManager proxyAdmin incorrect"); + + /// permissions/ + assertTrue( + Env._getProxyAdmin(address(Env.proxy.permissionController())) == pa, + "permissionController proxyAdmin incorrect" + ); + assertTrue(Env._getProxyAdmin(address(Env.proxy.keyRegistrar())) == pa, "keyRegistrar proxyAdmin incorrect"); + + /// strategies/ + assertTrue(Env._getProxyAdmin(address(Env.proxy.eigenStrategy())) == pa, "eigenStrategy proxyAdmin incorrect"); + + assertTrue(Env.beacon.strategyBase().owner() == Env.executorMultisig(), "strategyBase beacon owner incorrect"); + + assertTrue( + Env._getProxyAdmin(address(Env.proxy.strategyFactory())) == pa, "strategyFactory proxyAdmin incorrect" + ); + + /// pods/ + assertTrue( + Env._getProxyAdmin(address(Env.proxy.eigenPodManager())) == pa, "eigenPodManager proxyAdmin incorrect" + ); + + assertTrue(Env.beacon.eigenPod().owner() == Env.executorMultisig(), "eigenPod beacon owner incorrect"); + } + + /// @dev Validate the immutables set in the new implementation constructors + function _validateImplConstructors() internal view { + { + /// core/ + + AllocationManager allocationManager = Env.impl.allocationManager(); + assertTrue(allocationManager.delegation() == Env.proxy.delegationManager(), "alm.dm invalid"); + assertTrue(allocationManager.pauserRegistry() == Env.impl.pauserRegistry(), "alm.pR invalid"); + assertTrue(allocationManager.permissionController() == Env.proxy.permissionController(), "alm.pc invalid"); + assertTrue(allocationManager.DEALLOCATION_DELAY() == Env.MIN_WITHDRAWAL_DELAY(), "alm.deallocDelay invalid"); + assertTrue( + allocationManager.ALLOCATION_CONFIGURATION_DELAY() == Env.ALLOCATION_CONFIGURATION_DELAY(), + "alm.configDelay invalid" + ); + + AVSDirectory avsDirectory = Env.impl.avsDirectory(); + assertTrue(avsDirectory.delegation() == Env.proxy.delegationManager(), "avsD.dm invalid"); + assertTrue(avsDirectory.pauserRegistry() == Env.impl.pauserRegistry(), "avsD.pR invalid"); + + DelegationManager delegation = Env.impl.delegationManager(); + assertTrue(delegation.strategyManager() == Env.proxy.strategyManager(), "dm.sm invalid"); + assertTrue(delegation.eigenPodManager() == Env.proxy.eigenPodManager(), "dm.epm invalid"); + assertTrue(delegation.allocationManager() == Env.proxy.allocationManager(), "dm.alm invalid"); + assertTrue(delegation.pauserRegistry() == Env.impl.pauserRegistry(), "dm.pR invalid"); + assertTrue(delegation.permissionController() == Env.proxy.permissionController(), "dm.pc invalid"); + assertTrue( + delegation.minWithdrawalDelayBlocks() == Env.MIN_WITHDRAWAL_DELAY(), "dm.withdrawalDelay invalid" + ); + + RewardsCoordinator rewards = Env.impl.rewardsCoordinator(); + assertTrue(rewards.delegationManager() == Env.proxy.delegationManager(), "rc.dm invalid"); + assertTrue(rewards.strategyManager() == Env.proxy.strategyManager(), "rc.sm invalid"); + assertTrue(rewards.allocationManager() == Env.proxy.allocationManager(), "rc.alm invalid"); + assertTrue(rewards.pauserRegistry() == Env.impl.pauserRegistry(), "rc.pR invalid"); + assertTrue(rewards.permissionController() == Env.proxy.permissionController(), "rc.pc invalid"); + assertTrue( + rewards.CALCULATION_INTERVAL_SECONDS() == Env.CALCULATION_INTERVAL_SECONDS(), "rc.calcInterval invalid" + ); + assertTrue(rewards.MAX_REWARDS_DURATION() == Env.MAX_REWARDS_DURATION(), "rc.rewardsDuration invalid"); + assertTrue(rewards.MAX_RETROACTIVE_LENGTH() == Env.MAX_RETROACTIVE_LENGTH(), "rc.retroLength invalid"); + assertTrue(rewards.MAX_FUTURE_LENGTH() == Env.MAX_FUTURE_LENGTH(), "rc.futureLength invalid"); + assertTrue(rewards.GENESIS_REWARDS_TIMESTAMP() == Env.GENESIS_REWARDS_TIMESTAMP(), "rc.genesis invalid"); + + StrategyManager strategyManager = Env.impl.strategyManager(); + assertTrue(strategyManager.delegation() == Env.proxy.delegationManager(), "sm.dm invalid"); + assertTrue(strategyManager.pauserRegistry() == Env.impl.pauserRegistry(), "sm.pR invalid"); + + ReleaseManager releaseManager = Env.impl.releaseManager(); + assertTrue(releaseManager.permissionController() == Env.proxy.permissionController(), "rm.pc invalid"); + } + + { + // PermissionController has no constructor + + /// permissions/ + KeyRegistrar keyRegistrar = Env.impl.keyRegistrar(); + assertTrue(keyRegistrar.permissionController() == Env.proxy.permissionController(), "kr.pc invalid"); + assertTrue(keyRegistrar.allocationManager() == Env.proxy.allocationManager(), "kr.alm invalid"); + } + + { + /// pods/ + EigenPod eigenPod = Env.impl.eigenPod(); + assertTrue(eigenPod.ethPOS() == Env.ethPOS(), "ep.ethPOS invalid"); + assertTrue(eigenPod.eigenPodManager() == Env.proxy.eigenPodManager(), "ep.epm invalid"); + + EigenPodManager eigenPodManager = Env.impl.eigenPodManager(); + assertTrue(eigenPodManager.ethPOS() == Env.ethPOS(), "epm.ethPOS invalid"); + assertTrue(eigenPodManager.eigenPodBeacon() == Env.beacon.eigenPod(), "epm.epBeacon invalid"); + assertTrue(eigenPodManager.delegationManager() == Env.proxy.delegationManager(), "epm.dm invalid"); + assertTrue(eigenPodManager.pauserRegistry() == Env.impl.pauserRegistry(), "epm.pR invalid"); + } + + { + /// strategies/ + EigenStrategy eigenStrategy = Env.impl.eigenStrategy(); + assertTrue(eigenStrategy.strategyManager() == Env.proxy.strategyManager(), "eigStrat.sm invalid"); + assertTrue(eigenStrategy.pauserRegistry() == Env.impl.pauserRegistry(), "eigStrat.pR invalid"); + + StrategyBase strategyBase = Env.impl.strategyBase(); + assertTrue(strategyBase.strategyManager() == Env.proxy.strategyManager(), "stratBase.sm invalid"); + assertTrue(strategyBase.pauserRegistry() == Env.impl.pauserRegistry(), "stratBase.pR invalid"); + + StrategyFactory strategyFactory = Env.impl.strategyFactory(); + assertTrue(strategyFactory.strategyManager() == Env.proxy.strategyManager(), "sFact.sm invalid"); + assertTrue(strategyFactory.pauserRegistry() == Env.impl.pauserRegistry(), "sFact.pR invalid"); + } + } + + /// @dev Call initialize on all deployed implementations to ensure initializers are disabled + function _validateImplsInitialized() internal { + bytes memory errInit = "Initializable: contract is already initialized"; + + /// permissions/ + // PermissionController is initializable, but does not expose the `initialize` method + + { + /// core/ + + AllocationManager allocationManager = Env.impl.allocationManager(); + vm.expectRevert(errInit); + allocationManager.initialize(0); + + AVSDirectory avsDirectory = Env.impl.avsDirectory(); + vm.expectRevert(errInit); + avsDirectory.initialize(address(0), 0); + + DelegationManager delegation = Env.impl.delegationManager(); + vm.expectRevert(errInit); + delegation.initialize(0); + + RewardsCoordinator rewards = Env.impl.rewardsCoordinator(); + vm.expectRevert(errInit); + rewards.initialize(address(0), 0, address(0), 0, 0); + + StrategyManager strategyManager = Env.impl.strategyManager(); + vm.expectRevert(errInit); + strategyManager.initialize(address(0), address(0), 0); + + // ReleaseManager is not initializable + } + + // KeyRegistrar and PermissionController are not initializable + + { + /// pods/ + EigenPod eigenPod = Env.impl.eigenPod(); + vm.expectRevert(errInit); + eigenPod.initialize(address(0)); + + EigenPodManager eigenPodManager = Env.impl.eigenPodManager(); + vm.expectRevert(errInit); + eigenPodManager.initialize(address(0), 0); + } + + { + /// strategies/ + EigenStrategy eigenStrategy = Env.impl.eigenStrategy(); + vm.expectRevert(errInit); + eigenStrategy.initialize(IEigen(address(0)), IBackingEigen(address(0))); + + StrategyBase strategyBase = Env.impl.strategyBase(); + vm.expectRevert(errInit); + strategyBase.initialize(IERC20(address(0))); + + StrategyFactory strategyFactory = Env.impl.strategyFactory(); + vm.expectRevert(errInit); + strategyFactory.initialize(address(0), 0, UpgradeableBeacon(address(0))); + } + } + + /// @dev Validate that the beacons are set to the correct implementation contracts + function _validateBeacons() internal view { + assertEq( + address(Env.beacon.eigenPod().implementation()), + address(Env.impl.eigenPod()), + "eigenPod beacon implementation incorrect" + ); + assertEq( + address(Env.beacon.strategyBase().implementation()), + address(Env.impl.strategyBase()), + "strategyBase beacon implementation incorrect" + ); + } + + function _validateVersion() internal view { + // On future upgrades, just tick the major/minor/patch to validate + string memory expected = Env.deployVersion(); + + { + /// core/ + assertEq(Env.impl.allocationManager().version(), expected, "allocationManager version mismatch"); + assertEq(Env.impl.avsDirectory().version(), expected, "avsDirectory version mismatch"); + assertEq(Env.impl.delegationManager().version(), expected, "delegationManager version mismatch"); + assertEq(Env.impl.rewardsCoordinator().version(), expected, "rewardsCoordinator version mismatch"); + assertEq(Env.impl.strategyManager().version(), expected, "strategyManager version mismatch"); + assertEq(Env.impl.releaseManager().version(), expected, "releaseManager version mismatch"); + } + + { + /// permissions/ + assertEq(Env.impl.permissionController().version(), expected, "permissionController version mismatch"); + assertEq(Env.impl.keyRegistrar().version(), expected, "keyRegistrar version mismatch"); + } + + { + /// pods/ + assertEq(Env.impl.eigenPod().version(), expected, "eigenPod version mismatch"); + assertEq(Env.impl.eigenPodManager().version(), expected, "eigenPodManager version mismatch"); + } + + { + /// strategies/ + assertEq(Env.impl.eigenStrategy().version(), expected, "eigenStrategy version mismatch"); + assertEq(Env.impl.strategyBase().version(), expected, "strategyBase version mismatch"); + assertEq(Env.impl.strategyFactory().version(), expected, "strategyFactory version mismatch"); + } + } +} diff --git a/script/releases/v1.6.0-protocol-from-scratch/5-queueUpgrade.s.sol b/script/releases/v1.6.0-protocol-from-scratch/5-queueUpgrade.s.sol new file mode 100644 index 0000000000..0a33d5b2c8 --- /dev/null +++ b/script/releases/v1.6.0-protocol-from-scratch/5-queueUpgrade.s.sol @@ -0,0 +1,202 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import {MultisigBuilder} from "zeus-templates/templates/MultisigBuilder.sol"; +import {DeployToken} from "./3-deployToken.s.sol"; +import {DeployPauser} from "./2-deployPauser.s.sol"; +import {DeployGovernance} from "./1-deployGovernance.s.sol"; +import {DeployCore} from "./4-deployCore.s.sol"; +import "../Env.sol"; +import {Encode, MultisigCall} from "zeus-templates/utils/Encode.sol"; + +/// @notice Queues an upgrade for the previously deployed empty proxies. +/// @dev The Beacons have already been set to the correct implementation contracts in step 4 of the upgrade script. +/// Upgrades the following contracts: +/// - DelegationManager +/// - StrategyManager +/// - AllocationManager +/// - RewardsCoordinator +/// - AVSDirectory +/// - EigenPodManager +/// - StrategyFactory +/// - ReleaseManager +/// - PermissionController +/// - KeyRegistrar +/// - EigenStrategy +contract Queue is MultisigBuilder, DeployCore { + using Env for *; + using Encode for *; + + ProxyAdmin public proxyAdmin; + + function _runAsMultisig() internal virtual override prank(Env.opsMultisig()) { + proxyAdmin = ProxyAdmin(address(Env.proxyAdmin())); + bytes memory calldata_to_executor = _getCalldataToExecutor(); + + TimelockController timelock = Env.timelockController(); + timelock.schedule({ + target: Env.executorMultisig(), + value: 0, + data: calldata_to_executor, + predecessor: 0, + salt: 0, + delay: timelock.getMinDelay() + }); + } + + function _getCalldataToExecutor() internal returns (bytes memory) { + MultisigCall[] storage executorCalls = Encode.newMultisigCalls().append({ + to: Env.proxyAdmin(), + data: abi.encodeCall( + proxyAdmin.upgradeAndCall, + ( + ITransparentUpgradeableProxy(payable(address(Env.proxy.delegationManager()))), + address(Env.impl.delegationManager()), + abi.encodeCall(DelegationManager.initialize, (0)) + ) + ) + }).append({ + to: Env.proxyAdmin(), + data: abi.encodeCall( + proxyAdmin.upgradeAndCall, + ( + ITransparentUpgradeableProxy(payable(address(Env.proxy.strategyManager()))), + address(Env.impl.strategyManager()), + abi.encodeCall( + StrategyManager.initialize, + (address(Env.executorMultisig()), address(Env.proxy.strategyFactory()), 0) + ) + ) + ) + }).append({ + to: Env.proxyAdmin(), + data: abi.encodeCall( + proxyAdmin.upgradeAndCall, + ( + ITransparentUpgradeableProxy(payable(address(Env.proxy.allocationManager()))), + address(Env.impl.allocationManager()), + abi.encodeCall(AllocationManager.initialize, (0)) + ) + ) + }).append({ + to: Env.proxyAdmin(), + data: abi.encodeCall( + proxyAdmin.upgradeAndCall, + ( + ITransparentUpgradeableProxy(payable(address(Env.proxy.rewardsCoordinator()))), + address(Env.impl.rewardsCoordinator()), + abi.encodeCall( + RewardsCoordinator.initialize, + ( + address(Env.opsMultisig()), + Env.REWARDS_PAUSE_STATUS(), + Env.REWARDS_UPDATER(), + Env.ACTIVATION_DELAY(), + Env.DEFAULT_SPLIT_BIPS() + ) + ) + ) + ) + }).append({ + to: Env.proxyAdmin(), + data: abi.encodeCall( + proxyAdmin.upgradeAndCall, + ( + ITransparentUpgradeableProxy(payable(address(Env.proxy.avsDirectory()))), + address(Env.impl.avsDirectory()), + abi.encodeCall(AVSDirectory.initialize, (address(Env.executorMultisig()), 0)) + ) + ) + }).append({ + to: Env.proxyAdmin(), + data: abi.encodeCall( + proxyAdmin.upgradeAndCall, + ( + ITransparentUpgradeableProxy(payable(address(Env.proxy.eigenPodManager()))), + address(Env.impl.eigenPodManager()), + abi.encodeCall(EigenPodManager.initialize, (address(Env.executorMultisig()), 0)) + ) + ) + }).append({ + to: Env.proxyAdmin(), + data: abi.encodeCall( + proxyAdmin.upgradeAndCall, + ( + ITransparentUpgradeableProxy(payable(address(Env.proxy.strategyFactory()))), + address(Env.impl.strategyFactory()), + abi.encodeCall(StrategyFactory.initialize, (address(Env.opsMultisig()), 0, Env.beacon.strategyBase())) + ) + ) + }).append({ + to: Env.proxyAdmin(), + data: Encode.proxyAdmin.upgrade({ + proxy: address(Env.proxy.releaseManager()), + impl: address(Env.impl.releaseManager()) + }) + }).append({ + to: Env.proxyAdmin(), + data: Encode.proxyAdmin.upgrade({ + proxy: address(Env.proxy.permissionController()), + impl: address(Env.impl.permissionController()) + }) + }).append({ + to: Env.proxyAdmin(), + data: Encode.proxyAdmin.upgrade({ + proxy: address(Env.proxy.keyRegistrar()), + impl: address(Env.impl.keyRegistrar()) + }) + }).append({ + to: Env.proxyAdmin(), + data: abi.encodeCall( + proxyAdmin.upgradeAndCall, + ( + ITransparentUpgradeableProxy(payable(address(Env.proxy.eigenStrategy()))), + address(Env.impl.eigenStrategy()), + abi.encodeCall( + EigenStrategy.initialize, (IEigen(address(Env.proxy.eigen())), IERC20(address(Env.proxy.beigen()))) + ) + ) + ) + }); + + // Add a call to set the proof timestamp setter timestamp to the operations multisig. This value will be set in step 10 of the upgrade. + executorCalls.append({ + to: address(Env.proxy.eigenPodManager()), + data: abi.encodeCall(EigenPodManager.setProofTimestampSetter, (address(Env.opsMultisig()))) + }); + + return Encode.gnosisSafe.execTransaction({ + from: address(Env.timelockController()), + to: address(Env.multiSendCallOnly()), + op: Encode.Operation.DelegateCall, + data: Encode.multiSend(executorCalls) + }); + } + + function testScript() public virtual override { + // Run all the previous steps of the upgrade script + _mode = OperationalMode.EOA; + DeployGovernance._runAsEOA(); + DeployPauser._runAsEOA(); + DeployToken._runAsEOA(); + DeployCore._runAsEOA(); + + TimelockController timelock = Env.timelockController(); + bytes memory calldata_to_executor = _getCalldataToExecutor(); + bytes32 txHash = timelock.hashOperation({ + target: Env.executorMultisig(), + value: 0, + data: calldata_to_executor, + predecessor: 0, + salt: 0 + }); + + // Check that the upgrade does not exist in the timelock + assertFalse(timelock.isOperationPending(txHash), "Transaction should NOT be queued."); + + execute(); + + // Check that the upgrade has been added to the timelock + assertTrue(timelock.isOperationPending(txHash), "Transaction should be queued."); + } +} diff --git a/script/releases/v1.6.0-protocol-from-scratch/6-executeUpgrade.s.sol b/script/releases/v1.6.0-protocol-from-scratch/6-executeUpgrade.s.sol new file mode 100644 index 0000000000..f178d1250f --- /dev/null +++ b/script/releases/v1.6.0-protocol-from-scratch/6-executeUpgrade.s.sol @@ -0,0 +1,317 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import {MultisigBuilder} from "zeus-templates/templates/MultisigBuilder.sol"; +import {DeployToken} from "./3-deployToken.s.sol"; +import {DeployPauser} from "./2-deployPauser.s.sol"; +import {DeployGovernance} from "./1-deployGovernance.s.sol"; +import {DeployCore} from "./4-deployCore.s.sol"; +import {Queue} from "./5-queueUpgrade.s.sol"; +import "../Env.sol"; +import {Encode, MultisigCall} from "zeus-templates/utils/Encode.sol"; + +contract Execute is Queue { + using Env for *; + + function _runAsMultisig() internal virtual override prank(Env.protocolCouncilMultisig()) { + bytes memory calldata_to_executor = _getCalldataToExecutor(); + + TimelockController timelock = Env.timelockController(); + timelock.execute({ + target: Env.executorMultisig(), + value: 0, + payload: calldata_to_executor, + predecessor: 0, + salt: 0 + }); + } + + function testScript() public virtual override { + // 0- Run all the previous steps of the upgrade + _mode = OperationalMode.EOA; + DeployGovernance._runAsEOA(); + DeployPauser._runAsEOA(); + DeployToken._runAsEOA(); + DeployCore._runAsEOA(); + + TimelockController timelock = Env.timelockController(); + bytes memory calldata_to_executor = _getCalldataToExecutor(); + bytes32 txHash = timelock.hashOperation({ + target: Env.executorMultisig(), + value: 0, + data: calldata_to_executor, + predecessor: 0, + salt: 0 + }); + + assertFalse(timelock.isOperationPending(txHash), "Transaction should NOT be queued."); + + // 1- run queueing logic + Queue._runAsMultisig(); + _unsafeResetHasPranked(); // reset hasPranked so we can use it again + + assertTrue(timelock.isOperationPending(txHash), "Transaction should be queued."); + assertFalse(timelock.isOperationReady(txHash), "Transaction should NOT be ready for execution."); + assertFalse(timelock.isOperationDone(txHash), "Transaction should NOT be complete."); + + // 2- warp past delay + vm.warp(block.timestamp + timelock.getMinDelay()); // 1 tick after ETA + assertEq(timelock.isOperationReady(txHash), true, "Transaction should be executable."); + + // 3- execute + execute(); + assertTrue(timelock.isOperationDone(txHash), "Transaction should be complete."); + + // 4- validate the upgrade was successful + _validateNewImplAddresses({areMatching: true}); + _validateProxyConstructors(); + _validateProxiesInitialized(); + } + + /// @dev Validate that the `Env.impl` addresses are updated to be distinct from what the proxy + /// admin reports as the current implementation address. + /// + /// Note: The upgrade script can call this with `areMatching == true` to check that these impl + /// addresses _are_ matches. + function _validateNewImplAddresses( + bool areMatching + ) internal view { + /// core/ -- can't check AllocationManager as it didn't exist before this deploy + + function (bool, string memory) internal pure assertion = areMatching ? _assertTrue : _assertFalse; + + assertion( + Env._getProxyImpl(address(Env.proxy.permissionController())) == address(Env.impl.permissionController()), + "permissionController impl failed" + ); + + assertion( + Env._getProxyImpl(address(Env.proxy.delegationManager())) == address(Env.impl.delegationManager()), + "delegationManager impl failed" + ); + + assertion( + Env._getProxyImpl(address(Env.proxy.strategyManager())) == address(Env.impl.strategyManager()), + "strategyManager impl failed" + ); + + assertion( + Env._getProxyImpl(address(Env.proxy.allocationManager())) == address(Env.impl.allocationManager()), + "allocationManager impl failed" + ); + + assertion( + Env._getProxyImpl(address(Env.proxy.rewardsCoordinator())) == address(Env.impl.rewardsCoordinator()), + "rewardsCoordinator impl failed" + ); + + assertion( + Env._getProxyImpl(address(Env.proxy.avsDirectory())) == address(Env.impl.avsDirectory()), + "avsDirectory impl failed" + ); + + assertion( + Env._getProxyImpl(address(Env.proxy.eigenPodManager())) == address(Env.impl.eigenPodManager()), + "eigenPodManager impl failed" + ); + + assertion( + Env._getProxyImpl(address(Env.proxy.strategyFactory())) == address(Env.impl.strategyFactory()), + "strategyFactory impl failed" + ); + + assertion( + Env._getProxyImpl(address(Env.proxy.releaseManager())) == address(Env.impl.releaseManager()), + "releaseManager impl failed" + ); + + /// permissions/ + assertion( + Env._getProxyImpl(address(Env.proxy.keyRegistrar())) == address(Env.impl.keyRegistrar()), + "keyRegistrar impl failed" + ); + + /// strategies/ + assertion( + Env._getProxyImpl(address(Env.proxy.eigenStrategy())) == address(Env.impl.eigenStrategy()), + "eigenStrategy impl failed" + ); + } + + /// @dev Mirrors the checks done in 4-deployCore.s.sol, but now we check each contract's + /// proxy, as the upgrade should mean that each proxy can see these methods/immutables + function _validateProxyConstructors() internal view { + { + /// core/ + + // Permission controller has no constructor + + DelegationManager delegation = Env.proxy.delegationManager(); + assertTrue(delegation.strategyManager() == Env.proxy.strategyManager(), "dm.sm invalid"); + assertTrue(delegation.eigenPodManager() == Env.proxy.eigenPodManager(), "dm.epm invalid"); + assertTrue(delegation.allocationManager() == Env.proxy.allocationManager(), "dm.alm invalid"); + assertTrue(delegation.pauserRegistry() == Env.impl.pauserRegistry(), "dm.pR invalid"); + assertTrue(delegation.permissionController() == Env.proxy.permissionController(), "dm.pc invalid"); + assertTrue( + delegation.minWithdrawalDelayBlocks() == Env.MIN_WITHDRAWAL_DELAY(), "dm.withdrawalDelay invalid" + ); + + StrategyManager strategyManager = Env.proxy.strategyManager(); + assertTrue(strategyManager.delegation() == Env.proxy.delegationManager(), "sm.dm invalid"); + assertTrue(strategyManager.pauserRegistry() == Env.impl.pauserRegistry(), "sm.pR invalid"); + + AllocationManager allocationManager = Env.proxy.allocationManager(); + assertTrue(allocationManager.delegation() == Env.proxy.delegationManager(), "alm.dm invalid"); + assertTrue(allocationManager.pauserRegistry() == Env.impl.pauserRegistry(), "alm.pR invalid"); + assertTrue(allocationManager.permissionController() == Env.proxy.permissionController(), "alm.pc invalid"); + assertTrue(allocationManager.DEALLOCATION_DELAY() == Env.MIN_WITHDRAWAL_DELAY(), "alm.deallocDelay invalid"); + assertTrue( + allocationManager.ALLOCATION_CONFIGURATION_DELAY() == Env.ALLOCATION_CONFIGURATION_DELAY(), + "alm.configDelay invalid" + ); + + RewardsCoordinator rewards = Env.proxy.rewardsCoordinator(); + assertTrue(rewards.delegationManager() == Env.proxy.delegationManager(), "rc.dm invalid"); + assertTrue(rewards.strategyManager() == Env.proxy.strategyManager(), "rc.sm invalid"); + assertTrue(rewards.allocationManager() == Env.proxy.allocationManager(), "rc.alm invalid"); + assertTrue(rewards.pauserRegistry() == Env.impl.pauserRegistry(), "rc.pR invalid"); + assertTrue(rewards.permissionController() == Env.proxy.permissionController(), "rc.pc invalid"); + assertTrue( + rewards.CALCULATION_INTERVAL_SECONDS() == Env.CALCULATION_INTERVAL_SECONDS(), "rc.calcInterval invalid" + ); + assertTrue(rewards.MAX_REWARDS_DURATION() == Env.MAX_REWARDS_DURATION(), "rc.rewardsDuration invalid"); + assertTrue(rewards.MAX_RETROACTIVE_LENGTH() == Env.MAX_RETROACTIVE_LENGTH(), "rc.retroLength invalid"); + assertTrue(rewards.MAX_FUTURE_LENGTH() == Env.MAX_FUTURE_LENGTH(), "rc.futureLength invalid"); + assertTrue(rewards.GENESIS_REWARDS_TIMESTAMP() == Env.GENESIS_REWARDS_TIMESTAMP(), "rc.genesis invalid"); + + AVSDirectory avsDirectory = Env.proxy.avsDirectory(); + assertTrue(avsDirectory.delegation() == Env.proxy.delegationManager(), "avsD.dm invalid"); + assertTrue(avsDirectory.pauserRegistry() == Env.impl.pauserRegistry(), "avsD.pR invalid"); + + ReleaseManager releaseManager = Env.proxy.releaseManager(); + assertTrue(releaseManager.permissionController() == Env.proxy.permissionController(), "rm.pc invalid"); + } + + { + /// permissions/ + KeyRegistrar keyRegistrar = Env.proxy.keyRegistrar(); + assertTrue(keyRegistrar.permissionController() == Env.proxy.permissionController(), "kr.pc invalid"); + assertTrue(keyRegistrar.allocationManager() == Env.proxy.allocationManager(), "kr.alm invalid"); + } + + { + /// pods/ + UpgradeableBeacon eigenPodBeacon = Env.beacon.eigenPod(); + assertTrue(eigenPodBeacon.implementation() == address(Env.impl.eigenPod()), "eigenPodBeacon.impl invalid"); + + EigenPodManager eigenPodManager = Env.proxy.eigenPodManager(); + assertTrue(eigenPodManager.ethPOS() == Env.ethPOS(), "epm.ethPOS invalid"); + assertTrue(eigenPodManager.eigenPodBeacon() == Env.beacon.eigenPod(), "epm.epBeacon invalid"); + assertTrue(eigenPodManager.delegationManager() == Env.proxy.delegationManager(), "epm.dm invalid"); + assertTrue(eigenPodManager.pauserRegistry() == Env.impl.pauserRegistry(), "epm.pR invalid"); + } + + { + /// strategies/ + EigenStrategy eigenStrategy = Env.proxy.eigenStrategy(); + assertTrue(eigenStrategy.strategyManager() == Env.proxy.strategyManager(), "eigStrat.sm invalid"); + assertTrue(eigenStrategy.pauserRegistry() == Env.impl.pauserRegistry(), "eigStrat.pR invalid"); + + UpgradeableBeacon strategyBeacon = Env.beacon.strategyBase(); + assertTrue( + strategyBeacon.implementation() == address(Env.impl.strategyBase()), "strategyBeacon.impl invalid" + ); + + StrategyFactory strategyFactory = Env.proxy.strategyFactory(); + assertTrue(strategyFactory.strategyManager() == Env.proxy.strategyManager(), "sFact.sm invalid"); + assertTrue(strategyFactory.pauserRegistry() == Env.impl.pauserRegistry(), "sFact.pR invalid"); + } + } + + /// @dev Call initialize on all proxies to ensure they are initialized + /// Additionally, validate initialization variables + function _validateProxiesInitialized() internal { + bytes memory errInit = "Initializable: contract is already initialized"; + + { + /// core/ + DelegationManager delegation = Env.proxy.delegationManager(); + vm.expectRevert(errInit); + delegation.initialize(0); + assertTrue(delegation.paused() == 0, "dm.paused invalid"); + + StrategyManager strategyManager = Env.proxy.strategyManager(); + vm.expectRevert(errInit); + strategyManager.initialize(address(0), address(0), 0); + assertTrue(strategyManager.owner() == Env.executorMultisig(), "sm.owner invalid"); + assertTrue(strategyManager.paused() == 0, "sm.paused invalid"); + assertTrue( + strategyManager.strategyWhitelister() == address(Env.proxy.strategyFactory()), "sm.whitelister invalid" + ); + + AllocationManager allocationManager = Env.proxy.allocationManager(); + vm.expectRevert(errInit); + allocationManager.initialize(0); + assertTrue(allocationManager.paused() == 0, "alm.paused invalid"); + + RewardsCoordinator rewards = Env.proxy.rewardsCoordinator(); + vm.expectRevert(errInit); + rewards.initialize(address(0), 0, address(0), 0, 0); + assertTrue(rewards.owner() == Env.opsMultisig(), "rc.owner invalid"); + assertTrue(rewards.paused() == Env.REWARDS_PAUSE_STATUS(), "rc.paused invalid"); + assertTrue(rewards.rewardsUpdater() == Env.REWARDS_UPDATER(), "rc.updater invalid"); + assertTrue(rewards.activationDelay() == Env.ACTIVATION_DELAY(), "rc.activationDelay invalid"); + assertTrue(rewards.defaultOperatorSplitBips() == Env.DEFAULT_SPLIT_BIPS(), "rc.splitBips invalid"); + + AVSDirectory avsDirectory = Env.proxy.avsDirectory(); + vm.expectRevert(errInit); + avsDirectory.initialize(address(0), 0); + assertTrue(avsDirectory.owner() == Env.executorMultisig(), "avsD.owner invalid"); + assertTrue(avsDirectory.paused() == 0, "avsD.paused invalid"); + + // ReleaseManager is not initializable + } + + // KeyRegistrar and PermissionController are not initializable + + { + /// pods/ + // EigenPod proxies are initialized by individual users + + EigenPodManager eigenPodManager = Env.proxy.eigenPodManager(); + vm.expectRevert(errInit); + eigenPodManager.initialize(address(0), 0); + assertTrue(eigenPodManager.owner() == Env.executorMultisig(), "epm.owner invalid"); + assertTrue(eigenPodManager.paused() == 0, "epm.paused invalid"); + } + + { + /// strategies/ + + EigenStrategy eigenStrategy = Env.proxy.eigenStrategy(); + vm.expectRevert(errInit); + eigenStrategy.initialize(IEigen(address(0)), IBackingEigen(address(0))); + assertTrue(eigenStrategy.paused() == 0, "eigenStrat.paused invalid"); + assertEq(address(eigenStrategy.EIGEN()), address(Env.proxy.eigen()), "eigenStrat.EIGEN invalid"); + assertEq( + address(eigenStrategy.underlyingToken()), address(Env.proxy.beigen()), "eigenStrat.underlying invalid" + ); + + // StrategyBase proxies are initialized when deployed by factory + StrategyFactory strategyFactory = Env.proxy.strategyFactory(); + vm.expectRevert(errInit); + strategyFactory.initialize(address(0), 0, UpgradeableBeacon(address(0))); + assertTrue(strategyFactory.owner() == Env.opsMultisig(), "sFact.owner invalid"); + assertTrue(strategyFactory.paused() == 0, "sFact.paused invalid"); + assertTrue(strategyFactory.strategyBeacon() == Env.beacon.strategyBase(), "sFact.beacon invalid"); + } + } + + function _assertTrue(bool b, string memory err) private pure { + assertTrue(b, err); + } + + function _assertFalse(bool b, string memory err) private pure { + assertFalse(b, err); + } +} diff --git a/script/releases/v1.6.0-protocol-from-scratch/7-setForkTimestamp.s.sol b/script/releases/v1.6.0-protocol-from-scratch/7-setForkTimestamp.s.sol new file mode 100644 index 0000000000..e72afa5cc4 --- /dev/null +++ b/script/releases/v1.6.0-protocol-from-scratch/7-setForkTimestamp.s.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import "../Env.sol"; +import "forge-std/console.sol"; + +import {DeployGovernance} from "./1-deployGovernance.s.sol"; +import {DeployPauser} from "./2-deployPauser.s.sol"; +import {DeployToken} from "./3-deployToken.s.sol"; +import {DeployCore} from "./4-deployCore.s.sol"; +import {Queue} from "./5-queueUpgrade.s.sol"; +import {Execute} from "./6-executeUpgrade.s.sol"; + +/// @notice Proposes a transaction to set the proof timestamp +/// @dev This is needed for Eigenpods due to Pectra fork timestamp being set after the fork +contract SetForkTimestamp is Execute { + using Env for *; + using ZEnvHelpers for *; + + uint64 proofTimestamp; + + function _runAsMultisig() internal virtual override prank(Env.opsMultisig()) { + proofTimestamp = ZEnvHelpers.state().envU64("PECTRA_FORK_TIMESTAMP"); + + // Sanity check that the timestamp is nonzero + require(proofTimestamp > 0, "proofTimestamp is zero"); + + Env.proxy.eigenPodManager().setPectraForkTimestamp(proofTimestamp); + } + + function setTimestamp( + uint64 _proofTimestamp + ) public { + proofTimestamp = _proofTimestamp; + } + + function testScript() public virtual override { + /// Complete steps 1-6 of the upgrade script + _mode = OperationalMode.EOA; + DeployGovernance._runAsEOA(); + DeployPauser._runAsEOA(); + DeployToken._runAsEOA(); + DeployCore._runAsEOA(); + Queue._runAsMultisig(); + _unsafeResetHasPranked(); + vm.warp(block.timestamp + Env.timelockController().getMinDelay()); // 1 tick after ETA + Execute._runAsMultisig(); + _unsafeResetHasPranked(); + + // Execute the set fork timestamp + execute(); + + // Validate that the proof timestamp is set + assertEq(Env.proxy.eigenPodManager().pectraForkTimestamp(), proofTimestamp, "Proof timestamp is not set"); + } + + function _getProofTimestamp() internal view returns (uint64) { + string memory timestampPath = + string.concat(vm.projectRoot(), "/script/releases/v1.4.1-slashing/forkTimestamp.txt"); + string memory timestamp = vm.readFile(timestampPath); + return uint64(vm.parseUint(timestamp)); + } +} diff --git a/script/releases/v1.6.0-protocol-from-scratch/upgrade.json b/script/releases/v1.6.0-protocol-from-scratch/upgrade.json new file mode 100644 index 0000000000..3cd2c11af5 --- /dev/null +++ b/script/releases/v1.6.0-protocol-from-scratch/upgrade.json @@ -0,0 +1,35 @@ +{ + "name": "v0.0.0-source-genesis", + "from": "0.0.0", + "to": "1.6.0", + "phases": [ + { + "type": "eoa", + "filename": "1-deployGovernance.s.sol" + }, + { + "type": "eoa", + "filename": "2-deployPauser.s.sol" + }, + { + "type": "eoa", + "filename": "3-deployToken.s.sol" + }, + { + "type": "eoa", + "filename": "4-deployCore.s.sol" + }, + { + "type": "multisig", + "filename": "5-queueUpgrade.s.sol" + }, + { + "type": "multisig", + "filename": "6-executeUpgrade.s.sol" + }, + { + "type": "multisig", + "filename": "7-setForkTimestamp.s.sol" + } + ] +} \ No newline at end of file diff --git a/script/releases/v1.8.1-hourglass-testnet-replay-fix/1-deployTaskMailboxImpl.s.sol b/script/releases/v1.8.1-hourglass-testnet-replay-fix/1-deployTaskMailboxImpl.s.sol deleted file mode 100644 index a4fa863721..0000000000 --- a/script/releases/v1.8.1-hourglass-testnet-replay-fix/1-deployTaskMailboxImpl.s.sol +++ /dev/null @@ -1,118 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.12; - -import {EOADeployer} from "zeus-templates/templates/EOADeployer.sol"; -import "../Env.sol"; - -/** - * @title DeployTaskMailboxImpl - * @notice Deploy new TaskMailbox implementation with task replay fix for destination chains. - * This fixes a vulnerability where certificates could be replayed across different tasks - * directed at the same operator set by including the taskHash in the messageHash. - */ -contract DeployTaskMailboxImpl is EOADeployer { - using Env for *; - - /// forgefmt: disable-next-item - function _runAsEOA() internal override { - // If we're not on a destination chain and not on version 1.8.0, we don't need to deploy any contracts - if (!(Env.isDestinationChain() && Env._strEq(Env.envVersion(), "1.8.0"))) { - return; - } - - vm.startBroadcast(); - - // Deploy TaskMailbox implementation with the replay fix - deployImpl({ - name: type(TaskMailbox).name, - deployedTo: address( - new TaskMailbox({ - _bn254CertificateVerifier: address(Env.proxy.bn254CertificateVerifier()), - _ecdsaCertificateVerifier: address(Env.proxy.ecdsaCertificateVerifier()), - _maxTaskSLA: Env.MAX_TASK_SLA(), - _version: Env.deployVersion() - }) - ) - }); - - vm.stopBroadcast(); - } - - function testScript() public virtual { - if (!(Env.isDestinationChain() && Env._strEq(Env.envVersion(), "1.8.0"))) { - return; - } - - // Deploy the new TaskMailbox implementation - runAsEOA(); - - _validateNewImplAddress(); - _validateProxyAdmin(); - _validateImplConstructor(); - _validateImplInitialized(); - _validateVersion(); - } - - /// @dev Validate that the new TaskMailbox impl address is distinct from the current one - function _validateNewImplAddress() internal view { - address currentImpl = Env._getProxyImpl(address(Env.proxy.taskMailbox())); - address newImpl = address(Env.impl.taskMailbox()); - - assertFalse(currentImpl == newImpl, "TaskMailbox impl should be different from current implementation"); - } - - /// @dev Validate that the TaskMailbox proxy is still owned by the correct ProxyAdmin - function _validateProxyAdmin() internal view { - address pa = Env.proxyAdmin(); - - assertTrue(Env._getProxyAdmin(address(Env.proxy.taskMailbox())) == pa, "TaskMailbox proxyAdmin incorrect"); - } - - /// @dev Validate the immutables set in the new TaskMailbox implementation constructor - function _validateImplConstructor() internal view { - TaskMailbox taskMailboxImpl = Env.impl.taskMailbox(); - - // Validate version - assertEq( - keccak256(bytes(taskMailboxImpl.version())), - keccak256(bytes(Env.deployVersion())), - "TaskMailbox impl version mismatch" - ); - - // Validate certificate verifiers - assertTrue( - taskMailboxImpl.BN254_CERTIFICATE_VERIFIER() == address(Env.proxy.bn254CertificateVerifier()), - "TaskMailbox BN254_CERTIFICATE_VERIFIER mismatch" - ); - assertTrue( - taskMailboxImpl.ECDSA_CERTIFICATE_VERIFIER() == address(Env.proxy.ecdsaCertificateVerifier()), - "TaskMailbox ECDSA_CERTIFICATE_VERIFIER mismatch" - ); - - // Validate MAX_TASK_SLA - assertEq(taskMailboxImpl.MAX_TASK_SLA(), Env.MAX_TASK_SLA(), "TaskMailbox MAX_TASK_SLA mismatch"); - } - - /// @dev Validate that the new implementation cannot be initialized (should revert) - function _validateImplInitialized() internal { - bytes memory errInit = "Initializable: contract is already initialized"; - - TaskMailbox taskMailboxImpl = Env.impl.taskMailbox(); - - vm.expectRevert(errInit); - taskMailboxImpl.initialize( - address(0), // owner - 0, // feeSplit - address(0) // feeSplitCollector - ); - } - - /// @dev Validate the version is correctly set - function _validateVersion() internal view { - assertEq( - keccak256(bytes(Env.impl.taskMailbox().version())), - keccak256(bytes(Env.deployVersion())), - "TaskMailbox version should match deploy version" - ); - } -} diff --git a/script/releases/v1.8.1-hourglass-testnet-replay-fix/2-queueTaskMailboxUpgrade.s.sol b/script/releases/v1.8.1-hourglass-testnet-replay-fix/2-queueTaskMailboxUpgrade.s.sol deleted file mode 100644 index 80bba7a02a..0000000000 --- a/script/releases/v1.8.1-hourglass-testnet-replay-fix/2-queueTaskMailboxUpgrade.s.sol +++ /dev/null @@ -1,92 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.12; - -import {DeployTaskMailboxImpl} from "./1-deployTaskMailboxImpl.s.sol"; -import "../Env.sol"; - -import {MultisigBuilder} from "zeus-templates/templates/MultisigBuilder.sol"; -import {MultisigCall, Encode} from "zeus-templates/utils/Encode.sol"; - -import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol"; - -/** - * @title QueueTaskMailboxUpgrade - * @notice Queue the TaskMailbox upgrade transaction in the Timelock via the Operations Multisig. - * This queues the upgrade to fix the task replay vulnerability. - */ -contract QueueTaskMailboxUpgrade is MultisigBuilder, DeployTaskMailboxImpl { - using Env for *; - using Encode for *; - - function _runAsMultisig() internal virtual override prank(Env.opsMultisig()) { - if (!(Env.isDestinationChain() && Env._strEq(Env.envVersion(), "1.8.0"))) { - return; - } - - bytes memory calldata_to_executor = _getCalldataToExecutor(); - - TimelockController timelock = Env.timelockController(); - timelock.schedule({ - target: Env.executorMultisig(), - value: 0, - data: calldata_to_executor, - predecessor: 0, - salt: 0, - delay: timelock.getMinDelay() - }); - } - - /// @dev Get the calldata to be sent from the timelock to the executor - function _getCalldataToExecutor() internal returns (bytes memory) { - /// forgefmt: disable-next-item - MultisigCall[] storage executorCalls = Encode.newMultisigCalls().append({ - to: Env.proxyAdmin(), - data: Encode.proxyAdmin.upgrade({ - proxy: address(Env.proxy.taskMailbox()), - impl: address(Env.impl.taskMailbox()) - }) - }); - - return Encode.gnosisSafe.execTransaction({ - from: address(Env.timelockController()), - to: Env.multiSendCallOnly(), - op: Encode.Operation.DelegateCall, - data: Encode.multiSend(executorCalls) - }); - } - - function testScript() public virtual override { - if (!(Env.isDestinationChain() && Env._strEq(Env.envVersion(), "1.8.0"))) { - return; - } - - // Deploy the new TaskMailbox implementation first - runAsEOA(); - - TimelockController timelock = Env.timelockController(); - bytes memory calldata_to_executor = _getCalldataToExecutor(); - bytes32 txHash = timelock.hashOperation({ - target: Env.executorMultisig(), - value: 0, - data: calldata_to_executor, - predecessor: 0, - salt: 0 - }); - - // Check that the upgrade does not exist in the timelock - assertFalse(timelock.isOperationPending(txHash), "Transaction should NOT be queued yet"); - - // Queue the upgrade - execute(); - - // Check that the upgrade has been added to the timelock - assertTrue(timelock.isOperationPending(txHash), "Transaction should be queued"); - assertFalse(timelock.isOperationReady(txHash), "Transaction should NOT be ready immediately"); - assertFalse(timelock.isOperationDone(txHash), "Transaction should NOT be done"); - - // Validate that the TaskMailbox proxy still points to the old implementation - address currentImpl = Env._getProxyImpl(address(Env.proxy.taskMailbox())); - address newImpl = address(Env.impl.taskMailbox()); - assertFalse(currentImpl == newImpl, "TaskMailbox proxy should still point to old implementation"); - } -} diff --git a/script/releases/v1.8.1-hourglass-testnet-replay-fix/3-executeTaskMailboxUpgrade.s.sol b/script/releases/v1.8.1-hourglass-testnet-replay-fix/3-executeTaskMailboxUpgrade.s.sol deleted file mode 100644 index 92139ba38d..0000000000 --- a/script/releases/v1.8.1-hourglass-testnet-replay-fix/3-executeTaskMailboxUpgrade.s.sol +++ /dev/null @@ -1,150 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.12; - -import "../Env.sol"; -import {QueueTaskMailboxUpgrade} from "./2-queueTaskMailboxUpgrade.s.sol"; -import {Encode} from "zeus-templates/utils/Encode.sol"; - -/** - * @title ExecuteTaskMailboxUpgrade - * @notice Execute the queued TaskMailbox upgrade after the timelock delay. - * This completes the upgrade to fix the task replay vulnerability where certificates - * could be replayed across different tasks directed at the same operator set. - */ -contract ExecuteTaskMailboxUpgrade is QueueTaskMailboxUpgrade { - using Env for *; - using Encode for *; - - function _runAsMultisig() internal override prank(Env.protocolCouncilMultisig()) { - if (!(Env.isDestinationChain() && Env._strEq(Env.envVersion(), "1.8.0"))) { - return; - } - - bytes memory calldata_to_executor = _getCalldataToExecutor(); - TimelockController timelock = Env.timelockController(); - - timelock.execute({ - target: Env.executorMultisig(), - value: 0, - payload: calldata_to_executor, - predecessor: 0, - salt: 0 - }); - } - - function testScript() public virtual override { - if (!(Env.isDestinationChain() && Env._strEq(Env.envVersion(), "1.8.0"))) { - return; - } - - // 1 - Deploy. The new TaskMailbox implementation has been deployed - runAsEOA(); - - TimelockController timelock = Env.timelockController(); - bytes memory calldata_to_executor = _getCalldataToExecutor(); - bytes32 txHash = timelock.hashOperation({ - target: Env.executorMultisig(), - value: 0, - data: calldata_to_executor, - predecessor: 0, - salt: 0 - }); - - // 2 - Queue. Check that the operation IS ready - QueueTaskMailboxUpgrade._runAsMultisig(); - _unsafeResetHasPranked(); // reset hasPranked so we can use it again - - assertTrue(timelock.isOperationPending(txHash), "Transaction should be queued"); - assertFalse(timelock.isOperationReady(txHash), "Transaction should NOT be ready immediately"); - assertFalse(timelock.isOperationDone(txHash), "Transaction should NOT be complete"); - - // 3 - Warp past the timelock delay - vm.warp(block.timestamp + timelock.getMinDelay()); - assertTrue(timelock.isOperationReady(txHash), "Transaction should be ready for execution"); - - // 4 - Execute the upgrade - execute(); - assertTrue(timelock.isOperationDone(txHash), "v1.8.1 TaskMailbox upgrade should be complete"); - - // 5 - Validate the upgrade was successful - _validateUpgradeComplete(); - _validateProxyAdmin(); - _validateProxyConstructor(); - _validateProxyInitialized(); - _validateGetMessageHash(); - } - - /// @dev Validate that the TaskMailbox proxy now points to the new implementation - function _validateUpgradeComplete() internal view { - address currentImpl = Env._getProxyImpl(address(Env.proxy.taskMailbox())); - address expectedImpl = address(Env.impl.taskMailbox()); - - assertTrue(currentImpl == expectedImpl, "TaskMailbox proxy should point to new implementation"); - } - - /// @dev Validate the proxy's constructor values through the proxy - function _validateProxyConstructor() internal view { - TaskMailbox taskMailbox = Env.proxy.taskMailbox(); - - // Validate version - assertEq( - keccak256(bytes(taskMailbox.version())), - keccak256(bytes(Env.deployVersion())), - "TaskMailbox version mismatch" - ); - - // Validate certificate verifiers - assertTrue( - taskMailbox.BN254_CERTIFICATE_VERIFIER() == address(Env.proxy.bn254CertificateVerifier()), - "TaskMailbox BN254_CERTIFICATE_VERIFIER mismatch" - ); - assertTrue( - taskMailbox.ECDSA_CERTIFICATE_VERIFIER() == address(Env.proxy.ecdsaCertificateVerifier()), - "TaskMailbox ECDSA_CERTIFICATE_VERIFIER mismatch" - ); - - // Validate MAX_TASK_SLA - assertEq(taskMailbox.MAX_TASK_SLA(), Env.MAX_TASK_SLA(), "TaskMailbox MAX_TASK_SLA mismatch"); - } - - /// @dev Validate that the proxy cannot be re-initialized - function _validateProxyInitialized() internal { - bytes memory errInit = "Initializable: contract is already initialized"; - - TaskMailbox taskMailbox = Env.proxy.taskMailbox(); - - vm.expectRevert(errInit); - taskMailbox.initialize( - address(0), // owner - 0, // feeSplit - address(0) // feeSplitCollector - ); - } - - /// @dev Validate that the new getMessageHash function works correctly - function _validateGetMessageHash() internal view { - TaskMailbox taskMailbox = Env.proxy.taskMailbox(); - - // Test the new getMessageHash function with sample data - bytes32 taskHash = keccak256("test_task"); - bytes memory result = abi.encode("test_result"); - - // The new implementation should compute messageHash as keccak256(abi.encode(taskHash, result)) - bytes32 expectedMessageHash = keccak256(abi.encode(taskHash, result)); - bytes32 actualMessageHash = taskMailbox.getMessageHash(taskHash, result); - - assertTrue( - expectedMessageHash == actualMessageHash, - "getMessageHash should compute correct message hash with taskHash included" - ); - - // Verify that different tasks with same result produce different message hashes - bytes32 differentTaskHash = keccak256("different_task"); - bytes32 differentMessageHash = taskMailbox.getMessageHash(differentTaskHash, result); - - assertFalse( - actualMessageHash == differentMessageHash, - "Different tasks with same result should produce different message hashes" - ); - } -} diff --git a/script/releases/v1.8.1-hourglass-testnet-replay-fix/upgrade.json b/script/releases/v1.8.1-hourglass-testnet-replay-fix/upgrade.json deleted file mode 100644 index 565c726492..0000000000 --- a/script/releases/v1.8.1-hourglass-testnet-replay-fix/upgrade.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "hourglass-testnet-replay-fix-v1.8.1", - "from": "1.8.0", - "to": "1.8.1", - "phases": [ - { - "type": "eoa", - "filename": "1-deployTaskMailboxImpl.s.sol" - }, - { - "type": "multisig", - "filename": "2-queueTaskMailboxUpgrade.s.sol" - }, - { - "type": "multisig", - "filename": "3-executeTaskMailboxUpgrade.s.sol" - } - ] -} \ No newline at end of file