diff --git a/packages/contracts-bedrock/interfaces/safe/ITimelockGuard.sol b/packages/contracts-bedrock/interfaces/safe/ITimelockGuard.sol new file mode 100644 index 00000000000..92bbabbda97 --- /dev/null +++ b/packages/contracts-bedrock/interfaces/safe/ITimelockGuard.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Enum } from "safe-contracts/common/Enum.sol"; +import { ISemver } from "interfaces/universal/ISemver.sol"; +import { Guard as IGuard } from "safe-contracts/base/GuardManager.sol"; + + +/// @title ITimelockGuard +/// @notice Interface for the TimelockGuard Safe guard. +interface ITimelockGuard is IGuard, ISemver { + // Errors + error TimelockGuard_GuardNotConfigured(); + error TimelockGuard_GuardNotEnabled(); + error TimelockGuard_GuardStillEnabled(); + error TimelockGuard_InvalidTimelockDelay(); + + // Events + event GuardCleared(address indexed safe); + event GuardConfigured(address indexed safe, uint256 timelockDelay); + + // Views + function version() external view returns (string memory); + + function viewTimelockGuardConfiguration(address _safe) external view returns (uint256); + + function cancellationThreshold(address _safe) external view returns (uint256 cancellationThreshold_); + + function safeCancellationThreshold(address) external view returns (uint256); + + function safeConfigs(address) + external + view + returns (uint256 timelockDelay); + + // Admin + function configureTimelockGuard(uint256 _timelockDelay) external; + + function clearTimelockGuard() external; + + // Scheduling API (placeholders until fully implemented in the guard) + function scheduleTransaction( + address _safe, + address _to, + uint256 _value, + bytes memory _data, + Enum.Operation _operation, + uint256 _safeTxGas, + uint256 _baseGas, + uint256 _gasPrice, + address _gasToken, + address payable _refundReceiver, + bytes memory _signatures + ) + external + pure; + + function checkPendingTransactions(address _safe) external pure returns (bytes32[] memory pendingTxs_); + + function rejectTransaction(address _safe, bytes32 _txHash) external pure; + + function rejectTransactionWithSignature(address _safe, bytes32 _txHash, bytes memory _signatures) external pure; + + function cancelTransaction(address _safe, bytes32 _txHash) external pure; +} + diff --git a/packages/contracts-bedrock/snapshots/abi/TimelockGuard.json b/packages/contracts-bedrock/snapshots/abi/TimelockGuard.json new file mode 100644 index 00000000000..745fdf6ec13 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/abi/TimelockGuard.json @@ -0,0 +1,385 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "cancelTransaction", + "outputs": [], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_safe", + "type": "address" + } + ], + "name": "cancellationThreshold", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + }, + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "name": "checkAfterExecution", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "checkPendingTransactions", + "outputs": [ + { + "internalType": "bytes32[]", + "name": "", + "type": "bytes32[]" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + }, + { + "internalType": "enum Enum.Operation", + "name": "", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address payable", + "name": "", + "type": "address" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "checkTransaction", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "clearTimelockGuard", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_timelockDelay", + "type": "uint256" + } + ], + "name": "configureTimelockGuard", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "rejectTransaction", + "outputs": [], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "rejectTransactionWithSignature", + "outputs": [], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "safeCancellationThreshold", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "safeConfigs", + "outputs": [ + { + "internalType": "uint256", + "name": "timelockDelay", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + }, + { + "internalType": "enum Enum.Operation", + "name": "", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address payable", + "name": "", + "type": "address" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "scheduleTransaction", + "outputs": [], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_safe", + "type": "address" + } + ], + "name": "viewTimelockGuardConfiguration", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "safe", + "type": "address" + } + ], + "name": "GuardCleared", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "safe", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timelockDelay", + "type": "uint256" + } + ], + "name": "GuardConfigured", + "type": "event" + }, + { + "inputs": [], + "name": "TimelockGuard_GuardNotConfigured", + "type": "error" + }, + { + "inputs": [], + "name": "TimelockGuard_GuardNotEnabled", + "type": "error" + }, + { + "inputs": [], + "name": "TimelockGuard_GuardStillEnabled", + "type": "error" + }, + { + "inputs": [], + "name": "TimelockGuard_InvalidTimelockDelay", + "type": "error" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 69288d2fdbc..f55b894dfe8 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -207,6 +207,10 @@ "initCodeHash": "0xde3b3273aa37604048b5fa228b90f3b05997db613dfcda45061545a669b2476a", "sourceCodeHash": "0x918965e52bbd358ac827ebe35998f5d8fa5ca77d8eb9ab8986b44181b9aaa48a" }, + "src/safe/TimelockGuard.sol:TimelockGuard": { + "initCodeHash": "0x5bc2ee2e57f8e4d4713a232a8427997398e1d9cfb26d8afafcccf228820972a6", + "sourceCodeHash": "0x2f23c80216ea80843414d6bcab10237964a8039e4d712c7cc3e65d57278067df" + }, "src/universal/OptimismMintableERC20.sol:OptimismMintableERC20": { "initCodeHash": "0xc3289416829b252c830ad7d389a430986a7404df4fe0be37cb19e1c40907f047", "sourceCodeHash": "0xf5e29dd5c750ea935c7281ec916ba5277f5610a0a9e984e53ae5d5245b3cf2f4" @@ -231,4 +235,4 @@ "initCodeHash": "0x2bfce526f82622288333d53ca3f43a0a94306ba1bab99241daa845f8f4b18bd4", "sourceCodeHash": "0xf49d7b0187912a6bb67926a3222ae51121e9239495213c975b3b4b217ee57a1b" } -} \ No newline at end of file +} diff --git a/packages/contracts-bedrock/snapshots/storageLayout/TimelockGuard.json b/packages/contracts-bedrock/snapshots/storageLayout/TimelockGuard.json new file mode 100644 index 00000000000..a8c3b0cb554 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/storageLayout/TimelockGuard.json @@ -0,0 +1,16 @@ +[ + { + "bytes": "32", + "label": "safeConfigs", + "offset": 0, + "slot": "0", + "type": "mapping(address => struct TimelockGuard.GuardConfig)" + }, + { + "bytes": "32", + "label": "safeCancellationThreshold", + "offset": 0, + "slot": "1", + "type": "mapping(address => uint256)" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/safe/TimelockGuard.sol b/packages/contracts-bedrock/src/safe/TimelockGuard.sol new file mode 100644 index 00000000000..f87ba843dbc --- /dev/null +++ b/packages/contracts-bedrock/src/safe/TimelockGuard.sol @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Safe +import { GnosisSafe as Safe } from "safe-contracts/GnosisSafe.sol"; +import { Enum } from "safe-contracts/common/Enum.sol"; +import { GuardManager, Guard as IGuard } from "safe-contracts/base/GuardManager.sol"; + +// Interfaces +import { ISemver } from "interfaces/universal/ISemver.sol"; + +/// @title TimelockGuard +/// @notice This guard provides timelock functionality for Safe transactions +/// @dev This is a singleton contract. To use it: +/// 1. The Safe must first enable this guard using GuardManager.setGuard() +/// 2. The Safe must then configure the guard by calling configureTimelockGuard() +contract TimelockGuard is IGuard, ISemver { + /// @notice Configuration for a Safe's timelock guard + struct GuardConfig { + uint256 timelockDelay; + } + + /// @notice Mapping from Safe address to its guard configuration + mapping(address => GuardConfig) public safeConfigs; + + /// @notice Mapping from Safe address to its current cancellation threshold + mapping(address => uint256) public safeCancellationThreshold; + + /// @notice Error for when guard is not enabled for the Safe + error TimelockGuard_GuardNotEnabled(); + + /// @notice Error for when Safe is not configured for this guard + error TimelockGuard_GuardNotConfigured(); + + /// @notice Error for when attempt to clear guard while it is still enabled for the Safe + error TimelockGuard_GuardStillEnabled(); + + /// @notice Error for invalid timelock delay + error TimelockGuard_InvalidTimelockDelay(); + + /// @notice Emitted when a Safe configures the guard + event GuardConfigured(address indexed safe, uint256 timelockDelay); + + /// @notice Emitted when a Safe clears the guard configuration + event GuardCleared(address indexed safe); + + /// @notice Semantic version. + /// @custom:semver 1.0.0 + string public constant version = "1.0.0"; + + /// @notice Returns the timelock delay for a given Safe + /// @dev MUST never revert + /// @param _safe The Safe address to query + /// @return The timelock delay in seconds + function viewTimelockGuardConfiguration(address _safe) public view returns (uint256) { + return safeConfigs[_safe].timelockDelay; + } + + /// @notice Configure the contract as a timelock guard by setting the timelock delay + /// @dev MUST allow an arbitrary number of Safe contracts to use the contract as a guard + /// @dev MUST revert if the contract is not enabled as a guard for the Safe + /// @dev MUST revert if timelock_delay is longer than 1 year + /// @dev MUST set the caller as a Safe + /// @dev MUST take timelock_delay as a parameter and store it as related to the Safe + /// @dev MUST emit a GuardConfigured event with at least timelock_delay as a parameter + /// @param _timelockDelay The timelock delay in seconds + function configureTimelockGuard(uint256 _timelockDelay) external { + // Validate timelock delay - must be non-zero and not longer than 1 year + if (_timelockDelay == 0 || _timelockDelay > 365 days) { + revert TimelockGuard_InvalidTimelockDelay(); + } + + // Check that this guard is enabled on the calling Safe + if (!_isGuardEnabled(msg.sender)) { + revert TimelockGuard_GuardNotEnabled(); + } + + // Store the configuration for this safe + safeConfigs[msg.sender].timelockDelay = _timelockDelay; + + // Initialize cancellation threshold to 1 + safeCancellationThreshold[msg.sender] = 1; + + emit GuardConfigured(msg.sender, _timelockDelay); + } + + /// @notice Remove the timelock guard configuration by a previously enabled Safe + /// @dev MUST revert if the contract is not enabled as a guard for the Safe + /// @dev MUST erase the existing timelock_delay data related to the calling Safe + /// @dev MUST emit a GuardCleared event + function clearTimelockGuard() external { + // Check if the calling safe has configuration set + if (safeConfigs[msg.sender].timelockDelay == 0) { + revert TimelockGuard_GuardNotConfigured(); + } + + // Check that this guard is NOT enabled on the calling Safe + if (_isGuardEnabled(msg.sender)) { + revert TimelockGuard_GuardStillEnabled(); + } + + // Erase the configuration data for this safe + delete safeConfigs[msg.sender]; + delete safeCancellationThreshold[msg.sender]; + + emit GuardCleared(msg.sender); + } + + /// @notice Returns the cancellation threshold for a given safe + /// @dev MUST NOT revert + /// @dev MUST return 0 if the contract is not enabled as a guard for the safe + /// @param _safe The Safe address to query + /// @return The current cancellation threshold + function cancellationThreshold(address _safe) public view returns (uint256) { + // Return 0 if guard is not enabled + if (!_isGuardEnabled(_safe)) { + return 0; + } + + return safeCancellationThreshold[_safe]; + } + + /// @notice Internal helper to get the guard address from a Safe + /// @param _safe The Safe address + /// @return The current guard address + function _isGuardEnabled(address _safe) internal view returns (bool) { + // keccak256("guard_manager.guard.address") from GuardManager + bytes32 guardSlot = 0x4a204f620c8c5ccdca3fd54d003badd85ba500436a431f0cbda4f558c93c34c8; + Safe safe = Safe(payable(_safe)); + address guard = abi.decode(safe.getStorageAt(uint256(guardSlot), 1), (address)); + return guard == address(this); + } + + /// @notice Schedule a transaction for execution after the timelock delay + /// @dev Called by anyone using signatures from Safe owners - NOT IMPLEMENTED YET + function scheduleTransaction( + address, + address, + uint256, + bytes memory, + Enum.Operation, + uint256, + uint256, + uint256, + address, + address payable, + bytes memory + ) + external + pure + { + // TODO: Implement + } + + /// @notice Returns the list of all scheduled but not cancelled transactions for a given safe + /// @dev MUST NOT revert - NOT IMPLEMENTED YET + /// @return List of pending transaction hashes + function checkPendingTransactions(address) external pure returns (bytes32[] memory) { + return new bytes32[](0); + } + + /// @notice Signal rejection of a scheduled transaction by a Safe owner + /// @dev NOT IMPLEMENTED YET + function rejectTransaction(address, bytes32) external pure { + // TODO: Implement + } + + /// @notice Signal rejection of a scheduled transaction using signatures + /// @dev NOT IMPLEMENTED YET + function rejectTransactionWithSignature(address, bytes32, bytes memory) external pure { + // TODO: Implement + } + + /// @notice Cancel a scheduled transaction if cancellation threshold is met + /// @dev NOT IMPLEMENTED YET + function cancelTransaction(address, bytes32) external pure { + // TODO: Implement + } + + /// @notice Called by the Safe before executing a transaction + /// @dev Implementation of IGuard interface + function checkTransaction( + address, + uint256 _value, + bytes memory, + Enum.Operation, + uint256, + uint256, + uint256, + address, + address payable, + bytes memory, + address + ) + external + override + { + // TODO: Implement + } + + /// @notice Called by the Safe after executing a transaction + /// @dev Implementation of IGuard interface + function checkAfterExecution(bytes32, bool) external override { + // TODO: Implement + } +} diff --git a/packages/contracts-bedrock/test/safe/TimelockGuard.t.sol b/packages/contracts-bedrock/test/safe/TimelockGuard.t.sol new file mode 100644 index 00000000000..8be77e3b7e3 --- /dev/null +++ b/packages/contracts-bedrock/test/safe/TimelockGuard.t.sol @@ -0,0 +1,219 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import { Test } from "forge-std/Test.sol"; +import { Enum } from "safe-contracts/common/Enum.sol"; +import { GuardManager } from "safe-contracts/base/GuardManager.sol"; +import { StorageAccessible } from "safe-contracts/common/StorageAccessible.sol"; +import "test/safe-tools/SafeTestTools.sol"; + +import { TimelockGuard } from "src/safe/TimelockGuard.sol"; + +/// @title TimelockGuard_TestInit +/// @notice Reusable test initialization for `TimelockGuard` tests. +contract TimelockGuard_TestInit is Test, SafeTestTools { + using SafeTestLib for SafeInstance; + + // Events + event GuardConfigured(address indexed safe, uint256 timelockDelay); + event GuardCleared(address indexed safe); + + uint256 constant INIT_TIME = 10; + uint256 constant TIMELOCK_DELAY = 7 days; + uint256 constant NUM_OWNERS = 5; + uint256 constant THRESHOLD = 3; + uint256 constant ONE_YEAR = 365 days; + + TimelockGuard timelockGuard; + SafeInstance safeInstance; + SafeInstance safeInstance2; + address[] owners; + uint256[] ownerPKs; + + function setUp() public virtual { + vm.warp(INIT_TIME); + + // Deploy the singleton TimelockGuard + timelockGuard = new TimelockGuard(); + + // Create Safe owners + (address[] memory _owners, uint256[] memory _keys) = SafeTestLib.makeAddrsAndKeys("owners", NUM_OWNERS); + owners = _owners; + ownerPKs = _keys; + + // Set up Safe with owners + safeInstance = _setupSafe(ownerPKs, THRESHOLD); + + // Enable the guard on the Safe + SafeTestLib.execTransaction( + safeInstance, + address(safeInstance.safe), + 0, + abi.encodeCall(GuardManager.setGuard, (address(timelockGuard))), + Enum.Operation.Call + ); + } + + /// @notice Helper to configure the TimelockGuard for a Safe + function _configureGuard(SafeInstance memory _safe, uint256 _delay) internal { + SafeTestLib.execTransaction( + _safe, + address(timelockGuard), + 0, + abi.encodeCall(TimelockGuard.configureTimelockGuard, (_delay)), + Enum.Operation.Call + ); + } + + /// @notice Helper to disable guard on a Safe + function _disableGuard(SafeInstance memory _safe) internal { + SafeTestLib.execTransaction( + _safe, address(_safe.safe), 0, abi.encodeCall(GuardManager.setGuard, (address(0))), Enum.Operation.Call + ); + } + + /// @notice Helper to clear the TimelockGuard configuration for a Safe + function _clearGuard(SafeInstance memory _safe) internal { + SafeTestLib.execTransaction( + _safe, address(timelockGuard), 0, abi.encodeCall(TimelockGuard.clearTimelockGuard, ()), Enum.Operation.Call + ); + } +} + +/// @title TimelockGuard_ViewTimelockGuardConfiguration_Test +/// @notice Tests for viewTimelockGuardConfiguration function +contract TimelockGuard_ViewTimelockGuardConfiguration_Test is TimelockGuard_TestInit { + function test_viewTimelockGuardConfiguration_returnsZeroForUnconfiguredSafe_succeeds() external view { + uint256 delay = timelockGuard.viewTimelockGuardConfiguration(address(safeInstance.safe)); + assertEq(delay, 0); + } +} + +/// @title TimelockGuard_ConfigureTimelockGuard_Test +/// @notice Tests for configureTimelockGuard function +contract TimelockGuard_ConfigureTimelockGuard_Test is TimelockGuard_TestInit { + function test_configureTimelockGuard_succeeds() external { + vm.expectEmit(true, true, true, true); + emit GuardConfigured(address(safeInstance.safe), TIMELOCK_DELAY); + + _configureGuard(safeInstance, TIMELOCK_DELAY); + + uint256 storedDelay = timelockGuard.viewTimelockGuardConfiguration(address(safeInstance.safe)); + assertEq(storedDelay, TIMELOCK_DELAY); + } + + function test_configureTimelockGuard_revertsIfGuardNotEnabled_reverts() external { + // Create a safe without enabling the guard + // Reduce the threshold just to prevent a CREATE2 collision when deploying this safe. + SafeInstance memory unguardedSafe = _setupSafe(ownerPKs, THRESHOLD - 1); + + vm.expectRevert(TimelockGuard.TimelockGuard_GuardNotEnabled.selector); + vm.prank(address(unguardedSafe.safe)); + timelockGuard.configureTimelockGuard(TIMELOCK_DELAY); + } + + function test_configureTimelockGuard_revertsIfDelayTooLong_reverts() external { + uint256 tooLongDelay = ONE_YEAR + 1; + + vm.expectRevert(TimelockGuard.TimelockGuard_InvalidTimelockDelay.selector); + vm.prank(address(safeInstance.safe)); + timelockGuard.configureTimelockGuard(tooLongDelay); + } + + function test_configureTimelockGuard_acceptsMaxValidDelay_succeeds() external { + vm.expectEmit(true, true, true, true); + emit GuardConfigured(address(safeInstance.safe), ONE_YEAR); + + _configureGuard(safeInstance, ONE_YEAR); + + uint256 storedDelay = timelockGuard.viewTimelockGuardConfiguration(address(safeInstance.safe)); + assertEq(storedDelay, ONE_YEAR); + } + + function test_configureTimelockGuard_allowsReconfiguration_succeeds() external { + // Initial configuration + _configureGuard(safeInstance, TIMELOCK_DELAY); + assertEq(timelockGuard.viewTimelockGuardConfiguration(address(safeInstance.safe)), TIMELOCK_DELAY); + + // Reconfigure with different delay + uint256 newDelay = 14 days; + vm.expectEmit(true, true, true, true); + emit GuardConfigured(address(safeInstance.safe), newDelay); + + _configureGuard(safeInstance, newDelay); + assertEq(timelockGuard.viewTimelockGuardConfiguration(address(safeInstance.safe)), newDelay); + } +} + +/// @title TimelockGuard_ClearTimelockGuard_Test +/// @notice Tests for clearTimelockGuard function +contract TimelockGuard_ClearTimelockGuard_Test is TimelockGuard_TestInit { + function test_clearTimelockGuard_succeeds() external { + // First configure the guard + _configureGuard(safeInstance, TIMELOCK_DELAY); + assertEq(timelockGuard.viewTimelockGuardConfiguration(address(safeInstance.safe)), TIMELOCK_DELAY); + + // Disable the guard first + _disableGuard(safeInstance); + + // Clear should succeed and emit event + vm.expectEmit(true, true, true, true); + emit GuardCleared(address(safeInstance.safe)); + + _clearGuard(safeInstance); + + // Configuration should be cleared + assertEq(timelockGuard.viewTimelockGuardConfiguration(address(safeInstance.safe)), 0); + // Ensure cancellation threshold is reset to 0 + assertEq(timelockGuard.cancellationThreshold(address(safeInstance.safe)), 0); + + // TODO: Check that any active challenge is cancelled + } + + function test_clearTimelockGuard_revertsIfGuardStillEnabled_reverts() external { + // First configure the guard + _configureGuard(safeInstance, TIMELOCK_DELAY); + + // Try to clear without disabling guard first - should revert + vm.expectRevert(TimelockGuard.TimelockGuard_GuardStillEnabled.selector); + vm.prank(address(safeInstance.safe)); + timelockGuard.clearTimelockGuard(); + } + + function test_clearTimelockGuard_revertsIfNotConfigured_reverts() external { + // Try to clear - should revert because not configured + vm.expectRevert(TimelockGuard.TimelockGuard_GuardNotConfigured.selector); + vm.prank(address(safeInstance.safe)); + timelockGuard.clearTimelockGuard(); + } +} + +/// @title TimelockGuard_CancellationThreshold_Test +/// @notice Tests for cancellationThreshold function +contract TimelockGuard_CancellationThreshold_Test is TimelockGuard_TestInit { + function test_cancellationThreshold_returnsZeroIfGuardNotEnabled_succeeds() external { + // Safe without guard enabled should return 0 + SafeInstance memory unguardedSafe = _setupSafe(ownerPKs, THRESHOLD - 1); + + uint256 threshold = timelockGuard.cancellationThreshold(address(unguardedSafe.safe)); + assertEq(threshold, 0); + } + + function test_cancellationThreshold_returnsZeroIfGuardNotConfigured_succeeds() external { + // Safe with guard enabled but not configured should return 0 + uint256 threshold = timelockGuard.cancellationThreshold(address(safeInstance.safe)); + assertEq(threshold, 0); + } + + function test_cancellationThreshold_returnsOneAfterConfiguration_succeeds() external { + // Configure the guard + _configureGuard(safeInstance, TIMELOCK_DELAY); + + // Should default to 1 after configuration + uint256 threshold = timelockGuard.cancellationThreshold(address(safeInstance.safe)); + assertEq(threshold, 1); + } + + // Note: Testing increment/decrement behavior will require scheduleTransaction, + // cancelTransaction and execution functions to be implemented first +}