diff --git a/test/Timelock.t.sol b/test/Timelock.t.sol new file mode 100644 index 0000000..a3561f1 --- /dev/null +++ b/test/Timelock.t.sol @@ -0,0 +1,129 @@ +// 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); + } +}