// SPDX-License-Identifier: MIT pragma solidity 0.8.36; import {Test} from "forge-std/Test.sol"; import {Timelock} from "../src/Timelock.sol"; contract Target { address public immutable timelock; uint256 public value; constructor(address timelockAddress) { timelock = timelockAddress; } function setValue(uint256 newValue) external { require(msg.sender == timelock, "Target: FORBIDDEN"); value = newValue; } } contract TimelockTest is Test { Timelock private timelock; Target private target; uint256 private constant delay = 2 days; function setUp() public { vm.warp(1_000_000); timelock = new Timelock(address(this), delay); target = new Target(address(timelock)); } function queueSetValue(uint256 newValue, uint256 eta) private returns (bytes32) { return timelock.queueTransaction(address(target), 0, "setValue(uint256)", abi.encode(newValue), eta); } function testConstructorRejectsShortDelay() public { vm.expectRevert(bytes("Timelock: DELAY_SHORT")); new Timelock(address(this), 1 hours); } function testQueueMarksTransaction() public { uint256 eta = block.timestamp + delay; bytes32 txHash = queueSetValue(42, eta); assertTrue(timelock.queuedTransactions(txHash)); } function testQueueRevertsWhenEtaTooSoon() public { vm.expectRevert(bytes("Timelock: ETA_TOO_SOON")); queueSetValue(42, block.timestamp + 1 hours); } function testQueueOnlyAdmin() public { vm.prank(address(0xBAD)); vm.expectRevert(bytes("Timelock: ONLY_ADMIN")); queueSetValue(42, block.timestamp + delay); } function testExecuteRevertsBeforeEta() public { uint256 eta = block.timestamp + delay; queueSetValue(42, eta); vm.expectRevert(bytes("Timelock: NOT_READY")); timelock.executeTransaction(address(target), 0, "setValue(uint256)", abi.encode(42), eta); } function testExecuteAppliesEffectAfterDelay() public { uint256 eta = block.timestamp + delay; queueSetValue(42, eta); vm.warp(eta); timelock.executeTransaction(address(target), 0, "setValue(uint256)", abi.encode(42), eta); assertEq(target.value(), 42); } function testExecuteRevertsIfNotQueued() public { uint256 eta = block.timestamp + delay; vm.warp(eta); vm.expectRevert(bytes("Timelock: NOT_QUEUED")); timelock.executeTransaction(address(target), 0, "setValue(uint256)", abi.encode(42), eta); } function testCancelPreventsExecution() public { uint256 eta = block.timestamp + delay; queueSetValue(42, eta); timelock.cancelTransaction(address(target), 0, "setValue(uint256)", abi.encode(42), eta); vm.warp(eta); vm.expectRevert(bytes("Timelock: NOT_QUEUED")); timelock.executeTransaction(address(target), 0, "setValue(uint256)", abi.encode(42), eta); } function testExecuteRevertsAfterGracePeriod() public { uint256 eta = block.timestamp + delay; queueSetValue(42, eta); vm.warp(eta + 14 days + 1); vm.expectRevert(bytes("Timelock: STALE")); timelock.executeTransaction(address(target), 0, "setValue(uint256)", abi.encode(42), eta); } function testExecuteBubblesTargetRevert() public { uint256 eta = block.timestamp + delay; bytes memory badData = abi.encodeWithSignature("setValue(uint256)", uint256(7)); timelock.queueTransaction(address(target), 0, "", badData, eta); vm.warp(eta); vm.prank(address(0xBAD)); vm.expectRevert(bytes("Timelock: ONLY_ADMIN")); timelock.executeTransaction(address(target), 0, "", badData, eta); } function testSetDelayOnlyViaSelfCall() public { vm.expectRevert(bytes("Timelock: ONLY_SELF")); timelock.setDelay(3 days); } function testTimelockCanRescheduleItselfThroughQueue() public { uint256 eta = block.timestamp + delay; timelock.queueTransaction(address(timelock), 0, "setDelay(uint256)", abi.encode(uint256(5 days)), eta); vm.warp(eta); timelock.executeTransaction(address(timelock), 0, "setDelay(uint256)", abi.encode(uint256(5 days)), eta); assertEq(timelock.delay(), 5 days); } function testFuzzExecuteNeverSucceedsBeforeEta(uint256 wait, uint256 newValue) public { uint256 eta = block.timestamp + delay; queueSetValue(newValue, eta); wait = bound(wait, 0, delay - 1); vm.warp(block.timestamp + wait); vm.expectRevert(bytes("Timelock: NOT_READY")); timelock.executeTransaction(address(target), 0, "setValue(uint256)", abi.encode(newValue), eta); } }