diff --git a/contracts/governance/README.adoc b/contracts/governance/README.adoc new file mode 100644 index 00000000..f8aa55da --- /dev/null +++ b/contracts/governance/README.adoc @@ -0,0 +1,12 @@ += Governance + +[.readme-notice] +NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/governance + +This directory includes extensions and utilities for on-chain governance. + +* {TimelockControllerEnumerable}: Extension of OpenZeppelin's TimelockController with enumerable operations support. + +== Timelock + +{{TimelockControllerEnumerable}} \ No newline at end of file diff --git a/contracts/governance/TimelockControllerEnumerable.sol b/contracts/governance/TimelockControllerEnumerable.sol new file mode 100644 index 00000000..8088eccb --- /dev/null +++ b/contracts/governance/TimelockControllerEnumerable.sol @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +/// @dev Extends the TimelockController to allow for enumerable operations +abstract contract TimelockControllerEnumerable is TimelockController { + using EnumerableSet for EnumerableSet.Bytes32Set; + + /// @notice The operation struct + struct Operation { + address target; + uint256 value; + bytes data; + bytes32 predecessor; + bytes32 salt; + uint256 delay; + } + + /// @notice The operation batch struct + struct OperationBatch { + address[] targets; + uint256[] values; + bytes[] payloads; + bytes32 predecessor; + bytes32 salt; + uint256 delay; + } + + /// @dev The error when the operation index is not found + error OperationIndexNotFound(uint256 index); + /// @dev The error when the operation id is not found + error OperationIdNotFound(bytes32 id); + /// @dev The error when the operation batch index is not found + error OperationBatchIndexNotFound(uint256 index); + /// @dev The error when the operation batch id is not found + error OperationBatchIdNotFound(bytes32 id); + /// @dev The error when the index range is invalid + error InvalidIndexRange(uint256 start, uint256 end); + + /// @notice The operations id set + EnumerableSet.Bytes32Set private _operationsIdSet; + /// @notice The operations map + mapping(bytes32 id => Operation operation) private _operationsMap; + + /// @notice The operations batch id set + EnumerableSet.Bytes32Set private _operationsBatchIdSet; + /// @notice The operations batch map + mapping(bytes32 id => OperationBatch operationBatch) private _operationsBatchMap; + + /// @inheritdoc TimelockController + function schedule( + address target, + uint256 value, + bytes calldata data, + bytes32 predecessor, + bytes32 salt, + uint256 delay + ) public virtual override { + super.schedule(target, value, data, predecessor, salt, delay); + bytes32 id = hashOperation(target, value, data, predecessor, salt); + _operationsIdSet.add(id); + _operationsMap[id] = Operation({ + target: target, + value: value, + data: data, + predecessor: predecessor, + salt: salt, + delay: delay + }); + } + + /// @inheritdoc TimelockController + function scheduleBatch( + address[] calldata targets, + uint256[] calldata values, + bytes[] calldata payloads, + bytes32 predecessor, + bytes32 salt, + uint256 delay + ) public virtual override { + super.scheduleBatch(targets, values, payloads, predecessor, salt, delay); + bytes32 id = hashOperationBatch(targets, values, payloads, predecessor, salt); + _operationsBatchIdSet.add(id); + _operationsBatchMap[id] = OperationBatch({ + targets: targets, + values: values, + payloads: payloads, + predecessor: predecessor, + salt: salt, + delay: delay + }); + } + + /// @inheritdoc TimelockController + function cancel(bytes32 id) public virtual override { + super.cancel(id); + if (_operationsIdSet.contains(id)) { + _operationsIdSet.remove(id); + delete _operationsMap[id]; + } + if (_operationsBatchIdSet.contains(id)) { + _operationsBatchIdSet.remove(id); + delete _operationsBatchMap[id]; + } + } + + /// @dev Return all scheduled operations + /// WARNING: This is designed for view accessors queried without gas fees. Using it in state-changing + /// functions may become uncallable if the list grows too large. + function operations() public view returns (Operation[] memory operations_) { + return operations(0, _operationsIdSet.length()); + } + + /// @dev Return the operations in the given index range + /// @param start The start index + /// @param end The end index + /// @return operations_ The operations + /// WARNING: This is designed for view accessors queried without gas fees. Using it in state-changing + /// functions may become uncallable if the list grows too large. + function operations(uint256 start, uint256 end) public view returns (Operation[] memory operations_) { + if (start > end || start >= _operationsIdSet.length()) { + revert InvalidIndexRange(start, end); + } + operations_ = new Operation[](end - start); + for (uint256 i = start; i < end; i++) { + operations_[i] = _operationsMap[_operationsIdSet.at(i)]; + } + return operations_; + } + + /// @dev Return the number of operations from the set + function operationsCount() public view returns (uint256 operationsCount_) { + operationsCount_ = _operationsIdSet.length(); + return operationsCount_; + } + + /// @dev Return the operation at the given index + function operation(uint256 index) public view returns (Operation memory operation_) { + if (index >= _operationsIdSet.length()) { + revert OperationIndexNotFound(index); + } + operation_ = _operationsMap[_operationsIdSet.at(index)]; + return operation_; + } + + /// @dev Return the operation with the given id + function operation(bytes32 id) public view returns (Operation memory operation_) { + if (!_operationsIdSet.contains(id)) { + revert OperationIdNotFound(id); + } + operation_ = _operationsMap[id]; + return operation_; + } + + /// @dev Return all scheduled operation batches + /// WARNING: This is designed for view accessors queried without gas fees. Using it in state-changing + /// functions may become uncallable if the list grows too large. + function operationsBatch() public view returns (OperationBatch[] memory operationsBatch_) { + return operationsBatch(0, _operationsBatchIdSet.length()); + } + + /// @dev Return the operationsBatch in the given index range + /// @param start The start index + /// @param end The end index + /// @return operationsBatch_ The operationsBatch + /// WARNING: This is designed for view accessors queried without gas fees. Using it in state-changing + /// functions may become uncallable if the list grows too large. + function operationsBatch( + uint256 start, + uint256 end + ) public view returns (OperationBatch[] memory operationsBatch_) { + if (start > end || start >= _operationsBatchIdSet.length()) { + revert InvalidIndexRange(start, end); + } + operationsBatch_ = new OperationBatch[](end - start); + for (uint256 i = start; i < end; i++) { + operationsBatch_[i] = _operationsBatchMap[_operationsBatchIdSet.at(i)]; + } + return operationsBatch_; + } + + /// @dev Return the number of operationsBatch from the set + function operationsBatchCount() public view returns (uint256 operationsBatchCount_) { + operationsBatchCount_ = _operationsBatchIdSet.length(); + return operationsBatchCount_; + } + + /// @dev Return the operationsBatch at the given index + function operationBatch(uint256 index) public view returns (OperationBatch memory operationBatch_) { + if (index >= _operationsBatchIdSet.length()) { + revert OperationBatchIndexNotFound(index); + } + operationBatch_ = _operationsBatchMap[_operationsBatchIdSet.at(index)]; + return operationBatch_; + } + + /// @dev Return the operationsBatch with the given id + function operationBatch(bytes32 id) public view returns (OperationBatch memory operationBatch_) { + if (!_operationsBatchIdSet.contains(id)) { + revert OperationBatchIdNotFound(id); + } + operationBatch_ = _operationsBatchMap[id]; + return operationBatch_; + } +} diff --git a/foundry.lock b/foundry.lock new file mode 100644 index 00000000..86a352d9 --- /dev/null +++ b/foundry.lock @@ -0,0 +1,32 @@ +{ + "lib/@openzeppelin-contracts": { + "branch": { + "name": "master", + "rev": "c3961a45380831135b37e55bc3ed441f678a4f5e" + } + }, + "lib/@openzeppelin-contracts-upgradeable": { + "branch": { + "name": "master", + "rev": "87e32858518950ff89c5492b6c81bb3d87cba9e3" + } + }, + "lib/axelar-gmp-sdk-solidity": { + "rev": "00682b6c3db0cc922ec0c4ea3791852c93d7ae31" + }, + "lib/email-tx-builder": { + "rev": "895e1fe943e967b0faab6a476f3b82b37d14300d" + }, + "lib/forge-std": { + "rev": "60acb7aaadcce2d68e52986a0a66fe79f07d138f" + }, + "lib/wormhole-solidity-sdk": { + "rev": "575181b586a315d8f9813eab82e4cb98b45bc381" + }, + "lib/zk-email-verify": { + "branch": { + "name": "v6.3.2", + "rev": "9ed3769dc3d96fb0d7c45f1f014dcd9bfb63675b" + } + } +} \ No newline at end of file diff --git a/test/governance/TimelockControllerEnumerable.t.sol b/test/governance/TimelockControllerEnumerable.t.sol new file mode 100644 index 00000000..182e2b40 --- /dev/null +++ b/test/governance/TimelockControllerEnumerable.t.sol @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {TimelockControllerEnumerableMock} from "./TimelockControllerEnumerableMock.t.sol"; +import { + TimelockControllerEnumerable +} from "@openzeppelin/community-contracts/governance/TimelockControllerEnumerable.sol"; + +contract TimelockControllerEnumerableTest is Test { + TimelockControllerEnumerableMock public timelockControllerEnumerable; + + event Call(); + + function setUp() public { + address[] memory proposers = new address[](1); + address[] memory executors = new address[](1); + proposers[0] = address(this); + executors[0] = address(this); + uint256 minDelay = 1 days; + timelockControllerEnumerable = new TimelockControllerEnumerableMock(minDelay, proposers, executors, address(0)); + } + + function call() external { + emit Call(); + } + + function test_schedule() public { + timelockControllerEnumerable.schedule( + address(this), + 0, + abi.encodeCall(this.call, ()), + bytes32(0), + bytes32(0), + 1 days + ); + assertEq(timelockControllerEnumerable.operationsCount(), 1); + TimelockControllerEnumerable.Operation memory operation = timelockControllerEnumerable.operation(uint256(0)); + assertEq(operation.target, address(this)); + assertEq(operation.value, 0); + assertEq(operation.data, abi.encodeCall(this.call, ())); + assertEq(operation.predecessor, bytes32(0)); + assertEq(operation.salt, bytes32(0)); + assertEq(operation.delay, 1 days); + bytes32 id = timelockControllerEnumerable.hashOperation( + address(this), + 0, + abi.encodeCall(this.call, ()), + bytes32(0), + bytes32(0) + ); + operation = timelockControllerEnumerable.operation(id); + assertEq(operation.target, address(this)); + assertEq(operation.value, 0); + assertEq(operation.data, abi.encodeCall(this.call, ())); + assertEq(operation.predecessor, bytes32(0)); + assertEq(operation.salt, bytes32(0)); + assertEq(operation.delay, 1 days); + } + + function test_operations() public { + test_schedule(); + TimelockControllerEnumerable.Operation[] memory operations = timelockControllerEnumerable.operations(0, 1); + assertEq(operations.length, 1); + assertEq(operations[0].target, address(this)); + assertEq(operations[0].value, 0); + assertEq(operations[0].data, abi.encodeCall(this.call, ())); + assertEq(operations[0].predecessor, bytes32(0)); + assertEq(operations[0].salt, bytes32(0)); + assertEq(operations[0].delay, 1 days); + vm.expectRevert(abi.encodeWithSelector(TimelockControllerEnumerable.InvalidIndexRange.selector, 2, 1)); + timelockControllerEnumerable.operations(2, 1); + + operations = timelockControllerEnumerable.operations(); + assertEq(operations.length, 1); + assertEq(operations[0].target, address(this)); + assertEq(operations[0].value, 0); + assertEq(operations[0].data, abi.encodeCall(this.call, ())); + assertEq(operations[0].predecessor, bytes32(0)); + assertEq(operations[0].salt, bytes32(0)); + assertEq(operations[0].delay, 1 days); + } + + function test_schedule_execute() public { + test_schedule(); + TimelockControllerEnumerable.Operation memory operation = timelockControllerEnumerable.operation(uint256(0)); + bytes32 id = timelockControllerEnumerable.hashOperation( + operation.target, + operation.value, + operation.data, + operation.predecessor, + operation.salt + ); + assertEq(timelockControllerEnumerable.isOperationPending(id), true); + vm.warp(block.timestamp + operation.delay); + timelockControllerEnumerable.execute( + operation.target, + operation.value, + operation.data, + operation.predecessor, + operation.salt + ); + assertEq(timelockControllerEnumerable.isOperationPending(id), false); + } + + function test_scheduleBatch() public { + address[] memory targets = new address[](1); + uint256[] memory values = new uint256[](1); + bytes[] memory payloads = new bytes[](1); + targets[0] = address(this); + values[0] = 0; + payloads[0] = abi.encodeCall(this.call, ()); + timelockControllerEnumerable.scheduleBatch(targets, values, payloads, bytes32(0), bytes32(0), 1 days); + assertEq(timelockControllerEnumerable.operationsBatchCount(), 1); + TimelockControllerEnumerable.OperationBatch memory operationBatch = timelockControllerEnumerable.operationBatch( + uint256(0) + ); + assertEq(operationBatch.targets[0], address(this)); + assertEq(operationBatch.values[0], 0); + assertEq(operationBatch.payloads[0], abi.encodeCall(this.call, ())); + assertEq(operationBatch.predecessor, bytes32(0)); + assertEq(operationBatch.salt, bytes32(0)); + assertEq(operationBatch.delay, 1 days); + bytes32 id = timelockControllerEnumerable.hashOperationBatch(targets, values, payloads, bytes32(0), bytes32(0)); + operationBatch = timelockControllerEnumerable.operationBatch(id); + assertEq(operationBatch.targets[0], address(this)); + assertEq(operationBatch.values[0], 0); + assertEq(operationBatch.payloads[0], abi.encodeCall(this.call, ())); + assertEq(operationBatch.predecessor, bytes32(0)); + assertEq(operationBatch.salt, bytes32(0)); + assertEq(operationBatch.delay, 1 days); + } + + function test_operationsBatch() public { + test_scheduleBatch(); + TimelockControllerEnumerable.OperationBatch[] memory operationBatches = timelockControllerEnumerable + .operationsBatch(0, 1); + assertEq(operationBatches.length, 1); + assertEq(operationBatches[0].targets[0], address(this)); + assertEq(operationBatches[0].values[0], 0); + assertEq(operationBatches[0].payloads[0], abi.encodeCall(this.call, ())); + assertEq(operationBatches[0].predecessor, bytes32(0)); + assertEq(operationBatches[0].salt, bytes32(0)); + assertEq(operationBatches[0].delay, 1 days); + vm.expectRevert(abi.encodeWithSelector(TimelockControllerEnumerable.InvalidIndexRange.selector, 2, 1)); + timelockControllerEnumerable.operationsBatch(2, 1); + + operationBatches = timelockControllerEnumerable.operationsBatch(); + assertEq(operationBatches.length, 1); + assertEq(operationBatches[0].targets[0], address(this)); + assertEq(operationBatches[0].values[0], 0); + assertEq(operationBatches[0].payloads[0], abi.encodeCall(this.call, ())); + assertEq(operationBatches[0].predecessor, bytes32(0)); + assertEq(operationBatches[0].salt, bytes32(0)); + assertEq(operationBatches[0].delay, 1 days); + } + + function test_scheduleBatch_execute() public { + test_scheduleBatch(); + TimelockControllerEnumerable.OperationBatch memory operationBatch = timelockControllerEnumerable.operationBatch( + uint256(0) + ); + bytes32 id = timelockControllerEnumerable.hashOperationBatch( + operationBatch.targets, + operationBatch.values, + operationBatch.payloads, + operationBatch.predecessor, + operationBatch.salt + ); + assertEq(timelockControllerEnumerable.isOperationPending(id), true); + vm.warp(block.timestamp + operationBatch.delay); + timelockControllerEnumerable.executeBatch( + operationBatch.targets, + operationBatch.values, + operationBatch.payloads, + operationBatch.predecessor, + operationBatch.salt + ); + assertEq(timelockControllerEnumerable.isOperationPending(id), false); + } + + function test_cancel_schedule() public { + timelockControllerEnumerable.schedule( + address(this), + 0, + abi.encodeCall(this.call, ()), + bytes32(0), + bytes32(0), + 1 days + ); + assertEq(timelockControllerEnumerable.operationsCount(), 1); + bytes32 id = timelockControllerEnumerable.hashOperation( + address(this), + 0, + abi.encodeCall(this.call, ()), + bytes32(0), + bytes32(0) + ); + timelockControllerEnumerable.cancel(id); + assertEq(timelockControllerEnumerable.operationsCount(), 0); + vm.expectRevert(abi.encodeWithSelector(TimelockControllerEnumerable.OperationIdNotFound.selector, id)); + timelockControllerEnumerable.operation(id); + vm.expectRevert(abi.encodeWithSelector(TimelockControllerEnumerable.OperationIndexNotFound.selector, 0)); + timelockControllerEnumerable.operation(uint256(0)); + } + + function test_cancel_scheduleBatch() public { + address[] memory targets = new address[](1); + uint256[] memory values = new uint256[](1); + bytes[] memory payloads = new bytes[](1); + targets[0] = address(this); + values[0] = 0; + payloads[0] = abi.encodeCall(this.call, ()); + timelockControllerEnumerable.scheduleBatch(targets, values, payloads, bytes32(0), bytes32(0), 1 days); + assertEq(timelockControllerEnumerable.operationsBatchCount(), 1); + bytes32 id = timelockControllerEnumerable.hashOperationBatch(targets, values, payloads, bytes32(0), bytes32(0)); + timelockControllerEnumerable.cancel(id); + assertEq(timelockControllerEnumerable.operationsBatchCount(), 0); + vm.expectRevert(abi.encodeWithSelector(TimelockControllerEnumerable.OperationBatchIdNotFound.selector, id)); + timelockControllerEnumerable.operationBatch(id); + vm.expectRevert(abi.encodeWithSelector(TimelockControllerEnumerable.OperationBatchIndexNotFound.selector, 0)); + timelockControllerEnumerable.operationBatch(uint256(0)); + } +} diff --git a/test/governance/TimelockControllerEnumerableMock.t.sol b/test/governance/TimelockControllerEnumerableMock.t.sol new file mode 100644 index 00000000..cbe07211 --- /dev/null +++ b/test/governance/TimelockControllerEnumerableMock.t.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import { + TimelockControllerEnumerable +} from "@openzeppelin/community-contracts/governance/TimelockControllerEnumerable.sol"; +import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol"; + +contract TimelockControllerEnumerableMock is TimelockControllerEnumerable { + constructor( + uint256 minDelay, + address[] memory proposers, + address[] memory executors, + address admin + ) TimelockController(minDelay, proposers, executors, admin) {} +}