diff --git a/contracts/script/GenerateAlloc.s.sol b/contracts/script/GenerateAlloc.s.sol index dbdf2354..a0f28d0b 100644 --- a/contracts/script/GenerateAlloc.s.sol +++ b/contracts/script/GenerateAlloc.s.sol @@ -12,6 +12,7 @@ import { IPTokenStaking } from "../src/protocol/IPTokenStaking.sol"; import { UpgradeEntrypoint } from "../src/protocol/UpgradeEntrypoint.sol"; import { UBIPool } from "../src/protocol/UBIPool.sol"; +import { ChainIds } from "./utils/ChainIds.sol"; import { EIP1967Helper } from "./utils/EIP1967Helper.sol"; import { InitializableHelper } from "./utils/InitializableHelper.sol"; import { Predeploys } from "../src/libraries/Predeploys.sol"; @@ -49,7 +50,6 @@ contract GenerateAlloc is Script { string internal dumpPath = getDumpPath(); bool public saveState = true; - uint256 public constant STORY_CHAIN_ID = 1415; // Story mainnet chain ID. TBD // Optionally allocate 10k test accounts for devnets/testnets bool private constant ALLOCATE_10K_TEST_ACCOUNTS = false; // Optionally keep the timelock admin role for testnets @@ -57,13 +57,13 @@ contract GenerateAlloc is Script { /// @notice this call should only be available from Test.sol, for speed function disableStateDump() external { - require(block.chainid == 31337, "Only for local tests"); + require(block.chainid == ChainIds.FOUNDRY, "Only for local tests"); saveState = false; } /// @dev this call should only be available from Test.sol function setAdminAddresses(address protocol, address executor, address guardian) external { - require(block.chainid == 31337, "Only for local tests"); + require(block.chainid == ChainIds.FOUNDRY, "Only for local tests"); protocolAdmin = protocol; timelockExecutor = executor; timelockGuardian = guardian; @@ -71,18 +71,20 @@ contract GenerateAlloc is Script { /// @notice path where alloc file will be stored function getDumpPath() internal view returns (string memory) { - if (block.chainid == 1513) { + if (block.chainid == ChainIds.ILIAD) { return "./iliad-alloc.json"; - } else if (block.chainid == 1512) { + } else if (block.chainid == ChainIds.MININET) { return "./mininet-alloc.json"; - } else if (block.chainid == 1315) { + } else if (block.chainid == ChainIds.ODYSSEY_DEVNET) { return "./odyssey-devnet-alloc.json"; - } else if (block.chainid == 1516) { + } else if (block.chainid == ChainIds.ODYSSEY_TESTNET) { return "./odyssey-testnet-alloc.json"; - } else if (block.chainid == 31337) { + } else if (block.chainid == ChainIds.LOCAL) { return "./local-alloc.json"; - } else if (block.chainid == STORY_CHAIN_ID) { - return "./story-alloc.json"; + } else if (block.chainid == ChainIds.FOUNDRY) { + return "./foundry-alloc.json"; + } else if (block.chainid == ChainIds.STORY_MAINNET) { + return "./mainnet-alloc.json"; } else { revert("Unsupported chain id"); } @@ -90,46 +92,27 @@ contract GenerateAlloc is Script { /// @notice Get the minimum delay for the timelock function getTimelockMinDelay() internal view returns (uint256) { - if (block.chainid == 1513) { + if (block.chainid == ChainIds.ILIAD) { // Iliad return 1 days; - } else if (block.chainid == 1512) { + } else if (block.chainid == ChainIds.MININET) { // Mininet return 10 seconds; - } else if (block.chainid == 1315) { + } else if (block.chainid == ChainIds.ODYSSEY_DEVNET) { // Odyssey devnet return 10 seconds; - } else if (block.chainid == 1516) { + } else if (block.chainid == ChainIds.ODYSSEY_TESTNET) { // Odyssey testnet return 1 days; - } else if (block.chainid == 31337) { + } else if (block.chainid == ChainIds.LOCAL) { // Local return 10 seconds; - } else { - revert("Unsupported chain id"); - } - } - - /// @notice Gets the proposers, executors and canceller for the timelocks - function getTimelockControllers() - internal - view - returns (address[] memory proposers, address[] memory executors, address canceller) - { - proposers = new address[](1); - executors = new address[](1); - if (block.chainid == 1513) { - // Iliad - proposers[0] = protocolAdmin; - executors[0] = protocolAdmin; - canceller = protocolAdmin; - return (proposers, executors, canceller); - } else if (block.chainid == 1512 || block.chainid == 1315 || block.chainid == 31337 || block.chainid == 1516) { - // Mininet, Odyssey devnet, Local, Odyssey testnet - proposers[0] = protocolAdmin; - executors[0] = timelockExecutor; - canceller = timelockGuardian; - return (proposers, executors, canceller); + } else if (block.chainid == ChainIds.FOUNDRY) { + // Foundry + return 10 seconds; + } else if (block.chainid == ChainIds.STORY_MAINNET) { + // Mainnet + return 2 days; } else { revert("Unsupported chain id"); } @@ -146,14 +129,16 @@ contract GenerateAlloc is Script { if (timelockExecutor == address(0)) { timelockExecutor = vm.envAddress("TIMELOCK_EXECUTOR_ADDRESS"); } - require(timelockExecutor != address(0), "executor not set"); + if (timelockExecutor == address(0)) { + console2.log("TimelockExecutor not set, executing timelock operations is public"); + } if (timelockGuardian == address(0)) { timelockGuardian = vm.envAddress("TIMELOCK_GUARDIAN_ADDRESS"); } require(timelockGuardian != address(0), "canceller not set"); - if (block.chainid == STORY_CHAIN_ID) { + if (block.chainid == ChainIds.STORY_MAINNET) { require(!KEEP_TIMELOCK_ADMIN_ROLE, "Timelock admin role not allowed on mainnet"); } else { console2.log("Will timelock admin role be assigned?", KEEP_TIMELOCK_ADMIN_ROLE); @@ -214,28 +199,37 @@ contract GenerateAlloc is Script { /// @notice Deploy TimelockController to manage upgrades and admin actions /// @dev this is a deterministic deployment, not a predeploy (won't show in genesis file). function deployTimelock() internal { - // We deploy this with Create3 because we can't set storage variables in constructor with vm.etch - + // WARNING: Make sure protocolAdmin and timelockGuardian are multisigs on mainnet uint256 minDelay = getTimelockMinDelay(); - (address[] memory proposers, address[] memory executors, address canceller) = getTimelockControllers(); + address[] memory proposers = new address[](1); + proposers[0] = protocolAdmin; + address[] memory executors = new address[](1); + executors[0] = timelockExecutor; + address canceller = timelockGuardian; bytes memory creationCode = abi.encodePacked( type(TimelockController).creationCode, - abi.encode(minDelay, proposers, executors, deployer) + abi.encode(minDelay, proposers, executors, protocolAdmin) ); bytes32 salt = keccak256("STORY_TIMELOCK_CONTROLLER"); + // We deploy this with Create3 because we can't set storage variables in constructor with vm.etch timelock = Create3(Predeploys.Create3).deploy(salt, creationCode); - + vm.stopPrank(); + bytes32 cancellerRole = TimelockController(payable(timelock)).CANCELLER_ROLE(); + vm.prank(protocolAdmin); TimelockController(payable(timelock)).grantRole( - TimelockController(payable(timelock)).CANCELLER_ROLE(), + cancellerRole, canceller ); if (!KEEP_TIMELOCK_ADMIN_ROLE) { + bytes32 adminRole = TimelockController(payable(timelock)).DEFAULT_ADMIN_ROLE(); TimelockController(payable(timelock)).renounceRole( - TimelockController(payable(timelock)).DEFAULT_ADMIN_ROLE(), - deployer + adminRole, + protocolAdmin ); } + vm.stopPrank(); + vm.startPrank(deployer); console2.log("TimelockController deployed at:", timelock); } @@ -410,9 +404,9 @@ contract GenerateAlloc is Script { // Story's IPGraph precompile vm.deal(0x0000000000000000000000000000000000000101, 1); // Allocation - if (block.chainid == STORY_CHAIN_ID) { + if (block.chainid == ChainIds.STORY_MAINNET) { // TBD - } else if (block.chainid == 1315) { + } else if (block.chainid == ChainIds.ODYSSEY_DEVNET) { // Odyssey devnet alloc vm.deal(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266, 100000000 ether); vm.deal(0xf398C12A45Bc409b6C652E25bb0a3e702492A4ab, 100000000 ether); @@ -422,7 +416,7 @@ contract GenerateAlloc is Script { vm.deal(0x00FCeC044cD73e8eC6Ad771556859b00C9011111, 100000000 ether); vm.deal(0xb5350B7CaE94C2bF6B2b56Ef6A06cC1153900000, 100000000 ether); vm.deal(0x13919a0d8603c35DAC923f92D7E4e1D55e993898, 100000000 ether); - } else if (block.chainid == 1516) { + } else if (block.chainid == ChainIds.ODYSSEY_TESTNET) { // Odyssey testnet alloc vm.deal(0x5687400189B13551137e330F7ae081142EdfD866, 200000000 ether); vm.deal(0x56A26642ad963D3542DdAe4d8fdECC396153c2f6, 200000000 ether); @@ -443,8 +437,7 @@ contract GenerateAlloc is Script { vm.deal(0x13919a0d8603c35DAC923f92D7E4e1D55e993898, 100000000 ether); vm.deal(0x64a2fdc6f7CD8AA42e0bb59bf80bC47bFFbe4a73, 100000000 ether); } - // Test for big allocations - if (ALLOCATE_10K_TEST_ACCOUNTS && block.chainid != STORY_CHAIN_ID) { + if (ALLOCATE_10K_TEST_ACCOUNTS && block.chainid != ChainIds.STORY_MAINNET) { setTestAllocations(); } } diff --git a/contracts/script/utils/ChainIds.sol b/contracts/script/utils/ChainIds.sol new file mode 100644 index 00000000..856a9bcb --- /dev/null +++ b/contracts/script/utils/ChainIds.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +library ChainIds { + uint256 public constant STORY_MAINNET = 1415; // TBD + uint256 public constant ILIAD = 1513; + uint256 public constant MININET = 1512; + uint256 public constant ODYSSEY_DEVNET = 1315; + uint256 public constant ODYSSEY_TESTNET = 1516; + uint256 public constant LOCAL = 1511; + uint256 public constant FOUNDRY = 31337; +} diff --git a/contracts/test/timelock/Timelock.t.sol b/contracts/test/timelock/Timelock.t.sol index cbb62a40..91b26ab2 100644 --- a/contracts/test/timelock/Timelock.t.sol +++ b/contracts/test/timelock/Timelock.t.sol @@ -102,4 +102,35 @@ contract TimelockTest is Test { timelock.execute(target, value, data3, id2, salt3); assertEq(ipTokenStaking.fee(), 4 ether); } + + function testTimelockMinDelay() public { + address target = address(ipTokenStaking); + uint256 value = 0; + bytes memory data1 = abi.encodeWithSelector(IPTokenStaking.setFee.selector, 2 ether); + bytes32 salt1 = keccak256("SALT_1"); + uint256 minDelay = timelock.getMinDelay(); + + // Scheduling under the min delay should fail + vm.expectRevert( + abi.encodeWithSelector(TimelockController.TimelockInsufficientDelay.selector, minDelay - 1, minDelay) + ); + vm.prank(admin); + timelock.schedule(target, value, data1, bytes32(0), salt1, minDelay - 1); + + // Scheduling over the min delay should succeed + vm.prank(admin); + timelock.schedule(target, value, data1, bytes32(0), salt1, minDelay + 1 minutes); + + // Should revert if we try to execute before scheduled time + vm.warp(block.timestamp + minDelay); + vm.expectRevert(); + vm.prank(executor); + timelock.execute(target, value, data1, bytes32(0), salt1); + + // Should succeed if we wait for the delay to pass + vm.warp(block.timestamp + minDelay + 1 minutes); + vm.prank(executor); + timelock.execute(target, value, data1, bytes32(0), salt1); + assertEq(ipTokenStaking.fee(), 2 ether); + } } diff --git a/contracts/test/upgrades/PredeployUpgrades.t.sol b/contracts/test/upgrades/PredeployUpgrades.t.sol index a4afd51c..196c2236 100644 --- a/contracts/test/upgrades/PredeployUpgrades.t.sol +++ b/contracts/test/upgrades/PredeployUpgrades.t.sol @@ -191,4 +191,28 @@ contract PredeployUpgrades is Test { expectRevertTimelocked(address(proxyAdmin), upgradeAction, expectedReason); } } + + function testUpgradeLastPredeploy() public { + address newImpl = address(new InitialImplementation()); + address lastPredeploy = address(uint160(Predeploys.Namespace) + uint160(Predeploys.NamespaceSize)); + ProxyAdmin proxyAdmin = ProxyAdmin(EIP1967Helper.getAdmin(lastPredeploy)); + assertEq(proxyAdmin.owner(), address(timelock)); + + // Upgrade last predeploy to new implementation + performTimelocked( + address(proxyAdmin), + abi.encodeWithSelector( + ProxyAdmin.upgradeAndCall.selector, + ITransparentUpgradeableProxy(lastPredeploy), + newImpl, + "" + ) + ); + assertEq(EIP1967Helper.getImplementation(lastPredeploy), newImpl, "Last predeploy not upgraded"); + assertEq( + keccak256(abi.encode(InitialImplementation(lastPredeploy).foo())), + keccak256(abi.encode("bar")), + "Upgraded to wrong iface" + ); + } }