diff --git a/packages/contracts-bedrock/interfaces/safe/ITransactionGuard.sol b/packages/contracts-bedrock/interfaces/safe/ITransactionGuard.sol new file mode 100644 index 00000000000..f6ba8f105a4 --- /dev/null +++ b/packages/contracts-bedrock/interfaces/safe/ITransactionGuard.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Enum } from "safe-contracts/common/Enum.sol"; +import { IERC165 } from "safe-contracts/interfaces/IERC165.sol"; + +/// @title ITransactionGuard Interface +interface ITransactionGuard is IERC165 { + /// @notice Checks the transaction details. + /// @dev The function needs to implement transaction validation logic. + /// @param to The address to which the transaction is intended. + /// @param value The native token value of the transaction in Wei. + /// @param data The transaction data. + /// @param operation Operation type (0 for `CALL`, 1 for `DELEGATECALL`). + /// @param safeTxGas Gas used for the transaction. + /// @param baseGas The base gas for the transaction. + /// @param gasPrice The price of gas in Wei for the transaction. + /// @param gasToken The token used to pay for gas. + /// @param refundReceiver The address which should receive the refund. + /// @param signatures The signatures of the transaction. + /// @param msgSender The address of the message sender. + function checkTransaction( + address to, + uint256 value, + bytes memory data, + Enum.Operation operation, + uint256 safeTxGas, + uint256 baseGas, + uint256 gasPrice, + address gasToken, + address payable refundReceiver, + bytes memory signatures, + address msgSender + ) external; + + /// @notice Checks after execution of the transaction. + /// @dev The function needs to implement a check after the execution of the transaction. + /// @param hash The hash of the executed transaction. + /// @param success The status of the transaction execution. + function checkAfterExecution(bytes32 hash, bool success) external; +} diff --git a/packages/contracts-bedrock/snapshots/abi/SaferSafes.json b/packages/contracts-bedrock/snapshots/abi/SaferSafes.json index 64e28905550..1589521678a 100644 --- a/packages/contracts-bedrock/snapshots/abi/SaferSafes.json +++ b/packages/contracts-bedrock/snapshots/abi/SaferSafes.json @@ -559,6 +559,25 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "_interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 04fddbbce64..a1a7b9eaf17 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -208,8 +208,8 @@ "sourceCodeHash": "0x950725f8b9ad9bb3b6b5e836f67e18db824a7864bac547ee0eeba88ada3de0e9" }, "src/safe/SaferSafes.sol:SaferSafes": { - "initCodeHash": "0xaa17bb150c9bcf19675a33e9762b050148aceae9f6a9a6ba020fc6947ebaab39", - "sourceCodeHash": "0xc4201612048ff051ed795521efa3eece1a6556f2c514a268b180d84a2ad8b2d1" + "initCodeHash": "0x95ee7ae09ee281f224425f152c9154e43e49838edbe3eee48c15301e5f410d25", + "sourceCodeHash": "0xdc794e3d6decb47c51d86bb2f69523feade4fd3f81feb4734800f24b40d40a50" }, "src/universal/OptimismMintableERC20.sol:OptimismMintableERC20": { "initCodeHash": "0x3c85eed0d017dca8eda6396aa842ddc12492587b061e8c756a8d32c4610a9658", diff --git a/packages/contracts-bedrock/src/safe/SaferSafes.sol b/packages/contracts-bedrock/src/safe/SaferSafes.sol index 52ea97df94e..80d879de141 100644 --- a/packages/contracts-bedrock/src/safe/SaferSafes.sol +++ b/packages/contracts-bedrock/src/safe/SaferSafes.sol @@ -22,8 +22,8 @@ import { ISemver } from "interfaces/universal/ISemver.sol"; /// functionality is not desired, then there is no need to enable or configure it. contract SaferSafes is LivenessModule2, TimelockGuard, ISemver { /// @notice Semantic version. - /// @custom:semver 1.2.0 - string public constant version = "1.2.0"; + /// @custom:semver 1.3.0 + string public constant version = "1.3.0"; /// @notice Error for when the liveness response period is insufficient. error SaferSafes_InsufficientLivenessResponsePeriod(); diff --git a/packages/contracts-bedrock/src/safe/TimelockGuard.sol b/packages/contracts-bedrock/src/safe/TimelockGuard.sol index 2276cbdcf99..9905761fbe8 100644 --- a/packages/contracts-bedrock/src/safe/TimelockGuard.sol +++ b/packages/contracts-bedrock/src/safe/TimelockGuard.sol @@ -5,12 +5,16 @@ pragma solidity 0.8.15; import { GnosisSafe as Safe } from "safe-contracts/GnosisSafe.sol"; import { Enum } from "safe-contracts/common/Enum.sol"; import { Guard as IGuard } from "safe-contracts/base/GuardManager.sol"; +import { IERC165 } from "safe-contracts/interfaces/IERC165.sol"; // Libraries import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import { SemverComp } from "src/libraries/SemverComp.sol"; import { Constants } from "src/libraries/Constants.sol"; +// Interfaces +import { ITransactionGuard } from "interfaces/safe/ITransactionGuard.sol"; + /// @title TimelockGuard /// @notice This guard provides timelock functionality for Safe transactions /// @dev This is a singleton contract, any Safe on the network can use this guard to enforce a timelock delay, and @@ -65,7 +69,7 @@ import { Constants } from "src/libraries/Constants.sol"; /// | Quorum+ | challenge + | cancelTransaction | /// | | changeOwnershipToFallback | | /// +-------------------------------------------------------------------------------------------------+ -abstract contract TimelockGuard is IGuard { +abstract contract TimelockGuard is IGuard, IERC165 { using EnumerableSet for EnumerableSet.Bytes32Set; /// @notice Allowed states of a transaction @@ -673,4 +677,16 @@ abstract contract TimelockGuard is IGuard { function signCancellation(bytes32) public { emit Message("This function is not meant to be called, did you mean to call cancelTransaction?"); } + + //////////////////////////////////////////////////////////////// + // ERC165 Support // + //////////////////////////////////////////////////////////////// + + /// @notice ERC165 interface detection + /// @param _interfaceId The interface identifier to check + /// @return True if the contract implements the interface + function supportsInterface(bytes4 _interfaceId) external view virtual override returns (bool) { + return _interfaceId == type(ITransactionGuard).interfaceId // 0xe6d7a83a + || _interfaceId == type(IERC165).interfaceId; // 0x01ffc9a7 + } } diff --git a/packages/contracts-bedrock/test/safe/TimelockGuard.t.sol b/packages/contracts-bedrock/test/safe/TimelockGuard.t.sol index 1cd1331ccc5..55bb96a8933 100644 --- a/packages/contracts-bedrock/test/safe/TimelockGuard.t.sol +++ b/packages/contracts-bedrock/test/safe/TimelockGuard.t.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.15; import { Test } from "forge-std/Test.sol"; import { GnosisSafe as Safe } from "safe-contracts/GnosisSafe.sol"; import { GuardManager } from "safe-contracts/base/GuardManager.sol"; +import { ITransactionGuard } from "interfaces/safe/ITransactionGuard.sol"; import "test/safe-tools/SafeTestTools.sol"; import { TimelockGuard } from "src/safe/TimelockGuard.sol"; @@ -981,3 +982,23 @@ contract TimelockGuard_ClearTimelockGuard_Test is TimelockGuard_TestInit { timelockGuard.clearTimelockGuard(); } } + +/// @title TimelockGuard_SupportsInterface_Test +/// @notice Tests ERC165 interface support for TimelockGuard +contract TimelockGuard_SupportsInterface_Test is TimelockGuard_TestInit { + function test_supportsInterface_iTransactionGuard_succeeds() external view { + bytes4 interfaceId = 0xe6d7a83a; // ITransactionGuard interface ID + assertTrue(timelockGuard.supportsInterface(interfaceId), "Should support ITransactionGuard"); + } + + function test_supportsInterface_ierc165_succeeds() external view { + bytes4 interfaceId = 0x01ffc9a7; // IERC165 interface ID + assertTrue(timelockGuard.supportsInterface(interfaceId), "Should support IERC165"); + } + + function test_supportsInterface_invalidInterface_fails(bytes4 _interfaceId) external view { + vm.assume(_interfaceId != type(ITransactionGuard).interfaceId); + vm.assume(_interfaceId != type(IERC165).interfaceId); + assertFalse(timelockGuard.supportsInterface(_interfaceId), "Should not support invalid interface"); + } +}