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/script/DeployTimelock.s.sol b/script/DeployTimelock.s.sol new file mode 100644 index 000000000..e3e648e1c --- /dev/null +++ b/script/DeployTimelock.s.sol @@ -0,0 +1,38 @@ +// 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 { + + uint256 public privateKey; // dev: assigned in test setup + + TimelockController public timelock; + + function run() public { + + ActorAddresses.Actors memory _actors = getActors(); + + privateKey == 0 ? vm.envUint("PRIVATE_KEY") : privateKey; + vm.startBroadcast(privateKey); + + 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/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..43958adc2 --- /dev/null +++ b/test/scenarios/Timelock.t.sol @@ -0,0 +1,159 @@ +// 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 {DeployTimelock} from "script/DeployTimelock.s.sol"; + +import "./ScenarioBaseTest.sol"; + +contract TimelockTest is ScenarioBaseTest, DeployTimelock { + + event Upgraded(address indexed implementation); + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + uint256 public constant DELAY = 3 days; + + address[] public proxyContracts; + + // ============================================================================================ + // Setup + // ============================================================================================ + + function setUp() public override { + ScenarioBaseTest.setUp(); + + proxyContracts = [ + address(yneth), + address(stakingNodesManager), + address(rewardsDistributor), + address(executionLayerReceiver), + address(consensusLayerReceiver) + ]; + + (, privateKey) = makeAddrAndKey("deployer"); + DeployTimelock.run(); + } + + // ============================================================================================ + // 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.admin.ADMIN); + + // 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(); + } + + // @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.admin.ADMIN); + + // 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 + // ============================================================================================ + + 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