From 6e0331b63d87ddf8f7cb0f2e3ee945e3d3609d7f Mon Sep 17 00:00:00 2001 From: johnnyonline Date: Tue, 11 Jun 2024 23:26:30 +0300 Subject: [PATCH 1/5] feat: timelock test --- .env.sample | 1 + test/scenarios/ScenarioBaseTest.sol | 3 +- test/scenarios/Timelock.t.sol | 114 ++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 test/scenarios/Timelock.t.sol diff --git a/.env.sample b/.env.sample index cfb5b837a..6c9ba61d0 100644 --- a/.env.sample +++ b/.env.sample @@ -1,3 +1,4 @@ +ETHEREUM_RPC_URL=YOUR_ETH_RPC_URL PRIVATE_KEY=YOUR_PRIVATE_KEY INFURA_PROJECT_ID=YOUR_INFURA_PROJECT_ID ETHERSCAN_API_KEY=YOUR_ETHERSCAN_API_KEY diff --git a/test/scenarios/ScenarioBaseTest.sol b/test/scenarios/ScenarioBaseTest.sol index 56d74069e..be8c5ab27 100644 --- a/test/scenarios/ScenarioBaseTest.sol +++ b/test/scenarios/ScenarioBaseTest.sol @@ -13,7 +13,6 @@ import {IDelegationManager} from "lib/eigenlayer-contracts/src/contracts/interfa import {IStakingNodesManager} from "src/interfaces/IStakingNodesManager.sol"; import {IRewardsDistributor} from "src/interfaces/IRewardsDistributor.sol"; import {IynETH} from "src/interfaces/IynETH.sol"; -import {Test} from "forge-std/Test.sol"; import {ynETH} from "src/ynETH.sol"; import {ynLSD} from "src/ynLSD.sol"; import {YieldNestOracle} from "src/YieldNestOracle.sol"; @@ -29,6 +28,8 @@ import {Utils} from "script/Utils.sol"; import {ActorAddresses} from "script/Actors.sol"; import {TestAssetUtils} from "test/utils/TestAssetUtils.sol"; +import "forge-std/Test.sol"; + contract ScenarioBaseTest is Test, Utils { // Utils diff --git a/test/scenarios/Timelock.t.sol b/test/scenarios/Timelock.t.sol new file mode 100644 index 000000000..508f60cf0 --- /dev/null +++ b/test/scenarios/Timelock.t.sol @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity ^0.8.24; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol"; + +import "./ScenarioBaseTest.sol"; + +contract TimelockTest is ScenarioBaseTest { + + event Upgraded(address indexed implementation); + + uint256 public constant DELAY = 3 days; + + address[] public proxyContracts; + + TimelockController public timelock; + + // ============================================================================================ + // Setup + // ============================================================================================ + + function setUp() public override { + vm.selectFork(vm.createFork(vm.envString("ETHEREUM_RPC_URL"))); + + ScenarioBaseTest.setUp(); + + proxyContracts = [ + address(yneth), + address(stakingNodesManager), + address(rewardsDistributor), + address(executionLayerReceiver), + address(consensusLayerReceiver) + ]; + + address[] memory _proposers = new address[](1); + _proposers[0] = actors.eoa.DEFAULT_SIGNER; + address[] memory _executors = new address[](1); + _executors[0] = actors.eoa.DEFAULT_SIGNER; + timelock = new TimelockController( + DELAY, + _proposers, + _executors, + address(0) // no admin + ); + } + + // ============================================================================================ + // Tests + // ============================================================================================ + + function testScheduleAndExecuteUpgrade() public { + _updateProxyAdminOwnersToTimelock(); + + // operation data + address _target = getTransparentUpgradeableProxyAdminAddress(address(yneth)); // proxy admin + address _implementation = getTransparentUpgradeableProxyImplementationAddress(address(yneth)); // implementation (not changed) + uint256 _value = 0; + bytes memory _data = abi.encodeWithSignature( + "upgradeAndCall(address,address,bytes)", + address(yneth), // proxy + _implementation, // implementation + "" // no data + ); + bytes32 _predecessor = bytes32(0); + bytes32 _salt = bytes32(0); + uint256 _delay = 3 days; + + vm.startPrank(actors.eoa.DEFAULT_SIGNER); + + // schedule + timelock.schedule( + _target, + _value, + _data, + _predecessor, + _salt, + _delay + ); + + // skip delay duration + skip(DELAY); + + vm.expectEmit(address(yneth)); + emit Upgraded(_implementation); + + // execute + timelock.execute( + _target, + _value, + _data, + _predecessor, + _salt + ); + + vm.stopPrank(); + } + + // ============================================================================================ + // Internal helpers + // ============================================================================================ + + function _updateProxyAdminOwnersToTimelock() internal { + for (uint256 i = 0; i < proxyContracts.length; i++) { + + // get proxy admin + Ownable _proxyAdmin = Ownable(getTransparentUpgradeableProxyAdminAddress(address(proxyContracts[i]))); + + // transfer ownership to timelock + vm.prank(_proxyAdmin.owner()); + _proxyAdmin.transferOwnership(address(timelock)); + } + } +} \ No newline at end of file From a214b63b42bb2853c74e9fb9b266e16ea1b66112 Mon Sep 17 00:00:00 2001 From: johnnyonline Date: Wed, 12 Jun 2024 11:07:55 +0300 Subject: [PATCH 2/5] fix: ci --- test/scenarios/Timelock.t.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/scenarios/Timelock.t.sol b/test/scenarios/Timelock.t.sol index 508f60cf0..62cd27674 100644 --- a/test/scenarios/Timelock.t.sol +++ b/test/scenarios/Timelock.t.sol @@ -21,8 +21,6 @@ contract TimelockTest is ScenarioBaseTest { // ============================================================================================ function setUp() public override { - vm.selectFork(vm.createFork(vm.envString("ETHEREUM_RPC_URL"))); - ScenarioBaseTest.setUp(); proxyContracts = [ From 378072c5b3d52da552acbe78760148150eefeb45 Mon Sep 17 00:00:00 2001 From: johnnyonline Date: Tue, 18 Jun 2024 19:31:08 +0300 Subject: [PATCH 3/5] feat: demonstrate how you replace the timelock with another arbitrary admin --- test/scenarios/Timelock.t.sol | 55 +++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/test/scenarios/Timelock.t.sol b/test/scenarios/Timelock.t.sol index 62cd27674..ae8a91800 100644 --- a/test/scenarios/Timelock.t.sol +++ b/test/scenarios/Timelock.t.sol @@ -9,6 +9,7 @@ import "./ScenarioBaseTest.sol"; contract TimelockTest is ScenarioBaseTest { event Upgraded(address indexed implementation); + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); uint256 public constant DELAY = 3 days; @@ -94,6 +95,60 @@ contract TimelockTest is ScenarioBaseTest { vm.stopPrank(); } + // @note: change the owner of the contract from the timelock to the default signer + function testSwapTimelockOwnership() public { + _updateProxyAdminOwnersToTimelock(); + + // operation data + address _newOwner = actors.eoa.DEFAULT_SIGNER; + address _target = getTransparentUpgradeableProxyAdminAddress(address(yneth)); // proxy admin + assertEq(Ownable(_target).owner(), address(timelock), "testSwapTimelockOwnership: E0"); // check current owner + + uint256 _value = 0; + bytes memory _data = abi.encodeWithSignature( + "transferOwnership(address)", + _newOwner, // new owner + "" // no data + ); + bytes32 _predecessor = bytes32(0); + bytes32 _salt = bytes32(0); + uint256 _delay = 3 days; + + vm.startPrank(actors.eoa.DEFAULT_SIGNER); + + // schedule + timelock.schedule( + _target, + _value, + _data, + _predecessor, + _salt, + _delay + ); + + // skip delay duration + skip(DELAY); + + vm.expectEmit(address(_target)); + emit OwnershipTransferred( + address(timelock), // oldOwner + _newOwner // newOwner + ); + + // execute + timelock.execute( + _target, + _value, + _data, + _predecessor, + _salt + ); + + vm.stopPrank(); + + assertEq(Ownable(_target).owner(), _newOwner, "testSwapTimelockOwnership: E1"); + } + // ============================================================================================ // Internal helpers // ============================================================================================ From 1b842eb99eab6bd1fe8de2854f5af8abb087a257 Mon Sep 17 00:00:00 2001 From: johnnyonline Date: Wed, 19 Jun 2024 15:21:23 +0300 Subject: [PATCH 4/5] feat: deployment script --- script/DeployTimelock.s.sol | 35 +++++++++++++++++++++++++++++++++++ test/scenarios/Timelock.t.sol | 21 ++++++--------------- 2 files changed, 41 insertions(+), 15 deletions(-) create mode 100644 script/DeployTimelock.s.sol diff --git a/script/DeployTimelock.s.sol b/script/DeployTimelock.s.sol new file mode 100644 index 000000000..ab6e5d6f2 --- /dev/null +++ b/script/DeployTimelock.s.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: BSD 3-Clause License +pragma solidity ^0.8.24; + +import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol"; + +import {ContractAddresses} from "script/ContractAddresses.sol"; +import {BaseScript} from "script/BaseScript.s.sol"; +import {ActorAddresses} from "script/Actors.sol"; +import {console} from "lib/forge-std/src/console.sol"; + +contract DeployTimelock is BaseScript { + + TimelockController public timelock; + + function run() public { + + ActorAddresses.Actors memory _actors = getActors(); + + vm.startBroadcast(vm.envUint("PRIVATE_KEY")); + + address[] memory _proposers = new address[](2); + _proposers[0] = _actors.admin.ADMIN; + _proposers[1] = _actors.eoa.DEFAULT_SIGNER; + address[] memory _executors = new address[](1); + _executors[0] = _actors.admin.ADMIN; + timelock = new TimelockController( + 3 days, // delay + _proposers, + _executors, + _actors.admin.ADMIN // admin + ); + + vm.stopBroadcast(); + } +} \ No newline at end of file diff --git a/test/scenarios/Timelock.t.sol b/test/scenarios/Timelock.t.sol index ae8a91800..5931433ac 100644 --- a/test/scenarios/Timelock.t.sol +++ b/test/scenarios/Timelock.t.sol @@ -4,9 +4,11 @@ pragma solidity ^0.8.24; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol"; +import {DeployTimelock} from "script/DeployTimelock.s.sol"; + import "./ScenarioBaseTest.sol"; -contract TimelockTest is ScenarioBaseTest { +contract TimelockTest is ScenarioBaseTest, DeployTimelock { event Upgraded(address indexed implementation); event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); @@ -15,8 +17,6 @@ contract TimelockTest is ScenarioBaseTest { address[] public proxyContracts; - TimelockController public timelock; - // ============================================================================================ // Setup // ============================================================================================ @@ -32,16 +32,7 @@ contract TimelockTest is ScenarioBaseTest { address(consensusLayerReceiver) ]; - address[] memory _proposers = new address[](1); - _proposers[0] = actors.eoa.DEFAULT_SIGNER; - address[] memory _executors = new address[](1); - _executors[0] = actors.eoa.DEFAULT_SIGNER; - timelock = new TimelockController( - DELAY, - _proposers, - _executors, - address(0) // no admin - ); + DeployTimelock.run(); } // ============================================================================================ @@ -65,7 +56,7 @@ contract TimelockTest is ScenarioBaseTest { bytes32 _salt = bytes32(0); uint256 _delay = 3 days; - vm.startPrank(actors.eoa.DEFAULT_SIGNER); + vm.startPrank(actors.admin.ADMIN); // schedule timelock.schedule( @@ -114,7 +105,7 @@ contract TimelockTest is ScenarioBaseTest { bytes32 _salt = bytes32(0); uint256 _delay = 3 days; - vm.startPrank(actors.eoa.DEFAULT_SIGNER); + vm.startPrank(actors.admin.ADMIN); // schedule timelock.schedule( From ed96f7693f868d064001f28440ffe3532763c00d Mon Sep 17 00:00:00 2001 From: johnnyonline Date: Thu, 20 Jun 2024 00:01:24 +0300 Subject: [PATCH 5/5] fix: ci --- script/DeployTimelock.s.sol | 5 ++++- test/scenarios/Timelock.t.sol | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/script/DeployTimelock.s.sol b/script/DeployTimelock.s.sol index ab6e5d6f2..e3e648e1c 100644 --- a/script/DeployTimelock.s.sol +++ b/script/DeployTimelock.s.sol @@ -10,13 +10,16 @@ import {console} from "lib/forge-std/src/console.sol"; contract DeployTimelock is BaseScript { + uint256 public privateKey; // dev: assigned in test setup + TimelockController public timelock; function run() public { ActorAddresses.Actors memory _actors = getActors(); - vm.startBroadcast(vm.envUint("PRIVATE_KEY")); + privateKey == 0 ? vm.envUint("PRIVATE_KEY") : privateKey; + vm.startBroadcast(privateKey); address[] memory _proposers = new address[](2); _proposers[0] = _actors.admin.ADMIN; diff --git a/test/scenarios/Timelock.t.sol b/test/scenarios/Timelock.t.sol index 5931433ac..43958adc2 100644 --- a/test/scenarios/Timelock.t.sol +++ b/test/scenarios/Timelock.t.sol @@ -32,6 +32,7 @@ contract TimelockTest is ScenarioBaseTest, DeployTimelock { address(consensusLayerReceiver) ]; + (, privateKey) = makeAddrAndKey("deployer"); DeployTimelock.run(); }