From 44ece6d81ae87fddee8fd15c5b5e04fcee22143c Mon Sep 17 00:00:00 2001 From: Yash Patil <40046473+ypatil12@users.noreply.github.com> Date: Fri, 3 Jan 2025 16:52:00 -0500 Subject: [PATCH] feat: slashing 1.0.3 upgrade script (#995) * feat: add step 1 * feat: step 1 & 2 complete; pending step 3 sanity * test: add `_validateProxyDomainSeparators` * feat: add rc validation --------- Co-authored-by: clandestine.eth <96172957+0xClandestine@users.noreply.github.com> --- script/releases/v1.0.3-slashing/1-eoa.s.sol | 230 ++++++++++++++++++ .../releases/v1.0.3-slashing/2-multisig.s.sol | 93 +++++++ .../releases/v1.0.3-slashing/3-execute.s.sol | 148 +++++++++++ script/releases/v1.0.3-slashing/upgrade.json | 19 ++ 4 files changed, 490 insertions(+) create mode 100644 script/releases/v1.0.3-slashing/1-eoa.s.sol create mode 100644 script/releases/v1.0.3-slashing/2-multisig.s.sol create mode 100644 script/releases/v1.0.3-slashing/3-execute.s.sol create mode 100644 script/releases/v1.0.3-slashing/upgrade.json diff --git a/script/releases/v1.0.3-slashing/1-eoa.s.sol b/script/releases/v1.0.3-slashing/1-eoa.s.sol new file mode 100644 index 000000000..d9a5793aa --- /dev/null +++ b/script/releases/v1.0.3-slashing/1-eoa.s.sol @@ -0,0 +1,230 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import {EOADeployer} from "zeus-templates/templates/EOADeployer.sol"; +import "../Env.sol"; + +import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +// Just upgrade StrategyManager +contract Deploy is EOADeployer { + using Env for *; + + function _runAsEOA() internal override { + vm.startBroadcast(); + + // Deploy DM + deployImpl({ + name: type(DelegationManager).name, + deployedTo: address(new DelegationManager({ + _strategyManager: Env.proxy.strategyManager(), + _eigenPodManager: Env.proxy.eigenPodManager(), + _allocationManager: Env.proxy.allocationManager(), + _pauserRegistry: Env.impl.pauserRegistry(), + _permissionController: Env.proxy.permissionController(), + _MIN_WITHDRAWAL_DELAY: Env.MIN_WITHDRAWAL_DELAY() + })) + }); + + // Deploy AVSD + deployImpl({ + name: type(AVSDirectory).name, + deployedTo: address(new AVSDirectory({ + _delegation: Env.proxy.delegationManager(), + _pauserRegistry: Env.impl.pauserRegistry() + })) + }); + + // Deploy SM + deployImpl({ + name: type(StrategyManager).name, + deployedTo: address(new StrategyManager({ + _delegation: Env.proxy.delegationManager(), + _pauserRegistry: Env.impl.pauserRegistry() + })) + }); + + // Deploy RC + deployImpl({ + name: type(RewardsCoordinator).name, + deployedTo: address(new RewardsCoordinator({ + _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() + })) + }); + + vm.stopBroadcast(); + } + + function testDeploy() public virtual { + _runAsEOA(); + _validateDomainSeparatorNonZero(); + _validateNewImplAddresses(false); + _validateImplConstructors(); + _validateImplsInitialized(); + _validateRCValues(); + } + + function _validateDomainSeparatorNonZero() internal view { + bytes32 zeroDomainSeparator = bytes32(0); + + assertFalse(Env.impl.avsDirectory().domainSeparator() == zeroDomainSeparator, "avsD.domainSeparator is zero"); + assertFalse(Env.impl.delegationManager().domainSeparator() == zeroDomainSeparator, "dm.domainSeparator is zero"); + assertFalse(Env.impl.strategyManager().domainSeparator() == zeroDomainSeparator, "rc.domainSeparator is zero"); + } + + + /// @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 { + function (address, address, string memory) internal pure assertion = + areMatching ? _assertMatch : _assertNotMatch; + + + assertion( + _getProxyImpl(address(Env.proxy.strategyManager())), + address(Env.impl.strategyManager()), + "strategyManager impl failed" + ); + + assertion( + _getProxyImpl(address(Env.proxy.delegationManager())), + address(Env.impl.delegationManager()), + "delegationManager impl failed" + ); + + assertion( + _getProxyImpl(address(Env.proxy.avsDirectory())), + address(Env.impl.avsDirectory()), + "avsdirectory impl failed" + ); + + assertion( + _getProxyImpl(address(Env.proxy.rewardsCoordinator())), + address(Env.impl.rewardsCoordinator()), + "rewardsCoordinator impl failed" + ); + } + + /// @dev Validate the immutables set in the new implementation constructors + function _validateImplConstructors() internal view { + 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"); + } + + /// @dev Call initialize on all deployed implementations to ensure initializers are disabled + function _validateImplsInitialized() internal { + bytes memory errInit = "Initializable: contract is already initialized"; + + AVSDirectory avsDirectory = Env.impl.avsDirectory(); + vm.expectRevert(errInit); + avsDirectory.initialize(address(0), 0); + + DelegationManager delegation = Env.impl.delegationManager(); + vm.expectRevert(errInit); + delegation.initialize(address(0), 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); + } + + function _validateRCValues() internal view { + + RewardsCoordinator rewardsCoordinatorImpl = Env.impl.rewardsCoordinator(); + assertEq( + rewardsCoordinatorImpl.CALCULATION_INTERVAL_SECONDS(), + Env.CALCULATION_INTERVAL_SECONDS(), + "expected REWARDS_COORDINATOR_CALCULATION_INTERVAL_SECONDS" + ); + assertEq( + rewardsCoordinatorImpl.CALCULATION_INTERVAL_SECONDS(), + 1 days, + "expected REWARDS_COORDINATOR_CALCULATION_INTERVAL_SECONDS" + ); + assertGt( + rewardsCoordinatorImpl.CALCULATION_INTERVAL_SECONDS(), + 0, + "expected non-zero REWARDS_COORDINATOR_CALCULATION_INTERVAL_SECONDS" + ); + + assertEq(rewardsCoordinatorImpl.MAX_REWARDS_DURATION(), Env.MAX_REWARDS_DURATION()); + assertGt(rewardsCoordinatorImpl.MAX_REWARDS_DURATION(), 0); + + assertEq( + rewardsCoordinatorImpl.MAX_RETROACTIVE_LENGTH(), + Env.MAX_RETROACTIVE_LENGTH() + ); + assertGt(rewardsCoordinatorImpl.MAX_RETROACTIVE_LENGTH(), 0); + + assertEq(rewardsCoordinatorImpl.MAX_FUTURE_LENGTH(), Env.MAX_FUTURE_LENGTH()); + assertGt(rewardsCoordinatorImpl.MAX_FUTURE_LENGTH(), 0); + + assertEq( + rewardsCoordinatorImpl.GENESIS_REWARDS_TIMESTAMP(), + Env.GENESIS_REWARDS_TIMESTAMP() + ); + assertGt(rewardsCoordinatorImpl.GENESIS_REWARDS_TIMESTAMP(), 0); + } + + /// @dev Query and return `proxyAdmin.getProxyImplementation(proxy)` + function _getProxyImpl(address proxy) internal view returns (address) { + return ProxyAdmin(Env.proxyAdmin()).getProxyImplementation(ITransparentUpgradeableProxy(proxy)); + } + + /// @dev Query and return `proxyAdmin.getProxyAdmin(proxy)` + function _getProxyAdmin(address proxy) internal view returns (address) { + return ProxyAdmin(Env.proxyAdmin()).getProxyAdmin(ITransparentUpgradeableProxy(proxy)); + } + + function _assertMatch(address a, address b, string memory err) private pure { + assertEq(a, b, err); + } + + function _assertNotMatch(address a, address b, string memory err) private pure { + assertNotEq(a, b, err); + } +} \ No newline at end of file diff --git a/script/releases/v1.0.3-slashing/2-multisig.s.sol b/script/releases/v1.0.3-slashing/2-multisig.s.sol new file mode 100644 index 000000000..160419797 --- /dev/null +++ b/script/releases/v1.0.3-slashing/2-multisig.s.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import {Deploy} from "./1-eoa.s.sol"; +import "../Env.sol"; + +import {MultisigBuilder} from "zeus-templates/templates/MultisigBuilder.sol"; +import "zeus-templates/utils/Encode.sol"; + +import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol"; + +contract Queue is MultisigBuilder, Deploy { + using Env for *; + using Encode for *; + + function _runAsMultisig() prank(Env.opsMultisig()) internal virtual override { + 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) { + MultisigCall[] storage executorCalls = Encode.newMultisigCalls() + /// core/ + .append({ + to: Env.proxyAdmin(), + data: Encode.proxyAdmin.upgrade({ + proxy: address(Env.proxy.strategyManager()), + impl: address(Env.impl.strategyManager()) + }) + }) + .append({ + to: Env.proxyAdmin(), + data: Encode.proxyAdmin.upgrade({ + proxy: address(Env.proxy.avsDirectory()), + impl: address(Env.impl.avsDirectory()) + }) + }) + .append({ + to: Env.proxyAdmin(), + data: Encode.proxyAdmin.upgrade({ + proxy: address(Env.proxy.delegationManager()), + impl: address(Env.impl.delegationManager()) + }) + }) + .append({ + to: Env.proxyAdmin(), + data: Encode.proxyAdmin.upgrade({ + proxy: address(Env.proxy.rewardsCoordinator()), + impl: address(Env.impl.rewardsCoordinator()) + }) + }); + + + return Encode.gnosisSafe.execTransaction({ + from: address(Env.timelockController()), + to: address(Env.multiSendCallOnly()), + op: Encode.Operation.DelegateCall, + data: Encode.multiSend(executorCalls) + }); + } + + function testScript() public virtual { + 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."); + } +} \ No newline at end of file diff --git a/script/releases/v1.0.3-slashing/3-execute.s.sol b/script/releases/v1.0.3-slashing/3-execute.s.sol new file mode 100644 index 000000000..cb0a82a1b --- /dev/null +++ b/script/releases/v1.0.3-slashing/3-execute.s.sol @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import "../Env.sol"; +import {Queue} from "./2-multisig.s.sol"; + +import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; + +contract Execute is Queue { + using Env for *; + + function _runAsMultisig() prank(Env.protocolCouncilMultisig()) internal override(Queue) { + 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(Queue){ + // 0. Deploy Impls + 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. Queue Upgrade + Queue._runAsMultisig(); + _unsafeResetHasPranked(); // reset hasPranked so we can use it again + + // 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 + _validateNewImplAddresses(true); + _validateProxyConstructors(); + _validateProxyDomainSeparators(); + } + + function _validateProxyDomainSeparators() internal view { + bytes32 zeroDomainSeparator = bytes32(0); + + assertFalse(Env.proxy.avsDirectory().domainSeparator() == zeroDomainSeparator, "avsD.domainSeparator is zero"); + assertFalse(Env.proxy.delegationManager().domainSeparator() == zeroDomainSeparator, "dm.domainSeparator is zero"); + assertFalse(Env.proxy.strategyManager().domainSeparator() == zeroDomainSeparator, "rc.domainSeparator is zero"); + } + + function _validateProxyConstructors() internal view { + AVSDirectory avsDirectory = Env.proxy.avsDirectory(); + assertTrue(avsDirectory.delegation() == Env.proxy.delegationManager(), "avsD.dm invalid"); + assertTrue(avsDirectory.pauserRegistry() == Env.impl.pauserRegistry(), "avsD.pR invalid"); + + 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"); + + 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"); + + StrategyManager strategyManager = Env.proxy.strategyManager(); + assertTrue(strategyManager.delegation() == Env.proxy.delegationManager(), "sm.dm invalid"); + assertTrue(strategyManager.pauserRegistry() == Env.impl.pauserRegistry(), "sm.pR invalid"); + } + + function _validateRC() internal view { + RewardsCoordinator rewards = Env.proxy.rewardsCoordinator(); + assertEq( + rewards.defaultOperatorSplitBips(), + Env.DEFAULT_SPLIT_BIPS(), + "expected defaultOperatorSplitBips" + ); + + uint256 pausedStatusAfter = rewards.paused(); + + assertEq( + Env.REWARDS_PAUSE_STATUS(), + pausedStatusAfter, + "expected paused status to be the same before and after initialization" + ); + + assertEq( + rewards.CALCULATION_INTERVAL_SECONDS(), + Env.CALCULATION_INTERVAL_SECONDS(), + "expected REWARDS_COORDINATOR_CALCULATION_INTERVAL_SECONDS" + ); + assertEq( + rewards.CALCULATION_INTERVAL_SECONDS(), + 1 days, + "expected REWARDS_COORDINATOR_CALCULATION_INTERVAL_SECONDS" + ); + assertGt( + rewards.CALCULATION_INTERVAL_SECONDS(), + 0, + "expected non-zero REWARDS_COORDINATOR_CALCULATION_INTERVAL_SECONDS" + ); + + assertEq(rewards.MAX_REWARDS_DURATION(), Env.MAX_REWARDS_DURATION()); + assertGt(rewards.MAX_REWARDS_DURATION(), 0); + + assertEq( + rewards.MAX_RETROACTIVE_LENGTH(), + Env.MAX_RETROACTIVE_LENGTH() + ); + assertGt(rewards.MAX_RETROACTIVE_LENGTH(), 0); + + assertEq(rewards.MAX_FUTURE_LENGTH(), Env.MAX_FUTURE_LENGTH()); + assertGt(rewards.MAX_FUTURE_LENGTH(), 0); + + assertEq( + rewards.GENESIS_REWARDS_TIMESTAMP(), + Env.GENESIS_REWARDS_TIMESTAMP() + ); + assertGt(rewards.GENESIS_REWARDS_TIMESTAMP(), 0); + } +} \ No newline at end of file diff --git a/script/releases/v1.0.3-slashing/upgrade.json b/script/releases/v1.0.3-slashing/upgrade.json new file mode 100644 index 000000000..69c459487 --- /dev/null +++ b/script/releases/v1.0.3-slashing/upgrade.json @@ -0,0 +1,19 @@ +{ + "name": "slashing-patch-RC-domainSeparator", + "from": "1.0.2", + "to": "1.0.3", + "phases": [ + { + "type": "eoa", + "filename": "1-eoa.s.sol" + }, + { + "type": "multisig", + "filename": "2-multisig.s.sol" + }, + { + "type": "multisig", + "filename": "3-execute.s.sol" + } + ] +} \ No newline at end of file