// SPDX-License-Identifier: MIT pragma solidity 0.8.15; import { ISemver } from "src/universal/ISemver.sol"; /// @title DelayedVetoable /// @notice This contract enables a delay before a call is forwarded to a target contract, and during the delay period /// the call can be vetoed by the authorized vetoer. /// This contract does not support value transfers, only data is forwarded. /// Additionally, this contract cannot be used to forward calls with data beginning with the function selector /// of the queuedAt(bytes32) function. This is because of input validation checks which solidity performs at /// runtime on functions which take an argument. contract DelayedVetoable is ISemver { /// @notice Error for when the delay has already been set. error AlreadyDelayed(); /// @notice Error for when attempting to forward too early. error ForwardingEarly(); /// @notice Error for the target is not set. error TargetUnitialized(); /// @notice Error for unauthorized calls. error Unauthorized(address expected, address actual); /// @notice An event that is emitted when the delay is activated. /// @param delay The delay that was activated. event DelayActivated(uint256 delay); /// @notice An event that is emitted when a call is initiated. /// @param callHash The hash of the call data. /// @param data The data of the initiated call. event Initiated(bytes32 indexed callHash, bytes data); /// @notice An event that is emitted each time a call is forwarded. /// @param callHash The hash of the call data. /// @param data The data forwarded to the target. event Forwarded(bytes32 indexed callHash, bytes data); /// @notice An event that is emitted each time a call is vetoed. /// @param callHash The hash of the call data. /// @param data The data forwarded to the target. event Vetoed(bytes32 indexed callHash, bytes data); /// @notice The address that all calls are forwarded to after the delay. address internal immutable TARGET; /// @notice The address that can veto a call. address internal immutable VETOER; /// @notice The address that can initiate a call. address internal immutable INITIATOR; /// @notice The delay which will be set after the initial system deployment is completed. uint256 internal immutable OPERATING_DELAY; /// @notice The current amount of time to wait before forwarding a call. uint256 internal _delay; /// @notice The time that a call was initiated. mapping(bytes32 => uint256) internal _queuedAt; /// @notice A modifier that reverts if not called by the vetoer or by address(0) to allow /// eth_call to interact with this proxy without needing to use low-level storage /// inspection. We assume that nobody is able to trigger calls from address(0) during /// normal EVM execution. modifier readOrHandle() { if (msg.sender == address(0)) { _; } else { // This WILL halt the call frame on completion. _handleCall(); } } /// @notice Semantic version. /// @custom:semver 1.0.0 string public constant version = "1.0.0"; /// @notice Sets the target admin during contract deployment. /// @param vetoer_ Address of the vetoer. /// @param initiator_ Address of the initiator. /// @param target_ Address of the target. /// @param operatingDelay_ Time to delay when the system is operational. constructor(address vetoer_, address initiator_, address target_, uint256 operatingDelay_) { // Note that the _delay value is not set here. Having an initial delay of 0 is helpful // during the deployment of a new system. VETOER = vetoer_; INITIATOR = initiator_; TARGET = target_; OPERATING_DELAY = operatingDelay_; } /// @notice Gets the initiator /// @return initiator_ Initiator address. function initiator() external virtual readOrHandle returns (address initiator_) { initiator_ = INITIATOR; } //// @notice Queries the vetoer address. /// @return vetoer_ Vetoer address. function vetoer() external virtual readOrHandle returns (address vetoer_) { vetoer_ = VETOER; } //// @notice Queries the target address. /// @return target_ Target address. function target() external readOrHandle returns (address target_) { target_ = TARGET; } /// @notice Gets the delay /// @return delay_ Delay address. function delay() external readOrHandle returns (uint256 delay_) { delay_ = _delay; } /// @notice Gets entries in the _queuedAt mapping. /// @param callHash The hash of the call data. /// @return queuedAt_ The time the callHash was recorded. function queuedAt(bytes32 callHash) external readOrHandle returns (uint256 queuedAt_) { queuedAt_ = _queuedAt[callHash]; } /// @notice Used for all calls that pass data to the contract. fallback() external { _handleCall(); } /// @notice Receives all calls other than those made by the vetoer. /// This enables transparent initiation and forwarding of calls to the target and avoids /// the need for additional layers of abi encoding. function _handleCall() internal { // The initiator and vetoer activate the delay by passing in null data. if (msg.data.length == 0 && _delay == 0) { if (msg.sender != INITIATOR && msg.sender != VETOER) { revert Unauthorized(INITIATOR, msg.sender); } _delay = OPERATING_DELAY; emit DelayActivated(_delay); return; } bytes32 callHash = keccak256(msg.data); // Case 1: The initiator is calling the contract to initiate a call. if (msg.sender == INITIATOR && _queuedAt[callHash] == 0) { if (_delay == 0) { // This forward function will halt the call frame on completion. _forwardAndHalt(callHash); } _queuedAt[callHash] = block.timestamp; emit Initiated(callHash, msg.data); return; } // Case 2: The vetoer is calling the contract to veto a call. // Note: The vetoer retains the ability to veto even after the delay has passed. This makes censoring the vetoer // more costly, as there is no time limit after which their transaction can be included. if (msg.sender == VETOER && _queuedAt[callHash] != 0) { delete _queuedAt[callHash]; emit Vetoed(callHash, msg.data); return; } // Case 3: The call is from an unpermissioned actor. We'll forward the call if the delay has // passed. if (_queuedAt[callHash] == 0) { // The call has not been initiated, so we'll treat this is an unauthorized initiation attempt. revert Unauthorized(INITIATOR, msg.sender); } if (_queuedAt[callHash] + _delay > block.timestamp) { // Not enough time has passed, so we'll revert. revert ForwardingEarly(); } // Delete the call to prevent replays delete _queuedAt[callHash]; _forwardAndHalt(callHash); } /// @notice Forwards the call to the target and halts the call frame. function _forwardAndHalt(bytes32 callHash) internal { // Forward the call emit Forwarded(callHash, msg.data); (bool success, bytes memory returndata) = TARGET.call(msg.data); if (success == true) { assembly { return(add(returndata, 0x20), mload(returndata)) } } else { assembly { revert(add(returndata, 0x20), mload(returndata)) } } } }