diff --git a/src/AgoraGovernor.sol b/src/AgoraGovernor.sol index 847d6fa..af75f8c 100644 --- a/src/AgoraGovernor.sol +++ b/src/AgoraGovernor.sol @@ -75,7 +75,9 @@ contract AgoraGovernor is GovernorSettings(_votingDelay, _votingPeriod, _proposalThreshold) GovernorTimelockControl(_timelock) { - if (!_hooks.isValidHookAddress()) revert Hooks.HookAddressNotValid(address(_hooks)); + if (!_hooks.isValidHookAddress()) { + revert Hooks.HookAddressNotValid(address(_hooks)); + } // This call is made after the inhereted constructors have been called _hooks.beforeInitialize(); @@ -119,6 +121,24 @@ contract AgoraGovernor is return beforeProposalId != 0 ? beforeProposalId : proposalId; } + function queue(address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash) + public + virtual + override + returns (uint256) + { + uint256 beforeProposalId = hooks.beforeQueue(targets, values, calldatas, descriptionHash); + + uint256 proposalId = super.queue(targets, values, calldatas, descriptionHash); + + uint256 afterProposalId = hooks.afterQueue(proposalId, targets, values, calldatas, descriptionHash); + + // TODO: check that before and after proposal IDs are the same + + // TODO: replace this with a flag on hook address + return beforeProposalId != 0 ? beforeProposalId : proposalId; + } + function execute( address[] memory targets, uint256[] memory values, diff --git a/src/interfaces/IHooks.sol b/src/interfaces/IHooks.sol index d84b746..50576ad 100644 --- a/src/interfaces/IHooks.sol +++ b/src/interfaces/IHooks.sol @@ -76,6 +76,25 @@ interface IHooks { bytes32 descriptionHash ) external returns (bytes4, uint256); + /// @notice The hook called before a proposal is queued + function beforeQueue( + address sender, + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) external returns (bytes4, uint256); + + /// @notice The hook called after a proposal is queued + function afterQueue( + address sender, + uint256 proposalId, + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) external returns (bytes4, uint256); + /// @notice The hook called before a proposal is executed function beforeExecute( address sender, diff --git a/src/libraries/Hooks.sol b/src/libraries/Hooks.sol index 4720261..34ecf75 100644 --- a/src/libraries/Hooks.sol +++ b/src/libraries/Hooks.sol @@ -7,22 +7,25 @@ import {IHooks} from "src/interfaces/IHooks.sol"; library Hooks { using Hooks for IHooks; - uint160 internal constant ALL_HOOK_MASK = uint160((1 << 12) - 1); + uint160 internal constant ALL_HOOK_MASK = uint160((1 << 14) - 1); - uint160 internal constant BEFORE_INITIALIZE_FLAG = 1 << 11; - uint160 internal constant AFTER_INITIALIZE_FLAG = 1 << 10; + uint160 internal constant BEFORE_INITIALIZE_FLAG = 1 << 13; + uint160 internal constant AFTER_INITIALIZE_FLAG = 1 << 12; - uint160 internal constant BEFORE_QUORUM_CALCULATION_FLAG = 1 << 9; - uint160 internal constant AFTER_QUORUM_CALCULATION_FLAG = 1 << 8; + uint160 internal constant BEFORE_QUORUM_CALCULATION_FLAG = 1 << 11; + uint160 internal constant AFTER_QUORUM_CALCULATION_FLAG = 1 << 10; - uint160 internal constant BEFORE_VOTE_FLAG = 1 << 7; - uint160 internal constant AFTER_VOTE_FLAG = 1 << 6; + uint160 internal constant BEFORE_VOTE_FLAG = 1 << 9; + uint160 internal constant AFTER_VOTE_FLAG = 1 << 8; - uint160 internal constant BEFORE_PROPOSE_FLAG = 1 << 5; - uint160 internal constant AFTER_PROPOSE_FLAG = 1 << 4; + uint160 internal constant BEFORE_PROPOSE_FLAG = 1 << 7; + uint160 internal constant AFTER_PROPOSE_FLAG = 1 << 6; - uint160 internal constant BEFORE_CANCEL_FLAG = 1 << 3; - uint160 internal constant AFTER_CANCEL_FLAG = 1 << 2; + uint160 internal constant BEFORE_CANCEL_FLAG = 1 << 5; + uint160 internal constant AFTER_CANCEL_FLAG = 1 << 4; + + uint160 internal constant BEFORE_QUEUE_FLAG = 1 << 3; + uint160 internal constant AFTER_QUEUE_FLAG = 1 << 2; uint160 internal constant BEFORE_EXECUTE_FLAG = 1 << 1; uint160 internal constant AFTER_EXECUTE_FLAG = 1 << 0; @@ -38,6 +41,8 @@ library Hooks { bool afterPropose; bool beforeCancel; bool afterCancel; + bool beforeQueue; + bool afterQueue; bool beforeExecute; bool afterExecute; } @@ -68,6 +73,8 @@ library Hooks { || permissions.afterPropose != self.hasPermission(AFTER_PROPOSE_FLAG) || permissions.beforeCancel != self.hasPermission(BEFORE_CANCEL_FLAG) || permissions.afterCancel != self.hasPermission(AFTER_CANCEL_FLAG) + || permissions.beforeQueue != self.hasPermission(BEFORE_QUEUE_FLAG) + || permissions.afterQueue != self.hasPermission(AFTER_QUEUE_FLAG) || permissions.beforeExecute != self.hasPermission(BEFORE_EXECUTE_FLAG) || permissions.afterExecute != self.hasPermission(AFTER_EXECUTE_FLAG) ) { @@ -298,6 +305,49 @@ library Hooks { } } + /// @notice calls beforeQueue hook if permissioned and validates return value + function beforeQueue( + IHooks self, + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) internal noSelfCall(self) returns (uint256 returnedProposalId) { + if (self.hasPermission(BEFORE_QUEUE_FLAG)) { + bytes memory result = self.callHook( + abi.encodeCall(IHooks.beforeQueue, (msg.sender, targets, values, calldatas, descriptionHash)) + ); + + // A length of 36 bytes is required to return a bytes4 and a 32 byte proposal ID + if (result.length != 36) revert InvalidHookResponse(); + + // Extract the proposal ID from the result + returnedProposalId = parseUint256(result); + } + } + + /// @notice calls afterQueue hook if permissioned and validates return value + function afterQueue( + IHooks self, + uint256 proposalId, + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) internal noSelfCall(self) returns (uint256 returnedProposalId) { + if (self.hasPermission(AFTER_QUEUE_FLAG)) { + bytes memory result = self.callHook( + abi.encodeCall(IHooks.afterQueue, (msg.sender, proposalId, targets, values, calldatas, descriptionHash)) + ); + + // A length of 36 bytes is required to return a bytes4 and a 32 byte proposal ID + if (result.length != 36) revert InvalidHookResponse(); + + // Extract the proposal ID from the result + returnedProposalId = parseUint256(result); + } + } + /// @notice calls beforeCancel hook if permissioned and validates return value function beforeCancel( IHooks self, diff --git a/test/mocks/MockHooks.sol b/test/mocks/MockHooks.sol index c5690c6..011faa4 100644 --- a/test/mocks/MockHooks.sol +++ b/test/mocks/MockHooks.sol @@ -104,6 +104,26 @@ contract MockHooks is IHooks { return (returnValues[selector] == bytes4(0) ? selector : returnValues[selector], 0); } + function beforeQueue(address, address[] memory, uint256[] memory, bytes[] memory, bytes32) + external + override + returns (bytes4, uint256) + { + // beforeQueueData = hookData; + bytes4 selector = MockHooks.beforeQueue.selector; + return (returnValues[selector] == bytes4(0) ? selector : returnValues[selector], 0); + } + + function afterQueue(address, uint256, address[] memory, uint256[] memory, bytes[] memory, bytes32) + external + override + returns (bytes4, uint256) + { + // afterQueueData = hookData; + bytes4 selector = MockHooks.afterQueue.selector; + return (returnValues[selector] == bytes4(0) ? selector : returnValues[selector], 0); + } + function beforeExecute(address, address[] memory, uint256[] memory, bytes[] memory, bytes32) external override