Ver código fonte

Add Governor contracts (#2672)

Co-authored-by: Francisco Giordano <frangio.1@gmail.com>
Hadrien Croubois 4 anos atrás
pai
commit
6c1a634053
39 arquivos alterados com 5147 adições e 205 exclusões
  1. 2 1
      CHANGELOG.md
  2. 357 0
      contracts/governance/Governor.sol
  3. 214 0
      contracts/governance/IGovernor.sol
  4. 56 2
      contracts/governance/README.adoc
  5. 285 0
      contracts/governance/compatibility/GovernorCompatibilityBravo.sol
  6. 111 0
      contracts/governance/compatibility/IGovernorCompatibilityBravo.sol
  7. 107 0
      contracts/governance/extensions/GovernorCountingSimple.sol
  8. 242 0
      contracts/governance/extensions/GovernorTimelockCompound.sol
  9. 153 0
      contracts/governance/extensions/GovernorTimelockControl.sol
  10. 27 0
      contracts/governance/extensions/GovernorVotes.sol
  11. 26 0
      contracts/governance/extensions/GovernorVotesComp.sol
  12. 49 0
      contracts/governance/extensions/GovernorVotesQuorumFraction.sol
  13. 25 0
      contracts/governance/extensions/IGovernorTimelock.sol
  14. 55 0
      contracts/mocks/GovernorCompMock.sol
  15. 145 0
      contracts/mocks/GovernorCompatibilityBravoMock.sol
  16. 52 0
      contracts/mocks/GovernorMock.sol
  17. 111 0
      contracts/mocks/GovernorTimelockCompoundMock.sol
  18. 111 0
      contracts/mocks/GovernorTimelockControlMock.sol
  19. 39 0
      contracts/mocks/TimersBlockNumberImpl.sol
  20. 39 0
      contracts/mocks/TimersTimestampImpl.sol
  21. 174 0
      contracts/mocks/compound/CompTimelock.sol
  22. 1 1
      contracts/token/ERC20/extensions/ERC20FlashMint.sol
  23. 11 5
      contracts/utils/Address.sol
  24. 72 0
      contracts/utils/Timers.sol
  25. 4 1
      hardhat.config.js
  26. 112 184
      package-lock.json
  27. 1 1
      package.json
  28. 16 10
      scripts/inheritanceOrdering.js
  29. 819 0
      test/governance/Governor.test.js
  30. 133 0
      test/governance/GovernorWorkflow.behavior.js
  31. 430 0
      test/governance/compatibility/GovernorCompatibilityBravo.test.js
  32. 87 0
      test/governance/extensions/GovernorComp.test.js
  33. 432 0
      test/governance/extensions/GovernorTimelockCompound.test.js
  34. 369 0
      test/governance/extensions/GovernorTimelockControl.test.js
  35. 122 0
      test/governance/extensions/GovernorWeightQuorumFraction.test.js
  36. 24 0
      test/helpers/enums.js
  37. 55 0
      test/utils/TimersBlockNumberImpl.test.js
  38. 55 0
      test/utils/TimersTimestamp.test.js
  39. 24 0
      test/utils/introspection/SupportsInterface.behavior.js

+ 2 - 1
CHANGELOG.md

@@ -4,6 +4,7 @@
 
  * `ERC2771Context`: use private variable from storage to store the forwarder address. Fixes issues where `_msgSender()` was not callable from constructors. ([#2754](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2754))
  * `EnumerableSet`: add `values()` functions that returns an array containing all values in a single call. ([#2768](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2768))
+ * `Governor`: added a modular system of `Governor` contracts based on `GovernorAlpha` and `GovernorBravo`. ([#2672](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2672))
 
 ## 4.2.0 (2021-06-30)
 
@@ -18,7 +19,7 @@
  * `ERC1155Supply`: add a new `ERC1155` extension that keeps track of the totalSupply of each tokenId. ([#2593](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2593))
  * `BitMaps`: add a new `BitMaps` library that provides a storage efficient datastructure for `uint256` to `bool` mapping with contiguous keys. ([#2710](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2710))
 
- ### Breaking Changes
+### Breaking Changes
 
  * `ERC20FlashMint` is no longer a Draft ERC. ([#2673](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2673)))
 

+ 357 - 0
contracts/governance/Governor.sol

@@ -0,0 +1,357 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.0;
+
+import "../utils/cryptography/ECDSA.sol";
+import "../utils/cryptography/draft-EIP712.sol";
+import "../utils/introspection/ERC165.sol";
+import "../utils/math/SafeCast.sol";
+import "../utils/Address.sol";
+import "../utils/Context.sol";
+import "../utils/Timers.sol";
+import "./IGovernor.sol";
+
+/**
+ * @dev Core of the governance system, designed to be extended though various modules.
+ *
+ * This contract is abstract and requiers several function to be implemented in various modules:
+ *
+ * - A counting module must implement {quorum}, {_quorumReached}, {_voteSucceeded} and {_countVote}
+ * - A voting module must implement {getVotes}
+ * - Additionanly, the {votingPeriod} must also be implemented
+ *
+ * _Available since v4.3._
+ */
+abstract contract Governor is Context, ERC165, EIP712, IGovernor {
+    using SafeCast for uint256;
+    using Timers for Timers.BlockNumber;
+
+    bytes32 public constant BALLOT_TYPEHASH = keccak256("Ballot(uint256 proposalId,uint8 support)");
+
+    struct ProposalCore {
+        Timers.BlockNumber voteStart;
+        Timers.BlockNumber voteEnd;
+        bool executed;
+        bool canceled;
+    }
+
+    string private _name;
+
+    mapping(uint256 => ProposalCore) private _proposals;
+
+    /**
+     * @dev Restrict access to governor executing address. Some module might override the _executor function to make
+     * sure this modifier is consistant with the execution model.
+     */
+    modifier onlyGovernance() {
+        require(_msgSender() == _executor(), "Governor: onlyGovernance");
+        _;
+    }
+
+    /**
+     * @dev Sets the value for {name} and {version}
+     */
+    constructor(string memory name_) EIP712(name_, version()) {
+        _name = name_;
+    }
+
+    /**
+     * @dev See {IERC165-supportsInterface}.
+     */
+    function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165) returns (bool) {
+        return interfaceId == type(IGovernor).interfaceId || super.supportsInterface(interfaceId);
+    }
+
+    /**
+     * @dev See {IGovernor-name}.
+     */
+    function name() public view virtual override returns (string memory) {
+        return _name;
+    }
+
+    /**
+     * @dev See {IGovernor-version}.
+     */
+    function version() public view virtual override returns (string memory) {
+        return "1";
+    }
+
+    /**
+     * @dev See {IGovernor-hashProposal}.
+     *
+     * The proposal id is produced by hashing the RLC encoded `targets` array, the `values` array, the `calldatas` array
+     * and the descriptionHash (bytes32 which itself is the keccak256 hash of the description string). This proposal id
+     * can be produced from the proposal data which is part of the {ProposalCreated} event. It can even be computed in
+     * advance, before the proposal is submitted.
+     *
+     * Note that the chainId and the governor address are not part of the proposal id computation. Consequently, the
+     * same proposal (with same operation and same description) will have the same id if submitted on multiple governors
+     * accross multiple networks. This also means that in order to execute the same operation twice (on the same
+     * governor) the proposer will have to change the description in order to avoid proposal id conflicts.
+     */
+    function hashProposal(
+        address[] memory targets,
+        uint256[] memory values,
+        bytes[] memory calldatas,
+        bytes32 descriptionHash
+    ) public pure virtual override returns (uint256) {
+        return uint256(keccak256(abi.encode(targets, values, calldatas, descriptionHash)));
+    }
+
+    /**
+     * @dev See {IGovernor-state}.
+     */
+    function state(uint256 proposalId) public view virtual override returns (ProposalState) {
+        ProposalCore memory proposal = _proposals[proposalId];
+
+        if (proposal.executed) {
+            return ProposalState.Executed;
+        } else if (proposal.canceled) {
+            return ProposalState.Canceled;
+        } else if (proposal.voteStart.isPending()) {
+            return ProposalState.Pending;
+        } else if (proposal.voteEnd.isPending()) {
+            return ProposalState.Active;
+        } else if (proposal.voteEnd.isExpired()) {
+            return
+                _quorumReached(proposalId) && _voteSucceeded(proposalId)
+                    ? ProposalState.Succeeded
+                    : ProposalState.Defeated;
+        } else {
+            revert("Governor: unknown proposal id");
+        }
+    }
+
+    /**
+     * @dev See {IGovernor-proposalSnapshot}.
+     */
+    function proposalSnapshot(uint256 proposalId) public view virtual override returns (uint256) {
+        return _proposals[proposalId].voteStart.getDeadline();
+    }
+
+    /**
+     * @dev See {IGovernor-proposalDeadline}.
+     */
+    function proposalDeadline(uint256 proposalId) public view virtual override returns (uint256) {
+        return _proposals[proposalId].voteEnd.getDeadline();
+    }
+
+    /**
+     * @dev See {IGovernor-votingDelay}
+     */
+    function votingDelay() public view virtual override returns (uint256);
+
+    /**
+     * @dev See {IGovernor-votingPeriod}
+     */
+    function votingPeriod() public view virtual override returns (uint256);
+
+    /**
+     * @dev See {IGovernor-quorum}
+     */
+    function quorum(uint256 blockNumber) public view virtual override returns (uint256);
+
+    /**
+     * @dev See {IGovernor-getVotes}
+     */
+    function getVotes(address account, uint256 blockNumber) public view virtual override returns (uint256);
+
+    /**
+     * @dev Amount of votes already casted passes the threshold limit.
+     */
+    function _quorumReached(uint256 proposalId) internal view virtual returns (bool);
+
+    /**
+     * @dev Is the proposal successful or not.
+     */
+    function _voteSucceeded(uint256 proposalId) internal view virtual returns (bool);
+
+    /**
+     * @dev Register a vote with a given support and voting weight.
+     *
+     * Note: Support is generic and can represent various things depending on the voting system used.
+     */
+    function _countVote(
+        uint256 proposalId,
+        address account,
+        uint8 support,
+        uint256 weight
+    ) internal virtual;
+
+    /**
+     * @dev See {IGovernor-propose}.
+     */
+    function propose(
+        address[] memory targets,
+        uint256[] memory values,
+        bytes[] memory calldatas,
+        string memory description
+    ) public virtual override returns (uint256) {
+        uint256 proposalId = hashProposal(targets, values, calldatas, keccak256(bytes(description)));
+
+        require(targets.length == values.length, "Governor: invalid proposal length");
+        require(targets.length == calldatas.length, "Governor: invalid proposal length");
+        require(targets.length > 0, "Governor: empty proposal");
+
+        ProposalCore storage proposal = _proposals[proposalId];
+        require(proposal.voteStart.isUnset(), "Governor: proposal already exists");
+
+        uint64 snapshot = block.number.toUint64() + votingDelay().toUint64();
+        uint64 deadline = snapshot + votingPeriod().toUint64();
+
+        proposal.voteStart.setDeadline(snapshot);
+        proposal.voteEnd.setDeadline(deadline);
+
+        emit ProposalCreated(
+            proposalId,
+            _msgSender(),
+            targets,
+            values,
+            new string[](targets.length),
+            calldatas,
+            snapshot,
+            deadline,
+            description
+        );
+
+        return proposalId;
+    }
+
+    /**
+     * @dev See {IGovernor-execute}.
+     */
+    function execute(
+        address[] memory targets,
+        uint256[] memory values,
+        bytes[] memory calldatas,
+        bytes32 descriptionHash
+    ) public payable virtual override returns (uint256) {
+        uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash);
+
+        ProposalState status = state(proposalId);
+        require(
+            status == ProposalState.Succeeded || status == ProposalState.Queued,
+            "Governor: proposal not successful"
+        );
+        _proposals[proposalId].executed = true;
+
+        emit ProposalExecuted(proposalId);
+
+        _execute(proposalId, targets, values, calldatas, descriptionHash);
+
+        return proposalId;
+    }
+
+    /**
+     * @dev Internal execution mechanism. Can be overriden to implement different execution mechanism
+     */
+    function _execute(
+        uint256, /* proposalId */
+        address[] memory targets,
+        uint256[] memory values,
+        bytes[] memory calldatas,
+        bytes32 /*descriptionHash*/
+    ) internal virtual {
+        string memory errorMessage = "Governor: call reverted without message";
+        for (uint256 i = 0; i < targets.length; ++i) {
+            (bool success, bytes memory returndata) = targets[i].call{value: values[i]}(calldatas[i]);
+            Address.verifyCallResult(success, returndata, errorMessage);
+        }
+    }
+
+    /**
+     * @dev Internal cancel mechanism: locks up the proposal timer, preventing it from being re-submitted. Marks it as
+     * canceled to allow distinguishing it from executed proposals.
+     *
+     * Emits a {IGovernor-ProposalCanceled} event.
+     */
+    function _cancel(
+        address[] memory targets,
+        uint256[] memory values,
+        bytes[] memory calldatas,
+        bytes32 descriptionHash
+    ) internal virtual returns (uint256) {
+        uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash);
+        ProposalState status = state(proposalId);
+
+        require(
+            status != ProposalState.Canceled && status != ProposalState.Expired && status != ProposalState.Executed,
+            "Governor: proposal not active"
+        );
+        _proposals[proposalId].canceled = true;
+
+        emit ProposalCanceled(proposalId);
+
+        return proposalId;
+    }
+
+    /**
+     * @dev See {IGovernor-castVote}.
+     */
+    function castVote(uint256 proposalId, uint8 support) public virtual override returns (uint256) {
+        address voter = _msgSender();
+        return _castVote(proposalId, voter, support, "");
+    }
+
+    /**
+     * @dev See {IGovernor-castVoteWithReason}.
+     */
+    function castVoteWithReason(
+        uint256 proposalId,
+        uint8 support,
+        string calldata reason
+    ) public virtual override returns (uint256) {
+        address voter = _msgSender();
+        return _castVote(proposalId, voter, support, reason);
+    }
+
+    /**
+     * @dev See {IGovernor-castVoteBySig}.
+     */
+    function castVoteBySig(
+        uint256 proposalId,
+        uint8 support,
+        uint8 v,
+        bytes32 r,
+        bytes32 s
+    ) public virtual override returns (uint256) {
+        address voter = ECDSA.recover(
+            _hashTypedDataV4(keccak256(abi.encode(BALLOT_TYPEHASH, proposalId, support))),
+            v,
+            r,
+            s
+        );
+        return _castVote(proposalId, voter, support, "");
+    }
+
+    /**
+     * @dev Internal vote casting mechanism: Check that the vote is pending, that it has not been casted yet, retrieve
+     * voting weight using {IGovernor-getVotes} and call the {_countVote} internal function.
+     *
+     * Emits a {IGovernor-VoteCast} event.
+     */
+    function _castVote(
+        uint256 proposalId,
+        address account,
+        uint8 support,
+        string memory reason
+    ) internal virtual returns (uint256) {
+        ProposalCore storage proposal = _proposals[proposalId];
+        require(state(proposalId) == ProposalState.Active, "Governor: vote not currently active");
+
+        uint256 weight = getVotes(account, proposal.voteStart.getDeadline());
+        _countVote(proposalId, account, support, weight);
+
+        emit VoteCast(account, proposalId, support, weight, reason);
+
+        return weight;
+    }
+
+    /**
+     * @dev Address through which the governor executes action. Will be overloaded by module that execute actions
+     * through another contract such as a timelock.
+     */
+    function _executor() internal view virtual returns (address) {
+        return address(this);
+    }
+}

+ 214 - 0
contracts/governance/IGovernor.sol

@@ -0,0 +1,214 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.0;
+
+import "../utils/introspection/ERC165.sol";
+
+/**
+ * @dev Interface of the {Governor} core.
+ *
+ * _Available since v4.3._
+ */
+interface IGovernor is IERC165 {
+    enum ProposalState {
+        Pending,
+        Active,
+        Canceled,
+        Defeated,
+        Succeeded,
+        Queued,
+        Expired,
+        Executed
+    }
+
+    /**
+     * @dev Emitted when a proposal is created.
+     */
+    event ProposalCreated(
+        uint256 proposalId,
+        address proposer,
+        address[] targets,
+        uint256[] values,
+        string[] signatures,
+        bytes[] calldatas,
+        uint256 startBlock,
+        uint256 endBlock,
+        string description
+    );
+
+    /**
+     * @dev Emitted when a proposal is canceled.
+     */
+    event ProposalCanceled(uint256 proposalId);
+
+    /**
+     * @dev Emitted when a proposal is executed.
+     */
+    event ProposalExecuted(uint256 proposalId);
+
+    /**
+     * @dev Emitted when a vote is casted.
+     *
+     * Note: `support` values should be seen as buckets. There interpretation depends on the voting module used.
+     */
+    event VoteCast(address indexed voter, uint256 proposalId, uint8 support, uint256 weight, string reason);
+
+    /**
+     * @notice module:core
+     * @dev Name of the governor instance (used in building the ERC712 domain separator).
+     */
+    function name() external view returns (string memory);
+
+    /**
+     * @notice module:core
+     * @dev Version of the governor instance (used in building the ERC712 domain separator). Default: "1"
+     */
+    function version() external view returns (string memory);
+
+    /**
+     * @notice module:voting
+     * @dev A description of the possible `support` values for {castVote} and the way these votes are counted, meant to
+     * be consumed by UIs to show correct vote options and interpret the results. The string is a URL-encoded sequence of
+     * key-value pairs that each describe one aspect, for example `support=bravo&quorum=for,abstain`.
+     *
+     * There are 2 standard keys: `support` and `quorum`.
+     *
+     * - `support=bravo` refers to the vote options 0 = For, 1 = Against, 2 = Abstain, as in `GovernorBravo`.
+     * - `quorum=bravo` means that only For votes are counted towards quorum.
+     * - `quorum=for,abstain` means that both For and Abstain votes are counted towards quorum.
+     *
+     * NOTE: The string can be decoded by the standard
+     * https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams[`URLSearchParams`]
+     * JavaScript class.
+     */
+    // solhint-disable-next-line func-name-mixedcase
+    function COUNTING_MODE() external pure returns (string memory);
+
+    /**
+     * @notice module:core
+     * @dev Hashing function used to (re)build the proposal id from the proposal details..
+     */
+    function hashProposal(
+        address[] calldata targets,
+        uint256[] calldata values,
+        bytes[] calldata calldatas,
+        bytes32 descriptionHash
+    ) external pure returns (uint256);
+
+    /**
+     * @notice module:core
+     * @dev Current state of a proposal, following Compound's convention
+     */
+    function state(uint256 proposalId) external view returns (ProposalState);
+
+    /**
+     * @notice module:core
+     * @dev block number used to retrieve user's votes and quorum.
+     */
+    function proposalSnapshot(uint256 proposalId) external view returns (uint256);
+
+    /**
+     * @notice module:core
+     * @dev timestamp at which votes close.
+     */
+    function proposalDeadline(uint256 proposalId) external view returns (uint256);
+
+    /**
+     * @notice module:user-config
+     * @dev delay, in number of block, between the proposal is created and the vote starts. This can be increassed to
+     * leave time for users to buy voting power, of delegate it, before the voting of a proposal starts.
+     */
+    function votingDelay() external view returns (uint256);
+
+    /**
+     * @notice module:user-config
+     * @dev delay, in number of blocks, between the vote start and vote ends.
+     *
+     * Note: the {votingDelay} can delay the start of the vote. This must be considered when setting the voting
+     * duration compared to the voting delay.
+     */
+    function votingPeriod() external view returns (uint256);
+
+    /**
+     * @notice module:user-config
+     * @dev Minimum number of casted voted requiered for a proposal to be successful.
+     *
+     * Note: The `blockNumber` parameter corresponds to the snaphot used for counting vote. This allows to scale the
+     * quroum depending on values such as the totalSupply of a token at this block (see {ERC20Votes}).
+     */
+    function quorum(uint256 blockNumber) external view returns (uint256);
+
+    /**
+     * @notice module:reputation
+     * @dev Voting power of an `account` at a specific `blockNumber`.
+     *
+     * Note: this can be implemented in a number of ways, for example by reading the delegated balance from one (or
+     * multiple), {ERC20Votes} tokens.
+     */
+    function getVotes(address account, uint256 blockNumber) external view returns (uint256);
+
+    /**
+     * @notice module:voting
+     * @dev Returns weither `account` has casted a vote on `proposalId`.
+     */
+    function hasVoted(uint256 proposalId, address account) external view returns (bool);
+
+    /**
+     * @dev Create a new proposal. Vote start {IGovernor-votingDelay} blocks after the proposal is created and ends
+     * {IGovernor-votingPeriod} blocks after the voting starts.
+     *
+     * Emits a {ProposalCreated} event.
+     */
+    function propose(
+        address[] memory targets,
+        uint256[] memory values,
+        bytes[] memory calldatas,
+        string memory description
+    ) external returns (uint256 proposalId);
+
+    /**
+     * @dev Execute a successful proposal. This requiers the quorum to be reached, the vote to be successful, and the
+     * deadline to be reached.
+     *
+     * Emits a {ProposalExecuted} event.
+     *
+     * Note: some module can modify the requierements for execution, for example by adding an additional timelock.
+     */
+    function execute(
+        address[] memory targets,
+        uint256[] memory values,
+        bytes[] memory calldatas,
+        bytes32 descriptionHash
+    ) external payable returns (uint256 proposalId);
+
+    /**
+     * @dev Cast a vote
+     *
+     * Emits a {VoteCast} event.
+     */
+    function castVote(uint256 proposalId, uint8 support) external returns (uint256 balance);
+
+    /**
+     * @dev Cast a with a reason
+     *
+     * Emits a {VoteCast} event.
+     */
+    function castVoteWithReason(
+        uint256 proposalId,
+        uint8 support,
+        string calldata reason
+    ) external returns (uint256 balance);
+
+    /**
+     * @dev Cast a vote using the user cryptographic signature.
+     *
+     * Emits a {VoteCast} event.
+     */
+    function castVoteBySig(
+        uint256 proposalId,
+        uint8 support,
+        uint8 v,
+        bytes32 r,
+        bytes32 s
+    ) external returns (uint256 balance);
+}

+ 56 - 2
contracts/governance/README.adoc

@@ -3,10 +3,64 @@
 [.readme-notice]
 NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/governance
 
-This directory includes primitives for on-chain governance. We currently only offer the {TimelockController} contract, that can be used as a component in a governance system to introduce a delay between a proposal and its execution.
+This directory includes primitives for on-chain governance.
+
+== Governor
+
+The {Governor} contract provides primitive to set an on-chain voting system similar to https://compound.finance/docs/governance[Compound's Governor Alpha & Bravo].
+
+Similarly to our other contracts, it is customizable through inheritance and comes with extensions:
+
+* {GovernorTimelockControl}: A {Governor} extension that performs executions through a {TimelockController}. This requires a successful proposal to be queued before then can be executed. The {TimelockController} will enforce a delay between the queueing and the execution. With this module, proposals are executed by the external {TimelockController} contract, which would have to hold the assets that are being governed.
+
+* {GovernorTimelockCompound}: A {Governor} extension that performs executions through a compound https://github.com/compound-finance/compound-protocol/blob/master/contracts/Timelock.sol[`Timelock`]. This requires a successful proposal to be queued before then can be executed. The `Timelock` will enforce a delay between the queueing and the execution. With this module, proposals are executed by the external `Timelock` contract, which would have to hold the assets that are being governed.
+
+* {GovernorCountingSimple}: A simple voting mechanism for {Governor} with support 3 vote options: Against, For and Abstain.
+
+* {GovernorVotes}: Binding to extract voting weight from an {ERC20Votes} token.
+
+* {GovernorVotesQuorumFraction}: Binding to extract voting weight from an {ERC20Votes} token and set the quorum as a fraction of the (snapshoted) total token supply.
+
+* {GovernorVotesComp}: Binding to extract voting weight from a Comp or {ERC20VotesComp} token.
+
+In addition to modules, the {Governor} requires a few virtual functions to be implemented to your particular specifications:
+
+* <<Governor-votingOffset-,`votingOffset()`>>: Delay (number of blocks), between the proposal, is submitted and the snapshot block used for voting. This can be used to enforce a delay after a proposal is published for users to buy tokens, or delegate their votes (default: 0).
+* <<Governor-votingDuration-,`votingDuration()`>>: Delay (in seconds), between the proposal, is submitted and the vote ends.
+* <<Governor-quorum-uint256-,`quorum(uint256 blockNumber)`>>: Quorum required for a proposal to be successful. This function includes a `blockNumber` argument so the quorum can adapt through time, for example, to follow a token's `totalSupply`.
+
+Note: Function of the {Governor} contract does NOT include access control. If you want to restrict access (for example to require a minimum balance to submit a proposal), you should add these checks by overloading the particular functions. For security reasons, the {Governor-_cancel} method is internal, and you will have to expose it (which the right access control mechanism) yourself if this is a mechanism you need.
+
+Events emitted by the {Governor} contract are compatible with Compound's `GovernorBravo`. Additionnaly, function compatibility can be added using the {GovernorCompatibilityBravo} compatibility layer. This layer includes a voting system but does not include token bindings. This layer also requiers a timelock module (either {GovernorTimelockControl} or {GovernorTimelockCompound}).
+
+=== Core
+
+{{IGovernor}}
+
+{{Governor}}
+
+=== Extensions
+
+{{GovernorTimelockControl}}
+
+{{GovernorTimelockCompound}}
+
+{{GovernorCountingSimple}}
+
+{{GovernorVotes}}
+
+{{GovernorVotesQuorumFraction}}
+
+{{GovernorVotesComp}}
+
+=== Compatibility
+
+{{GovernorCompatibilityBravo}}
 
 == Timelock
 
+In a governance systems, the {TimelockController} contract is in carge of introducing a delay between a proposal and its execution. It can be used with or without a {Governor}.
+
 {{TimelockController}}
 
 [[timelock-terminology]]
@@ -27,7 +81,7 @@ This directory includes primitives for on-chain governance. We currently only of
 [[timelock-operation]]
 ==== Operation structure
 
-Operation executed by the xref:api:governance.adoc#TimelockController[`TimelockControler`] can contain one or multiple subsequent calls. Depending on whether you need to multiple calls to be executed atomically, you can either use simple or batched operations.
+Operation executed by the xref:api:governance.adoc#TimelockController[`TimelockController`] can contain one or multiple subsequent calls. Depending on whether you need to multiple calls to be executed atomically, you can either use simple or batched operations.
 
 Both operations contain:
 

+ 285 - 0
contracts/governance/compatibility/GovernorCompatibilityBravo.sol

@@ -0,0 +1,285 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.0;
+
+import "../../utils/Counters.sol";
+import "../../utils/math/SafeCast.sol";
+import "../extensions/IGovernorTimelock.sol";
+import "../Governor.sol";
+import "./IGovernorCompatibilityBravo.sol";
+
+/**
+ * @dev Compatibility layer that implements GovernorBravo compatibility on to of {Governor}.
+ *
+ * This compatibility layer includes a voting system and requires a {IGovernorTimelock} compatible module to be added
+ * through inheritance. It does not include token bindings, not does it include any variable upgrade patterns.
+ *
+ * _Available since v4.3._
+ */
+abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorCompatibilityBravo, Governor {
+    using Counters for Counters.Counter;
+    using Timers for Timers.BlockNumber;
+
+    enum VoteType {
+        Against,
+        For,
+        Abstain
+    }
+
+    struct ProposalDetails {
+        address proposer;
+        address[] targets;
+        uint256[] values;
+        string[] signatures;
+        bytes[] calldatas;
+        uint256 forVotes;
+        uint256 againstVotes;
+        uint256 abstainVotes;
+        mapping(address => Receipt) receipts;
+        bytes32 descriptionHash;
+    }
+
+    mapping(uint256 => ProposalDetails) private _proposalDetails;
+
+    // public for hooking
+    function proposalThreshold() public view virtual override returns (uint256);
+
+    // public for hooking
+    function proposalEta(uint256 proposalId) public view virtual override returns (uint256);
+
+    // public for hooking
+    function queue(
+        address[] memory targets,
+        uint256[] memory values,
+        bytes[] memory calldatas,
+        bytes32 descriptionHash
+    ) public virtual override returns (uint256);
+
+    // solhint-disable-next-line func-name-mixedcase
+    function COUNTING_MODE() public pure virtual override returns (string memory) {
+        return "support=bravo&quorum=bravo";
+    }
+
+    // ============================================== Proposal lifecycle ==============================================
+    /**
+     * @dev See {IGovernor-propose}.
+     */
+    function propose(
+        address[] memory targets,
+        uint256[] memory values,
+        bytes[] memory calldatas,
+        string memory description
+    ) public virtual override(IGovernor, Governor) returns (uint256) {
+        return propose(targets, values, new string[](calldatas.length), calldatas, description);
+    }
+
+    /**
+     * @dev See {IGovernorCompatibilityBravo-propose}.
+     */
+    function propose(
+        address[] memory targets,
+        uint256[] memory values,
+        string[] memory signatures,
+        bytes[] memory calldatas,
+        string memory description
+    ) public virtual override returns (uint256) {
+        require(
+            getVotes(msg.sender, block.number - 1) >= proposalThreshold(),
+            "GovernorCompatibilityBravo: proposer votes below proposal threshold"
+        );
+
+        uint256 proposalId = super.propose(targets, values, _encodeCalldata(signatures, calldatas), description);
+        _storeProposal(proposalId, _msgSender(), targets, values, signatures, calldatas, description);
+        return proposalId;
+    }
+
+    /**
+     * @dev See {IGovernorCompatibilityBravo-queue}.
+     */
+    function queue(uint256 proposalId) public virtual override {
+        ProposalDetails storage details = _proposalDetails[proposalId];
+        queue(
+            details.targets,
+            details.values,
+            _encodeCalldata(details.signatures, details.calldatas),
+            details.descriptionHash
+        );
+    }
+
+    /**
+     * @dev See {IGovernorCompatibilityBravo-execute}.
+     */
+    function execute(uint256 proposalId) public payable virtual override {
+        ProposalDetails storage details = _proposalDetails[proposalId];
+        execute(
+            details.targets,
+            details.values,
+            _encodeCalldata(details.signatures, details.calldatas),
+            details.descriptionHash
+        );
+    }
+
+    /**
+     * @dev Encodes calldatas with optional function signature.
+     */
+    function _encodeCalldata(string[] memory signatures, bytes[] memory calldatas)
+        private
+        pure
+        returns (bytes[] memory)
+    {
+        bytes[] memory fullcalldatas = new bytes[](calldatas.length);
+
+        for (uint256 i = 0; i < signatures.length; ++i) {
+            fullcalldatas[i] = bytes(signatures[i]).length == 0
+                ? calldatas[i]
+                : abi.encodePacked(bytes4(keccak256(bytes(signatures[i]))), calldatas[i]);
+        }
+
+        return fullcalldatas;
+    }
+
+    /**
+     * @dev Store proposal metadata for later lookup
+     */
+    function _storeProposal(
+        uint256 proposalId,
+        address proposer,
+        address[] memory targets,
+        uint256[] memory values,
+        string[] memory signatures,
+        bytes[] memory calldatas,
+        string memory description
+    ) private {
+        ProposalDetails storage details = _proposalDetails[proposalId];
+
+        details.proposer = proposer;
+        details.targets = targets;
+        details.values = values;
+        details.signatures = signatures;
+        details.calldatas = calldatas;
+        details.descriptionHash = keccak256(bytes(description));
+    }
+
+    // ==================================================== Views =====================================================
+    /**
+     * @dev See {IGovernorCompatibilityBravo-proposals}.
+     */
+    function proposals(uint256 proposalId)
+        public
+        view
+        virtual
+        override
+        returns (
+            uint256 id,
+            address proposer,
+            uint256 eta,
+            uint256 startBlock,
+            uint256 endBlock,
+            uint256 forVotes,
+            uint256 againstVotes,
+            uint256 abstainVotes,
+            bool canceled,
+            bool executed
+        )
+    {
+        id = proposalId;
+        eta = proposalEta(proposalId);
+        startBlock = proposalSnapshot(proposalId);
+        endBlock = proposalDeadline(proposalId);
+
+        ProposalDetails storage details = _proposalDetails[proposalId];
+        proposer = details.proposer;
+        forVotes = details.forVotes;
+        againstVotes = details.againstVotes;
+        abstainVotes = details.abstainVotes;
+
+        ProposalState status = state(proposalId);
+        canceled = status == ProposalState.Canceled;
+        executed = status == ProposalState.Executed;
+    }
+
+    /**
+     * @dev See {IGovernorCompatibilityBravo-getActions}.
+     */
+    function getActions(uint256 proposalId)
+        public
+        view
+        virtual
+        override
+        returns (
+            address[] memory targets,
+            uint256[] memory values,
+            string[] memory signatures,
+            bytes[] memory calldatas
+        )
+    {
+        ProposalDetails storage details = _proposalDetails[proposalId];
+        return (details.targets, details.values, details.signatures, details.calldatas);
+    }
+
+    /**
+     * @dev See {IGovernorCompatibilityBravo-getReceipt}.
+     */
+    function getReceipt(uint256 proposalId, address voter) public view virtual override returns (Receipt memory) {
+        return _proposalDetails[proposalId].receipts[voter];
+    }
+
+    /**
+     * @dev See {IGovernorCompatibilityBravo-quorumVotes}.
+     */
+    function quorumVotes() public view virtual override returns (uint256) {
+        return quorum(block.number - 1);
+    }
+
+    // ==================================================== Voting ====================================================
+    /**
+     * @dev See {IGovernor-hasVoted}.
+     */
+    function hasVoted(uint256 proposalId, address account) public view virtual override returns (bool) {
+        return _proposalDetails[proposalId].receipts[account].hasVoted;
+    }
+
+    /**
+     * @dev See {Governor-_quorumReached}. In this module, only forVotes count toward the quorum.
+     */
+    function _quorumReached(uint256 proposalId) internal view virtual override returns (bool) {
+        ProposalDetails storage details = _proposalDetails[proposalId];
+        return quorum(proposalSnapshot(proposalId)) < details.forVotes;
+    }
+
+    /**
+     * @dev See {Governor-_voteSucceeded}. In this module, the forVotes must be scritly over the againstVotes.
+     */
+    function _voteSucceeded(uint256 proposalId) internal view virtual override returns (bool) {
+        ProposalDetails storage details = _proposalDetails[proposalId];
+        return details.forVotes > details.againstVotes;
+    }
+
+    /**
+     * @dev See {Governor-_countVote}. In this module, the support follows Governor Bravo.
+     */
+    function _countVote(
+        uint256 proposalId,
+        address account,
+        uint8 support,
+        uint256 weight
+    ) internal virtual override {
+        ProposalDetails storage details = _proposalDetails[proposalId];
+        Receipt storage receipt = details.receipts[account];
+
+        require(!receipt.hasVoted, "GovernorCompatibilityBravo: vote already casted");
+        receipt.hasVoted = true;
+        receipt.support = support;
+        receipt.votes = SafeCast.toUint96(weight);
+
+        if (support == uint8(VoteType.Against)) {
+            details.againstVotes += weight;
+        } else if (support == uint8(VoteType.For)) {
+            details.forVotes += weight;
+        } else if (support == uint8(VoteType.Abstain)) {
+            details.abstainVotes += weight;
+        } else {
+            revert("GovernorCompatibilityBravo: invalid vote type");
+        }
+    }
+}

+ 111 - 0
contracts/governance/compatibility/IGovernorCompatibilityBravo.sol

@@ -0,0 +1,111 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.0;
+
+import "../IGovernor.sol";
+
+/**
+ * @dev Interface extension that adds missing functions to the {Governor} core to provide `GovernorBravo` compatibility.
+ *
+ * _Available since v4.3._
+ */
+interface IGovernorCompatibilityBravo is IGovernor {
+    /**
+     * @dev Proposal structure from Compound Governor Bravo. Not actually used by the compatibility layer, as
+     * {{proposal}} returns a very different structure.
+     */
+    struct Proposal {
+        uint256 id;
+        address proposer;
+        uint256 eta;
+        address[] targets;
+        uint256[] values;
+        string[] signatures;
+        bytes[] calldatas;
+        uint256 startBlock;
+        uint256 endBlock;
+        uint256 forVotes;
+        uint256 againstVotes;
+        uint256 abstainVotes;
+        bool canceled;
+        bool executed;
+        mapping(address => Receipt) receipts;
+    }
+
+    /**
+     * @dev Receipt structure from Compound Governor Bravo
+     */
+    struct Receipt {
+        bool hasVoted;
+        uint8 support;
+        uint96 votes;
+    }
+
+    /**
+     * @dev Part of the Governor Bravo's interface.
+     */
+    function quorumVotes() external view returns (uint256);
+
+    /**
+     * @dev Part of the Governor Bravo's interface: _"The official record of all proposals ever proposed"_.
+     */
+    function proposals(uint256)
+        external
+        view
+        returns (
+            uint256 id,
+            address proposer,
+            uint256 eta,
+            uint256 startBlock,
+            uint256 endBlock,
+            uint256 forVotes,
+            uint256 againstVotes,
+            uint256 abstainVotes,
+            bool canceled,
+            bool executed
+        );
+
+    /**
+     * @dev Part of the Governor Bravo's interface: _"Function used to propose a new proposal"_.
+     */
+    function propose(
+        address[] memory targets,
+        uint256[] memory values,
+        string[] memory signatures,
+        bytes[] memory calldatas,
+        string memory description
+    ) external returns (uint256);
+
+    /**
+     * @dev Part of the Governor Bravo's interface: _"Queues a proposal of state succeeded"_.
+     */
+    function queue(uint256 proposalId) external;
+
+    /**
+     * @dev Part of the Governor Bravo's interface: _"Executes a queued proposal if eta has passed"_.
+     */
+    function execute(uint256 proposalId) external payable;
+
+    /**
+     * @dev Part of the Governor Bravo's interface: _"Gets actions of a proposal"_.
+     */
+    function getActions(uint256 proposalId)
+        external
+        view
+        returns (
+            address[] memory targets,
+            uint256[] memory values,
+            string[] memory signatures,
+            bytes[] memory calldatas
+        );
+
+    /**
+     * @dev Part of the Governor Bravo's interface: _"Gets the receipt for a voter on a given proposal"_.
+     */
+    function getReceipt(uint256 proposalId, address voter) external view returns (Receipt memory);
+
+    /**
+     * @dev Part of the Governor Bravo's interface: _"The number of votes required in order for a voter to become a proposer"_.
+     */
+    function proposalThreshold() external view returns (uint256);
+}

+ 107 - 0
contracts/governance/extensions/GovernorCountingSimple.sol

@@ -0,0 +1,107 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.0;
+
+import "../Governor.sol";
+
+/**
+ * @dev Extension of {Governor} for simple, 3 options, vote counting.
+ *
+ * _Available since v4.3._
+ */
+abstract contract GovernorCountingSimple is Governor {
+    /**
+     * @dev Supported vote types. Matches Governor Bravo ordering.
+     */
+    enum VoteType {
+        Against,
+        For,
+        Abstain
+    }
+
+    struct ProposalVote {
+        uint256 againstVotes;
+        uint256 forVotes;
+        uint256 abstainVotes;
+        mapping(address => bool) hasVoted;
+    }
+
+    mapping(uint256 => ProposalVote) private _proposalVotes;
+
+    /**
+     * @dev See {IGovernor-COUNTING_MODE}.
+     */
+    // solhint-disable-next-line func-name-mixedcase
+    function COUNTING_MODE() public pure virtual override returns (string memory) {
+        return "support=bravo&quorum=for,abstain";
+    }
+
+    /**
+     * @dev See {IGovernor-hasVoted}.
+     */
+    function hasVoted(uint256 proposalId, address account) public view virtual override returns (bool) {
+        return _proposalVotes[proposalId].hasVoted[account];
+    }
+
+    /**
+     * @dev Accessor to the internal vote counts.
+     */
+    function proposalVotes(uint256 proposalId)
+        public
+        view
+        virtual
+        returns (
+            uint256 againstVotes,
+            uint256 forVotes,
+            uint256 abstainVotes
+        )
+    {
+        ProposalVote storage proposalvote = _proposalVotes[proposalId];
+        return (proposalvote.againstVotes, proposalvote.forVotes, proposalvote.abstainVotes);
+    }
+
+    /**
+     * @dev See {Governor-_quorumReached}.
+     */
+    function _quorumReached(uint256 proposalId) internal view virtual override returns (bool) {
+        ProposalVote storage proposalvote = _proposalVotes[proposalId];
+
+        return
+            quorum(proposalSnapshot(proposalId)) <=
+            proposalvote.againstVotes + proposalvote.forVotes + proposalvote.abstainVotes;
+    }
+
+    /**
+     * @dev See {Governor-_voteSucceeded}. In this module, the forVotes must be scritly over the againstVotes.
+     */
+    function _voteSucceeded(uint256 proposalId) internal view virtual override returns (bool) {
+        ProposalVote storage proposalvote = _proposalVotes[proposalId];
+
+        return proposalvote.forVotes > proposalvote.againstVotes;
+    }
+
+    /**
+     * @dev See {Governor-_countVote}. In this module, the support follows the `VoteType` enum (from Governor Bravo).
+     */
+    function _countVote(
+        uint256 proposalId,
+        address account,
+        uint8 support,
+        uint256 weight
+    ) internal virtual override {
+        ProposalVote storage proposalvote = _proposalVotes[proposalId];
+
+        require(!proposalvote.hasVoted[account], "GovernorVotingSimple: vote already casted");
+        proposalvote.hasVoted[account] = true;
+
+        if (support == uint8(VoteType.Against)) {
+            proposalvote.againstVotes += weight;
+        } else if (support == uint8(VoteType.For)) {
+            proposalvote.forVotes += weight;
+        } else if (support == uint8(VoteType.Abstain)) {
+            proposalvote.abstainVotes += weight;
+        } else {
+            revert("GovernorVotingSimple: invalid value for enum VoteType");
+        }
+    }
+}

+ 242 - 0
contracts/governance/extensions/GovernorTimelockCompound.sol

@@ -0,0 +1,242 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.0;
+
+import "./IGovernorTimelock.sol";
+import "../Governor.sol";
+import "../../utils/math/SafeCast.sol";
+
+/**
+ * https://github.com/compound-finance/compound-protocol/blob/master/contracts/Timelock.sol[Compound's timelock] interface
+ */
+interface ICompoundTimelock {
+    receive() external payable;
+
+    // solhint-disable-next-line func-name-mixedcase
+    function GRACE_PERIOD() external view returns (uint256);
+
+    // solhint-disable-next-line func-name-mixedcase
+    function MINIMUM_DELAY() external view returns (uint256);
+
+    // solhint-disable-next-line func-name-mixedcase
+    function MAXIMUM_DELAY() external view returns (uint256);
+
+    function admin() external view returns (address);
+
+    function pendingAdmin() external view returns (address);
+
+    function delay() external view returns (uint256);
+
+    function queuedTransactions(bytes32) external view returns (bool);
+
+    function setDelay(uint256) external;
+
+    function acceptAdmin() external;
+
+    function setPendingAdmin(address) external;
+
+    function queueTransaction(
+        address target,
+        uint256 value,
+        string memory signature,
+        bytes memory data,
+        uint256 eta
+    ) external returns (bytes32);
+
+    function cancelTransaction(
+        address target,
+        uint256 value,
+        string memory signature,
+        bytes memory data,
+        uint256 eta
+    ) external;
+
+    function executeTransaction(
+        address target,
+        uint256 value,
+        string memory signature,
+        bytes memory data,
+        uint256 eta
+    ) external payable returns (bytes memory);
+}
+
+/**
+ * @dev Extension of {Governor} that binds the execution process to a Compound Timelock. This adds a delay, enforced by
+ * the external timelock to all successful proposal (in addition to the voting duration). The {Governor} needs to be
+ * the admin of the timelock for any operation to be performed. A public, unrestricted,
+ * {GovernorTimelockCompound-__acceptAdmin} is available to accept ownership of the timelock.
+ *
+ * Using this model means the proposal will be operated by the {TimelockController} and not by the {Governor}. Thus,
+ * the assets and permissions must be attached to the {TimelockController}. Any asset sent to the {Governor} will be
+ * inaccessible.
+ *
+ * _Available since v4.3._
+ */
+abstract contract GovernorTimelockCompound is IGovernorTimelock, Governor {
+    using SafeCast for uint256;
+    using Timers for Timers.Timestamp;
+
+    struct ProposalTimelock {
+        Timers.Timestamp timer;
+    }
+
+    ICompoundTimelock private _timelock;
+
+    mapping(uint256 => ProposalTimelock) private _proposalTimelocks;
+
+    /**
+     * @dev Emitted when the timelock controller used for proposal execution is modified.
+     */
+    event TimelockChange(address oldTimelock, address newTimelock);
+
+    /**
+     * @dev Set the timelock.
+     */
+    constructor(ICompoundTimelock timelockAddress) {
+        _updateTimelock(timelockAddress);
+    }
+
+    /**
+     * @dev See {IERC165-supportsInterface}.
+     */
+    function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, Governor) returns (bool) {
+        return interfaceId == type(IGovernorTimelock).interfaceId || super.supportsInterface(interfaceId);
+    }
+
+    /**
+     * @dev Overriden version of the {Governor-state} function with added support for the `Queued` and `Expired` status.
+     */
+    function state(uint256 proposalId) public view virtual override(IGovernor, Governor) returns (ProposalState) {
+        ProposalState status = super.state(proposalId);
+
+        if (status != ProposalState.Succeeded) {
+            return status;
+        }
+
+        uint256 eta = proposalEta(proposalId);
+        if (eta == 0) {
+            return status;
+        } else if (block.timestamp >= eta + _timelock.GRACE_PERIOD()) {
+            return ProposalState.Expired;
+        } else {
+            return ProposalState.Queued;
+        }
+    }
+
+    /**
+     * @dev Public accessor to check the address of the timelock
+     */
+    function timelock() public view virtual override returns (address) {
+        return address(_timelock);
+    }
+
+    /**
+     * @dev Public accessor to check the eta of a queued proposal
+     */
+    function proposalEta(uint256 proposalId) public view virtual override returns (uint256) {
+        return _proposalTimelocks[proposalId].timer.getDeadline();
+    }
+
+    /**
+     * @dev Function to queue a proposal to the timelock.
+     */
+    function queue(
+        address[] memory targets,
+        uint256[] memory values,
+        bytes[] memory calldatas,
+        bytes32 descriptionHash
+    ) public virtual override returns (uint256) {
+        uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash);
+
+        require(state(proposalId) == ProposalState.Succeeded, "Governor: proposal not successful");
+
+        uint256 eta = block.timestamp + _timelock.delay();
+        _proposalTimelocks[proposalId].timer.setDeadline(eta.toUint64());
+        for (uint256 i = 0; i < targets.length; ++i) {
+            require(
+                !_timelock.queuedTransactions(keccak256(abi.encode(targets[i], values[i], "", calldatas[i], eta))),
+                "GovernorTimelockCompound: identical proposal action already queued"
+            );
+            _timelock.queueTransaction(targets[i], values[i], "", calldatas[i], eta);
+        }
+
+        emit ProposalQueued(proposalId, eta);
+
+        return proposalId;
+    }
+
+    /**
+     * @dev Overriden execute function that run the already queued proposal through the timelock.
+     */
+    function _execute(
+        uint256 proposalId,
+        address[] memory targets,
+        uint256[] memory values,
+        bytes[] memory calldatas,
+        bytes32 /*descriptionHash*/
+    ) internal virtual override {
+        uint256 eta = proposalEta(proposalId);
+        require(eta > 0, "GovernorTimelockCompound: proposal not yet queued");
+        for (uint256 i = 0; i < targets.length; ++i) {
+            _timelock.executeTransaction{value: values[i]}(targets[i], values[i], "", calldatas[i], eta);
+        }
+    }
+
+    /**
+     * @dev Overriden version of the {Governor-_cancel} function to cancel the timelocked proposal if it as already
+     * been queued.
+     */
+    function _cancel(
+        address[] memory targets,
+        uint256[] memory values,
+        bytes[] memory calldatas,
+        bytes32 descriptionHash
+    ) internal virtual override returns (uint256) {
+        uint256 proposalId = super._cancel(targets, values, calldatas, descriptionHash);
+
+        uint256 eta = proposalEta(proposalId);
+        if (eta > 0) {
+            for (uint256 i = 0; i < targets.length; ++i) {
+                _timelock.cancelTransaction(targets[i], values[i], "", calldatas[i], eta);
+            }
+            _proposalTimelocks[proposalId].timer.reset();
+        }
+
+        return proposalId;
+    }
+
+    /**
+     * @dev Address through which the governor executes action. In this case, the timelock.
+     */
+    function _executor() internal view virtual override returns (address) {
+        return address(_timelock);
+    }
+
+    /**
+     * @dev Accept admin right over the timelock.
+     */
+    // solhint-disable-next-line private-vars-leading-underscore
+    function __acceptAdmin() public {
+        _timelock.acceptAdmin();
+    }
+
+    /**
+     * @dev Public endpoint to update the underlying timelock instance. Restricted to the timelock itself, so updates
+     * must be proposed, scheduled and executed using the {Governor} workflow.
+     *
+     * For security reason, the timelock must be handed over to another admin before setting up a new one. The two
+     * operations (hand over the timelock) and do the update can be batched in a single proposal.
+     *
+     * Note that if the timelock admin has been handed over in a previous operation, we refuse updates made through the
+     * timelock if admin of the timelock has already been accepted and the operation is executed outside the scope of
+     * governance.
+     */
+    function updateTimelock(ICompoundTimelock newTimelock) external virtual onlyGovernance {
+        _updateTimelock(newTimelock);
+    }
+
+    function _updateTimelock(ICompoundTimelock newTimelock) private {
+        emit TimelockChange(address(_timelock), address(newTimelock));
+        _timelock = newTimelock;
+    }
+}

+ 153 - 0
contracts/governance/extensions/GovernorTimelockControl.sol

@@ -0,0 +1,153 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.0;
+
+import "./IGovernorTimelock.sol";
+import "../Governor.sol";
+import "../TimelockController.sol";
+
+/**
+ * @dev Extension of {Governor} that binds the execution process to an instance of {TimelockController}. This adds a
+ * delay, enforced by the {TimelockController} to all successful proposal (in addition to the voting duration). The
+ * {Governor} needs the proposer (an ideally the executor) roles for the {Governor} to work properly.
+ *
+ * Using this model means the proposal will be operated by the {TimelockController} and not by the {Governor}. Thus,
+ * the assets and permissions must be attached to the {TimelockController}. Any asset sent to the {Governor} will be
+ * inaccessible.
+ *
+ * _Available since v4.3._
+ */
+abstract contract GovernorTimelockControl is IGovernorTimelock, Governor {
+    TimelockController private _timelock;
+    mapping(uint256 => bytes32) private _timelockIds;
+
+    /**
+     * @dev Emitted when the timelock controller used for proposal execution is modified.
+     */
+    event TimelockChange(address oldTimelock, address newTimelock);
+
+    /**
+     * @dev Set the timelock.
+     */
+    constructor(TimelockController timelockAddress) {
+        _updateTimelock(timelockAddress);
+    }
+
+    /**
+     * @dev See {IERC165-supportsInterface}.
+     */
+    function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, Governor) returns (bool) {
+        return interfaceId == type(IGovernorTimelock).interfaceId || super.supportsInterface(interfaceId);
+    }
+
+    /**
+     * @dev Overriden version of the {Governor-state} function with added support for the `Queued` status.
+     */
+    function state(uint256 proposalId) public view virtual override(IGovernor, Governor) returns (ProposalState) {
+        ProposalState status = super.state(proposalId);
+
+        if (status != ProposalState.Succeeded) {
+            return status;
+        }
+
+        // core tracks execution, so we just have to check if successful proposal have been queued.
+        bytes32 queueid = _timelockIds[proposalId];
+        if (queueid == bytes32(0)) {
+            return status;
+        } else if (_timelock.isOperationDone(queueid)) {
+            return ProposalState.Executed;
+        } else {
+            return ProposalState.Queued;
+        }
+    }
+
+    /**
+     * @dev Public accessor to check the address of the timelock
+     */
+    function timelock() public view virtual override returns (address) {
+        return address(_timelock);
+    }
+
+    /**
+     * @dev Public accessor to check the eta of a queued proposal
+     */
+    function proposalEta(uint256 proposalId) public view virtual override returns (uint256) {
+        uint256 eta = _timelock.getTimestamp(_timelockIds[proposalId]);
+        return eta == 1 ? 0 : eta; // _DONE_TIMESTAMP (1) should be replaced with a 0 value
+    }
+
+    /**
+     * @dev Function to queue a proposal to the timelock.
+     */
+    function queue(
+        address[] memory targets,
+        uint256[] memory values,
+        bytes[] memory calldatas,
+        bytes32 descriptionHash
+    ) public virtual override returns (uint256) {
+        uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash);
+
+        require(state(proposalId) == ProposalState.Succeeded, "Governor: proposal not successful");
+
+        uint256 delay = _timelock.getMinDelay();
+        _timelockIds[proposalId] = _timelock.hashOperationBatch(targets, values, calldatas, 0, descriptionHash);
+        _timelock.scheduleBatch(targets, values, calldatas, 0, descriptionHash, delay);
+
+        emit ProposalQueued(proposalId, block.timestamp + delay);
+
+        return proposalId;
+    }
+
+    /**
+     * @dev Overriden execute function that run the already queued proposal through the timelock.
+     */
+    function _execute(
+        uint256, /* proposalId */
+        address[] memory targets,
+        uint256[] memory values,
+        bytes[] memory calldatas,
+        bytes32 descriptionHash
+    ) internal virtual override {
+        _timelock.executeBatch{value: msg.value}(targets, values, calldatas, 0, descriptionHash);
+    }
+
+    /**
+     * @dev Overriden version of the {Governor-_cancel} function to cancel the timelocked proposal if it as already
+     * been queued.
+     */
+    function _cancel(
+        address[] memory targets,
+        uint256[] memory values,
+        bytes[] memory calldatas,
+        bytes32 descriptionHash
+    ) internal virtual override returns (uint256) {
+        uint256 proposalId = super._cancel(targets, values, calldatas, descriptionHash);
+
+        if (_timelockIds[proposalId] != 0) {
+            _timelock.cancel(_timelockIds[proposalId]);
+            delete _timelockIds[proposalId];
+        }
+
+        return proposalId;
+    }
+
+    /**
+     * @dev Address through which the governor executes action. In this case, the timelock.
+     */
+    function _executor() internal view virtual override returns (address) {
+        return address(_timelock);
+    }
+
+    /**
+     * @dev Public endpoint to update the underlying timelock instance. Restricted to the timelock itself, so updates
+     * must be proposed, scheduled and executed using the {Governor} workflow.
+     */
+    function updateTimelock(TimelockController newTimelock) external virtual onlyGovernance {
+        _updateTimelock(newTimelock);
+    }
+
+    function _updateTimelock(TimelockController newTimelock) private {
+        emit TimelockChange(address(_timelock), address(newTimelock));
+        _timelock = newTimelock;
+    }
+}

+ 27 - 0
contracts/governance/extensions/GovernorVotes.sol

@@ -0,0 +1,27 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.0;
+
+import "../Governor.sol";
+import "../../token/ERC20/extensions/ERC20Votes.sol";
+import "../../utils/math/Math.sol";
+
+/**
+ * @dev Extension of {Governor} for voting weight extraction from an {ERC20Votes} token.
+ *
+ * _Available since v4.3._
+ */
+abstract contract GovernorVotes is Governor {
+    ERC20Votes public immutable token;
+
+    constructor(ERC20Votes tokenAddress) {
+        token = tokenAddress;
+    }
+
+    /**
+     * Read the voting weight from the token's built in snapshot mechanism (see {IGovernor-getVotes}).
+     */
+    function getVotes(address account, uint256 blockNumber) public view virtual override returns (uint256) {
+        return token.getPastVotes(account, blockNumber);
+    }
+}

+ 26 - 0
contracts/governance/extensions/GovernorVotesComp.sol

@@ -0,0 +1,26 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.0;
+
+import "../Governor.sol";
+import "../../token/ERC20/extensions/ERC20VotesComp.sol";
+
+/**
+ * @dev Extension of {Governor} for voting weight extraction from a Comp token.
+ *
+ * _Available since v4.3._
+ */
+abstract contract GovernorVotesComp is Governor {
+    ERC20VotesComp public immutable token;
+
+    constructor(ERC20VotesComp token_) {
+        token = token_;
+    }
+
+    /**
+     * Read the voting weight from the token's built in snapshot mechanism (see {IGovernor-getVotes}).
+     */
+    function getVotes(address account, uint256 blockNumber) public view virtual override returns (uint256) {
+        return token.getPriorVotes(account, blockNumber);
+    }
+}

+ 49 - 0
contracts/governance/extensions/GovernorVotesQuorumFraction.sol

@@ -0,0 +1,49 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.0;
+
+import "./GovernorVotes.sol";
+
+/**
+ * @dev Extension of {Governor} for voting weight extraction from an {ERC20Votes} token and a quorum expressed as a
+ * fraction of the total supply.
+ *
+ * _Available since v4.3._
+ */
+abstract contract GovernorVotesQuorumFraction is GovernorVotes {
+    uint256 private _quorumNumerator;
+
+    event QuorumNumeratorUpdated(uint256 oldQuorumNumerator, uint256 newQuorumNumerator);
+
+    constructor(uint256 quorumNumeratorValue) {
+        _updateQuorumNumerator(quorumNumeratorValue);
+    }
+
+    function quorumNumerator() public view virtual returns (uint256) {
+        return _quorumNumerator;
+    }
+
+    function quorumDenominator() public view virtual returns (uint256) {
+        return 100;
+    }
+
+    function quorum(uint256 blockNumber) public view virtual override returns (uint256) {
+        return (token.getPastTotalSupply(blockNumber) * quorumNumerator()) / quorumDenominator();
+    }
+
+    function updateQuorumNumerator(uint256 newQuorumNumerator) external virtual onlyGovernance {
+        _updateQuorumNumerator(newQuorumNumerator);
+    }
+
+    function _updateQuorumNumerator(uint256 newQuorumNumerator) internal virtual {
+        require(
+            newQuorumNumerator <= quorumDenominator(),
+            "GovernorVotesQuorumFraction: quorumNumerator over quorumDenominator"
+        );
+
+        uint256 oldQuorumNumerator = _quorumNumerator;
+        _quorumNumerator = newQuorumNumerator;
+
+        emit QuorumNumeratorUpdated(oldQuorumNumerator, newQuorumNumerator);
+    }
+}

+ 25 - 0
contracts/governance/extensions/IGovernorTimelock.sol

@@ -0,0 +1,25 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.0;
+
+import "../IGovernor.sol";
+
+/**
+ * @dev Extension of the {IGovernor} for timelock supporting modules.
+ *
+ * _Available since v4.3._
+ */
+interface IGovernorTimelock is IGovernor {
+    event ProposalQueued(uint256 proposalId, uint256 eta);
+
+    function timelock() external view returns (address);
+
+    function proposalEta(uint256 proposalId) external view returns (uint256);
+
+    function queue(
+        address[] calldata targets,
+        uint256[] calldata values,
+        bytes[] calldata calldatas,
+        bytes32 descriptionHash
+    ) external returns (uint256 proposalId);
+}

+ 55 - 0
contracts/mocks/GovernorCompMock.sol

@@ -0,0 +1,55 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.0;
+
+import "../governance/Governor.sol";
+import "../governance/extensions/GovernorCountingSimple.sol";
+import "../governance/extensions/GovernorVotesComp.sol";
+
+contract GovernorCompMock is Governor, GovernorVotesComp, GovernorCountingSimple {
+    uint256 immutable _votingDelay;
+    uint256 immutable _votingPeriod;
+
+    constructor(
+        string memory name_,
+        ERC20VotesComp token_,
+        uint256 votingDelay_,
+        uint256 votingPeriod_
+    ) Governor(name_) GovernorVotesComp(token_) {
+        _votingDelay = votingDelay_;
+        _votingPeriod = votingPeriod_;
+    }
+
+    receive() external payable {}
+
+    function votingDelay() public view override returns (uint256) {
+        return _votingDelay;
+    }
+
+    function votingPeriod() public view override returns (uint256) {
+        return _votingPeriod;
+    }
+
+    function quorum(uint256) public pure override returns (uint256) {
+        return 0;
+    }
+
+    function cancel(
+        address[] memory targets,
+        uint256[] memory values,
+        bytes[] memory calldatas,
+        bytes32 salt
+    ) public returns (uint256 proposalId) {
+        return _cancel(targets, values, calldatas, salt);
+    }
+
+    function getVotes(address account, uint256 blockNumber)
+        public
+        view
+        virtual
+        override(Governor, GovernorVotesComp)
+        returns (uint256)
+    {
+        return super.getVotes(account, blockNumber);
+    }
+}

+ 145 - 0
contracts/mocks/GovernorCompatibilityBravoMock.sol

@@ -0,0 +1,145 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.0;
+
+import "../governance/compatibility/GovernorCompatibilityBravo.sol";
+import "../governance/extensions/GovernorVotesComp.sol";
+import "../governance/extensions/GovernorTimelockCompound.sol";
+
+contract GovernorCompatibilityBravoMock is GovernorCompatibilityBravo, GovernorTimelockCompound, GovernorVotesComp {
+    uint256 immutable _votingDelay;
+    uint256 immutable _votingPeriod;
+    uint256 immutable _proposalThreshold;
+
+    constructor(
+        string memory name_,
+        ERC20VotesComp token_,
+        uint256 votingDelay_,
+        uint256 votingPeriod_,
+        uint256 proposalThreshold_,
+        ICompoundTimelock timelock_
+    ) Governor(name_) GovernorVotesComp(token_) GovernorTimelockCompound(timelock_) {
+        _votingDelay = votingDelay_;
+        _votingPeriod = votingPeriod_;
+        _proposalThreshold = proposalThreshold_;
+    }
+
+    function supportsInterface(bytes4 interfaceId)
+        public
+        view
+        virtual
+        override(IERC165, Governor, GovernorTimelockCompound)
+        returns (bool)
+    {
+        return super.supportsInterface(interfaceId);
+    }
+
+    function votingDelay() public view override(IGovernor, Governor) returns (uint256) {
+        return _votingDelay;
+    }
+
+    function votingPeriod() public view override(IGovernor, Governor) returns (uint256) {
+        return _votingPeriod;
+    }
+
+    function proposalThreshold() public view virtual override returns (uint256) {
+        return _proposalThreshold;
+    }
+
+    function quorum(uint256) public pure override(IGovernor, Governor) returns (uint256) {
+        return 0;
+    }
+
+    function state(uint256 proposalId)
+        public
+        view
+        virtual
+        override(IGovernor, Governor, GovernorTimelockCompound)
+        returns (ProposalState)
+    {
+        return super.state(proposalId);
+    }
+
+    function proposalEta(uint256 proposalId)
+        public
+        view
+        virtual
+        override(GovernorCompatibilityBravo, GovernorTimelockCompound)
+        returns (uint256)
+    {
+        return super.proposalEta(proposalId);
+    }
+
+    function propose(
+        address[] memory targets,
+        uint256[] memory values,
+        bytes[] memory calldatas,
+        string memory description
+    ) public virtual override(IGovernor, Governor, GovernorCompatibilityBravo) returns (uint256) {
+        return super.propose(targets, values, calldatas, description);
+    }
+
+    function queue(
+        address[] memory targets,
+        uint256[] memory values,
+        bytes[] memory calldatas,
+        bytes32 salt
+    ) public virtual override(GovernorCompatibilityBravo, GovernorTimelockCompound) returns (uint256) {
+        return super.queue(targets, values, calldatas, salt);
+    }
+
+    function execute(
+        address[] memory targets,
+        uint256[] memory values,
+        bytes[] memory calldatas,
+        bytes32 salt
+    ) public payable virtual override(IGovernor, Governor) returns (uint256) {
+        return super.execute(targets, values, calldatas, salt);
+    }
+
+    function _execute(
+        uint256 proposalId,
+        address[] memory targets,
+        uint256[] memory values,
+        bytes[] memory calldatas,
+        bytes32 descriptionHash
+    ) internal virtual override(Governor, GovernorTimelockCompound) {
+        super._execute(proposalId, targets, values, calldatas, descriptionHash);
+    }
+
+    /**
+     * @notice WARNING: this is for mock purposes only. Ability to the _cancel function should be restricted for live
+     * deployments.
+     */
+    function cancel(
+        address[] memory targets,
+        uint256[] memory values,
+        bytes[] memory calldatas,
+        bytes32 salt
+    ) public returns (uint256 proposalId) {
+        return _cancel(targets, values, calldatas, salt);
+    }
+
+    function _cancel(
+        address[] memory targets,
+        uint256[] memory values,
+        bytes[] memory calldatas,
+        bytes32 salt
+    ) internal virtual override(Governor, GovernorTimelockCompound) returns (uint256 proposalId) {
+        return super._cancel(targets, values, calldatas, salt);
+    }
+
+    function getVotes(address account, uint256 blockNumber)
+        public
+        view
+        virtual
+        override(IGovernor, GovernorVotesComp)
+        returns (uint256)
+    {
+        return super.getVotes(account, blockNumber);
+    }
+
+    function _executor() internal view virtual override(Governor, GovernorTimelockCompound) returns (address) {
+        return super._executor();
+    }
+}

+ 52 - 0
contracts/mocks/GovernorMock.sol

@@ -0,0 +1,52 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.0;
+
+import "../governance/Governor.sol";
+import "../governance/extensions/GovernorCountingSimple.sol";
+import "../governance/extensions/GovernorVotesQuorumFraction.sol";
+
+contract GovernorMock is Governor, GovernorVotesQuorumFraction, GovernorCountingSimple {
+    uint256 immutable _votingDelay;
+    uint256 immutable _votingPeriod;
+
+    constructor(
+        string memory name_,
+        ERC20Votes token_,
+        uint256 votingDelay_,
+        uint256 votingPeriod_,
+        uint256 quorumNumerator_
+    ) Governor(name_) GovernorVotes(token_) GovernorVotesQuorumFraction(quorumNumerator_) {
+        _votingDelay = votingDelay_;
+        _votingPeriod = votingPeriod_;
+    }
+
+    receive() external payable {}
+
+    function votingDelay() public view override returns (uint256) {
+        return _votingDelay;
+    }
+
+    function votingPeriod() public view override returns (uint256) {
+        return _votingPeriod;
+    }
+
+    function cancel(
+        address[] memory targets,
+        uint256[] memory values,
+        bytes[] memory calldatas,
+        bytes32 salt
+    ) public returns (uint256 proposalId) {
+        return _cancel(targets, values, calldatas, salt);
+    }
+
+    function getVotes(address account, uint256 blockNumber)
+        public
+        view
+        virtual
+        override(Governor, GovernorVotes)
+        returns (uint256)
+    {
+        return super.getVotes(account, blockNumber);
+    }
+}

+ 111 - 0
contracts/mocks/GovernorTimelockCompoundMock.sol

@@ -0,0 +1,111 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.0;
+
+import "../governance/extensions/GovernorTimelockCompound.sol";
+import "../governance/extensions/GovernorCountingSimple.sol";
+import "../governance/extensions/GovernorVotesQuorumFraction.sol";
+
+contract GovernorTimelockCompoundMock is GovernorTimelockCompound, GovernorVotesQuorumFraction, GovernorCountingSimple {
+    uint256 immutable _votingDelay;
+    uint256 immutable _votingPeriod;
+
+    constructor(
+        string memory name_,
+        ERC20Votes token_,
+        uint256 votingDelay_,
+        uint256 votingPeriod_,
+        ICompoundTimelock timelock_,
+        uint256 quorumNumerator_
+    )
+        Governor(name_)
+        GovernorTimelockCompound(timelock_)
+        GovernorVotes(token_)
+        GovernorVotesQuorumFraction(quorumNumerator_)
+    {
+        _votingDelay = votingDelay_;
+        _votingPeriod = votingPeriod_;
+    }
+
+    function supportsInterface(bytes4 interfaceId)
+        public
+        view
+        virtual
+        override(Governor, GovernorTimelockCompound)
+        returns (bool)
+    {
+        return super.supportsInterface(interfaceId);
+    }
+
+    function votingDelay() public view override(IGovernor, Governor) returns (uint256) {
+        return _votingDelay;
+    }
+
+    function votingPeriod() public view override(IGovernor, Governor) returns (uint256) {
+        return _votingPeriod;
+    }
+
+    function quorum(uint256 blockNumber)
+        public
+        view
+        override(IGovernor, Governor, GovernorVotesQuorumFraction)
+        returns (uint256)
+    {
+        return super.quorum(blockNumber);
+    }
+
+    function cancel(
+        address[] memory targets,
+        uint256[] memory values,
+        bytes[] memory calldatas,
+        bytes32 salt
+    ) public returns (uint256 proposalId) {
+        return _cancel(targets, values, calldatas, salt);
+    }
+
+    /**
+     * Overriding nightmare
+     */
+    function state(uint256 proposalId)
+        public
+        view
+        virtual
+        override(Governor, GovernorTimelockCompound)
+        returns (ProposalState)
+    {
+        return super.state(proposalId);
+    }
+
+    function _execute(
+        uint256 proposalId,
+        address[] memory targets,
+        uint256[] memory values,
+        bytes[] memory calldatas,
+        bytes32 descriptionHash
+    ) internal virtual override(Governor, GovernorTimelockCompound) {
+        super._execute(proposalId, targets, values, calldatas, descriptionHash);
+    }
+
+    function _cancel(
+        address[] memory targets,
+        uint256[] memory values,
+        bytes[] memory calldatas,
+        bytes32 salt
+    ) internal virtual override(Governor, GovernorTimelockCompound) returns (uint256 proposalId) {
+        return super._cancel(targets, values, calldatas, salt);
+    }
+
+    function getVotes(address account, uint256 blockNumber)
+        public
+        view
+        virtual
+        override(IGovernor, Governor, GovernorVotes)
+        returns (uint256)
+    {
+        return super.getVotes(account, blockNumber);
+    }
+
+    function _executor() internal view virtual override(Governor, GovernorTimelockCompound) returns (address) {
+        return super._executor();
+    }
+}

+ 111 - 0
contracts/mocks/GovernorTimelockControlMock.sol

@@ -0,0 +1,111 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.0;
+
+import "../governance/extensions/GovernorTimelockControl.sol";
+import "../governance/extensions/GovernorCountingSimple.sol";
+import "../governance/extensions/GovernorVotesQuorumFraction.sol";
+
+contract GovernorTimelockControlMock is GovernorTimelockControl, GovernorVotesQuorumFraction, GovernorCountingSimple {
+    uint256 immutable _votingDelay;
+    uint256 immutable _votingPeriod;
+
+    constructor(
+        string memory name_,
+        ERC20Votes token_,
+        uint256 votingDelay_,
+        uint256 votingPeriod_,
+        TimelockController timelock_,
+        uint256 quorumNumerator_
+    )
+        Governor(name_)
+        GovernorTimelockControl(timelock_)
+        GovernorVotes(token_)
+        GovernorVotesQuorumFraction(quorumNumerator_)
+    {
+        _votingDelay = votingDelay_;
+        _votingPeriod = votingPeriod_;
+    }
+
+    function supportsInterface(bytes4 interfaceId)
+        public
+        view
+        virtual
+        override(Governor, GovernorTimelockControl)
+        returns (bool)
+    {
+        return super.supportsInterface(interfaceId);
+    }
+
+    function votingDelay() public view override(IGovernor, Governor) returns (uint256) {
+        return _votingDelay;
+    }
+
+    function votingPeriod() public view override(IGovernor, Governor) returns (uint256) {
+        return _votingPeriod;
+    }
+
+    function quorum(uint256 blockNumber)
+        public
+        view
+        override(IGovernor, Governor, GovernorVotesQuorumFraction)
+        returns (uint256)
+    {
+        return super.quorum(blockNumber);
+    }
+
+    function cancel(
+        address[] memory targets,
+        uint256[] memory values,
+        bytes[] memory calldatas,
+        bytes32 descriptionHash
+    ) public returns (uint256 proposalId) {
+        return _cancel(targets, values, calldatas, descriptionHash);
+    }
+
+    /**
+     * Overriding nightmare
+     */
+    function state(uint256 proposalId)
+        public
+        view
+        virtual
+        override(Governor, GovernorTimelockControl)
+        returns (ProposalState)
+    {
+        return super.state(proposalId);
+    }
+
+    function _execute(
+        uint256 proposalId,
+        address[] memory targets,
+        uint256[] memory values,
+        bytes[] memory calldatas,
+        bytes32 descriptionHash
+    ) internal virtual override(Governor, GovernorTimelockControl) {
+        super._execute(proposalId, targets, values, calldatas, descriptionHash);
+    }
+
+    function _cancel(
+        address[] memory targets,
+        uint256[] memory values,
+        bytes[] memory calldatas,
+        bytes32 descriptionHash
+    ) internal virtual override(Governor, GovernorTimelockControl) returns (uint256 proposalId) {
+        return super._cancel(targets, values, calldatas, descriptionHash);
+    }
+
+    function getVotes(address account, uint256 blockNumber)
+        public
+        view
+        virtual
+        override(IGovernor, Governor, GovernorVotes)
+        returns (uint256)
+    {
+        return super.getVotes(account, blockNumber);
+    }
+
+    function _executor() internal view virtual override(Governor, GovernorTimelockControl) returns (address) {
+        return super._executor();
+    }
+}

+ 39 - 0
contracts/mocks/TimersBlockNumberImpl.sol

@@ -0,0 +1,39 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.0;
+
+import "../utils/Timers.sol";
+
+contract TimersBlockNumberImpl {
+    using Timers for Timers.BlockNumber;
+
+    Timers.BlockNumber private _timer;
+
+    function getDeadline() public view returns (uint64) {
+        return _timer.getDeadline();
+    }
+
+    function setDeadline(uint64 timestamp) public {
+        _timer.setDeadline(timestamp);
+    }
+
+    function reset() public {
+        _timer.reset();
+    }
+
+    function isUnset() public view returns (bool) {
+        return _timer.isUnset();
+    }
+
+    function isStarted() public view returns (bool) {
+        return _timer.isStarted();
+    }
+
+    function isPending() public view returns (bool) {
+        return _timer.isPending();
+    }
+
+    function isExpired() public view returns (bool) {
+        return _timer.isExpired();
+    }
+}

+ 39 - 0
contracts/mocks/TimersTimestampImpl.sol

@@ -0,0 +1,39 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.0;
+
+import "../utils/Timers.sol";
+
+contract TimersTimestampImpl {
+    using Timers for Timers.Timestamp;
+
+    Timers.Timestamp private _timer;
+
+    function getDeadline() public view returns (uint64) {
+        return _timer.getDeadline();
+    }
+
+    function setDeadline(uint64 timestamp) public {
+        _timer.setDeadline(timestamp);
+    }
+
+    function reset() public {
+        _timer.reset();
+    }
+
+    function isUnset() public view returns (bool) {
+        return _timer.isUnset();
+    }
+
+    function isStarted() public view returns (bool) {
+        return _timer.isStarted();
+    }
+
+    function isPending() public view returns (bool) {
+        return _timer.isPending();
+    }
+
+    function isExpired() public view returns (bool) {
+        return _timer.isExpired();
+    }
+}

+ 174 - 0
contracts/mocks/compound/CompTimelock.sol

@@ -0,0 +1,174 @@
+// SPDX-License-Identifier: BSD-3-Clause
+// solhint-disable private-vars-leading-underscore
+/**
+ * Copyright 2020 Compound Labs, Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+ * following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+ *    disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
+ *    following disclaimer in the documentation and/or other materials provided with the distribution.
+ *
+ * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
+ *    products derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+pragma solidity ^0.8.0;
+
+contract CompTimelock {
+    event NewAdmin(address indexed newAdmin);
+    event NewPendingAdmin(address indexed newPendingAdmin);
+    event NewDelay(uint256 indexed newDelay);
+    event CancelTransaction(
+        bytes32 indexed txHash,
+        address indexed target,
+        uint256 value,
+        string signature,
+        bytes data,
+        uint256 eta
+    );
+    event ExecuteTransaction(
+        bytes32 indexed txHash,
+        address indexed target,
+        uint256 value,
+        string signature,
+        bytes data,
+        uint256 eta
+    );
+    event QueueTransaction(
+        bytes32 indexed txHash,
+        address indexed target,
+        uint256 value,
+        string signature,
+        bytes data,
+        uint256 eta
+    );
+
+    uint256 public constant GRACE_PERIOD = 14 days;
+    uint256 public constant MINIMUM_DELAY = 2 days;
+    uint256 public constant MAXIMUM_DELAY = 30 days;
+
+    address public admin;
+    address public pendingAdmin;
+    uint256 public delay;
+
+    mapping(bytes32 => bool) public queuedTransactions;
+
+    constructor(address admin_, uint256 delay_) {
+        require(delay_ >= MINIMUM_DELAY, "Timelock::constructor: Delay must exceed minimum delay.");
+        require(delay_ <= MAXIMUM_DELAY, "Timelock::setDelay: Delay must not exceed maximum delay.");
+
+        admin = admin_;
+        delay = delay_;
+    }
+
+    receive() external payable {}
+
+    function setDelay(uint256 delay_) public {
+        require(msg.sender == address(this), "Timelock::setDelay: Call must come from Timelock.");
+        require(delay_ >= MINIMUM_DELAY, "Timelock::setDelay: Delay must exceed minimum delay.");
+        require(delay_ <= MAXIMUM_DELAY, "Timelock::setDelay: Delay must not exceed maximum delay.");
+        delay = delay_;
+
+        emit NewDelay(delay);
+    }
+
+    function acceptAdmin() public {
+        require(msg.sender == pendingAdmin, "Timelock::acceptAdmin: Call must come from pendingAdmin.");
+        admin = msg.sender;
+        pendingAdmin = address(0);
+
+        emit NewAdmin(admin);
+    }
+
+    function setPendingAdmin(address pendingAdmin_) public {
+        require(msg.sender == address(this), "Timelock::setPendingAdmin: Call must come from Timelock.");
+        pendingAdmin = pendingAdmin_;
+
+        emit NewPendingAdmin(pendingAdmin);
+    }
+
+    function queueTransaction(
+        address target,
+        uint256 value,
+        string memory signature,
+        bytes memory data,
+        uint256 eta
+    ) public returns (bytes32) {
+        require(msg.sender == admin, "Timelock::queueTransaction: Call must come from admin.");
+        require(
+            eta >= getBlockTimestamp() + delay,
+            "Timelock::queueTransaction: Estimated execution block must satisfy delay."
+        );
+
+        bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta));
+        queuedTransactions[txHash] = true;
+
+        emit QueueTransaction(txHash, target, value, signature, data, eta);
+        return txHash;
+    }
+
+    function cancelTransaction(
+        address target,
+        uint256 value,
+        string memory signature,
+        bytes memory data,
+        uint256 eta
+    ) public {
+        require(msg.sender == admin, "Timelock::cancelTransaction: Call must come from admin.");
+
+        bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta));
+        queuedTransactions[txHash] = false;
+
+        emit CancelTransaction(txHash, target, value, signature, data, eta);
+    }
+
+    function executeTransaction(
+        address target,
+        uint256 value,
+        string memory signature,
+        bytes memory data,
+        uint256 eta
+    ) public payable returns (bytes memory) {
+        require(msg.sender == admin, "Timelock::executeTransaction: Call must come from admin.");
+
+        bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta));
+        require(queuedTransactions[txHash], "Timelock::executeTransaction: Transaction hasn't been queued.");
+        require(getBlockTimestamp() >= eta, "Timelock::executeTransaction: Transaction hasn't surpassed time lock.");
+        require(getBlockTimestamp() <= eta + GRACE_PERIOD, "Timelock::executeTransaction: Transaction is stale.");
+
+        queuedTransactions[txHash] = false;
+
+        bytes memory callData;
+
+        if (bytes(signature).length == 0) {
+            callData = data;
+        } else {
+            callData = abi.encodePacked(bytes4(keccak256(bytes(signature))), data);
+        }
+
+        // solium-disable-next-line security/no-call-value
+        (bool success, bytes memory returnData) = target.call{value: value}(callData);
+        require(success, "Timelock::executeTransaction: Transaction execution reverted.");
+
+        emit ExecuteTransaction(txHash, target, value, signature, data, eta);
+
+        return returnData;
+    }
+
+    function getBlockTimestamp() internal view returns (uint256) {
+        // solium-disable-next-line security/no-block-members
+        return block.timestamp;
+    }
+}

+ 1 - 1
contracts/token/ERC20/extensions/ERC20FlashMint.sol

@@ -53,7 +53,7 @@ abstract contract ERC20FlashMint is ERC20, IERC3156FlashLender {
      * supported.
      * @param amount The amount of tokens to be loaned.
      * @param data An arbitrary datafield that is passed to the receiver.
-     * @return `true` is the flash loan was successfull.
+     * @return `true` is the flash loan was successful.
      */
     function flashLoan(
         IERC3156FlashBorrower receiver,

+ 11 - 5
contracts/utils/Address.sol

@@ -129,7 +129,7 @@ library Address {
         require(isContract(target), "Address: call to non-contract");
 
         (bool success, bytes memory returndata) = target.call{value: value}(data);
-        return _verifyCallResult(success, returndata, errorMessage);
+        return verifyCallResult(success, returndata, errorMessage);
     }
 
     /**
@@ -156,7 +156,7 @@ library Address {
         require(isContract(target), "Address: static call to non-contract");
 
         (bool success, bytes memory returndata) = target.staticcall(data);
-        return _verifyCallResult(success, returndata, errorMessage);
+        return verifyCallResult(success, returndata, errorMessage);
     }
 
     /**
@@ -183,14 +183,20 @@ library Address {
         require(isContract(target), "Address: delegate call to non-contract");
 
         (bool success, bytes memory returndata) = target.delegatecall(data);
-        return _verifyCallResult(success, returndata, errorMessage);
+        return verifyCallResult(success, returndata, errorMessage);
     }
 
-    function _verifyCallResult(
+    /**
+     * @dev Tool to verifies that a low level call was successful, and revert if it wasn't, either by bubbling the
+     * revert reason using the provided one.
+     *
+     * _Available since v4.3._
+     */
+    function verifyCallResult(
         bool success,
         bytes memory returndata,
         string memory errorMessage
-    ) private pure returns (bytes memory) {
+    ) internal pure returns (bytes memory) {
         if (success) {
             return returndata;
         } else {

+ 72 - 0
contracts/utils/Timers.sol

@@ -0,0 +1,72 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.0;
+
+/**
+ * @dev Tooling for timepoints, timers and delays
+ */
+library Timers {
+    struct Timestamp {
+        uint64 _deadline;
+    }
+
+    function getDeadline(Timestamp memory timer) internal pure returns (uint64) {
+        return timer._deadline;
+    }
+
+    function setDeadline(Timestamp storage timer, uint64 timestamp) internal {
+        timer._deadline = timestamp;
+    }
+
+    function reset(Timestamp storage timer) internal {
+        timer._deadline = 0;
+    }
+
+    function isUnset(Timestamp memory timer) internal pure returns (bool) {
+        return timer._deadline == 0;
+    }
+
+    function isStarted(Timestamp memory timer) internal pure returns (bool) {
+        return timer._deadline > 0;
+    }
+
+    function isPending(Timestamp memory timer) internal view returns (bool) {
+        return timer._deadline > block.timestamp;
+    }
+
+    function isExpired(Timestamp memory timer) internal view returns (bool) {
+        return isStarted(timer) && timer._deadline <= block.timestamp;
+    }
+
+    struct BlockNumber {
+        uint64 _deadline;
+    }
+
+    function getDeadline(BlockNumber memory timer) internal pure returns (uint64) {
+        return timer._deadline;
+    }
+
+    function setDeadline(BlockNumber storage timer, uint64 timestamp) internal {
+        timer._deadline = timestamp;
+    }
+
+    function reset(BlockNumber storage timer) internal {
+        timer._deadline = 0;
+    }
+
+    function isUnset(BlockNumber memory timer) internal pure returns (bool) {
+        return timer._deadline == 0;
+    }
+
+    function isStarted(BlockNumber memory timer) internal pure returns (bool) {
+        return timer._deadline > 0;
+    }
+
+    function isPending(BlockNumber memory timer) internal view returns (bool) {
+        return timer._deadline > block.number;
+    }
+
+    function isExpired(BlockNumber memory timer) internal view returns (bool) {
+        return isStarted(timer) && timer._deadline <= block.number;
+    }
+}

+ 4 - 1
hardhat.config.js

@@ -23,6 +23,8 @@ for (const f of fs.readdirSync(path.join(__dirname, 'hardhat'))) {
   require(path.join(__dirname, 'hardhat', f));
 }
 
+const withOptimizations = argv.enableGasReport || argv.compileMode === 'production';
+
 /**
  * @type import('hardhat/config').HardhatUserConfig
  */
@@ -31,7 +33,7 @@ module.exports = {
     version: '0.8.3',
     settings: {
       optimizer: {
-        enabled: argv.enableGasReport || argv.compileMode === 'production',
+        enabled: withOptimizations,
         runs: 200,
       },
     },
@@ -39,6 +41,7 @@ module.exports = {
   networks: {
     hardhat: {
       blockGasLimit: 10000000,
+      allowUnlimitedContractSize: !withOptimizations,
     },
   },
   gasReporter: {

Diferenças do arquivo suprimidas por serem muito extensas
+ 112 - 184
package-lock.json


+ 1 - 1
package.json

@@ -74,7 +74,7 @@
     "merkletreejs": "^0.2.13",
     "micromatch": "^4.0.2",
     "prettier": "^2.3.0",
-    "prettier-plugin-solidity": "^1.0.0-beta.13",
+    "prettier-plugin-solidity": "^1.0.0-beta.16",
     "rimraf": "^3.0.2",
     "solhint": "^3.3.6",
     "solidity-ast": "^0.4.25",

+ 16 - 10
scripts/inheritanceOrdering.js

@@ -21,16 +21,22 @@ for (const artifact of artifacts) {
     }
   }
 
-  graphlib.alg.findCycles(graph).forEach(([ c1, c2 ]) => {
-    console.log(`Conflict between ${names[c1]} and ${names[c2]} detected in the following dependency chains:`);
-    linearized
-      .filter(chain => chain.includes(parseInt(c1)) && chain.includes(parseInt(c2)))
-      .forEach(chain => {
-        const comp = chain.indexOf(c1) < chain.indexOf(c2) ? '>' : '<';
-        console.log(`- ${names[c1]} ${comp} ${names[c2]}: ${chain.reverse().map(id => names[id]).join(', ')}`);
-      });
-    process.exitCode = 1;
-  });
+  /// graphlib.alg.findCycles will not find minimal cycles.
+  /// We are only interested int cycles of lengths 2 (needs proof)
+  graph.nodes().forEach((x, i, nodes) => nodes
+    .slice(i + 1)
+    .filter(y => graph.hasEdge(x, y) && graph.hasEdge(y, x))
+    .map(y => {
+      console.log(`Conflict between ${names[x]} and ${names[y]} detected in the following dependency chains:`);
+      linearized
+        .filter(chain => chain.includes(parseInt(x)) && chain.includes(parseInt(y)))
+        .forEach(chain => {
+          const comp = chain.indexOf(parseInt(x)) < chain.indexOf(parseInt(y)) ? '>' : '<';
+          console.log(`- ${names[x]} ${comp} ${names[y]} in ${names[chain.find(Boolean)]}`);
+          // console.log(`- ${names[x]} ${comp} ${names[y]}: ${chain.reverse().map(id => names[id]).join(', ')}`);
+        });
+      process.exitCode = 1;
+    }));
 }
 
 if (!process.exitCode) {

+ 819 - 0
test/governance/Governor.test.js

@@ -0,0 +1,819 @@
+const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers');
+const ethSigUtil = require('eth-sig-util');
+const Wallet = require('ethereumjs-wallet').default;
+const Enums = require('../helpers/enums');
+const { EIP712Domain } = require('../helpers/eip712');
+const { fromRpcSig } = require('ethereumjs-util');
+
+const {
+  runGovernorWorkflow,
+} = require('./GovernorWorkflow.behavior');
+
+const {
+  shouldSupportInterfaces,
+} = require('../utils/introspection/SupportsInterface.behavior');
+
+const Token = artifacts.require('ERC20VotesMock');
+const Governor = artifacts.require('GovernorMock');
+const CallReceiver = artifacts.require('CallReceiverMock');
+
+contract('Governor', function (accounts) {
+  const [ owner, proposer, voter1, voter2, voter3, voter4 ] = accounts;
+
+  const name = 'OZ-Governor';
+  const version = '1';
+  const tokenName = 'MockToken';
+  const tokenSymbol = 'MTKN';
+  const tokenSupply = web3.utils.toWei('100');
+
+  beforeEach(async function () {
+    this.owner = owner;
+    this.token = await Token.new(tokenName, tokenSymbol);
+    this.mock = await Governor.new(name, this.token.address, 4, 16, 0);
+    this.receiver = await CallReceiver.new();
+    await this.token.mint(owner, tokenSupply);
+    await this.token.delegate(voter1, { from: voter1 });
+    await this.token.delegate(voter2, { from: voter2 });
+    await this.token.delegate(voter3, { from: voter3 });
+    await this.token.delegate(voter4, { from: voter4 });
+  });
+
+  shouldSupportInterfaces([
+    'ERC165',
+    'Governor',
+  ]);
+
+  it('deployment check', async function () {
+    expect(await this.mock.name()).to.be.equal(name);
+    expect(await this.mock.token()).to.be.equal(this.token.address);
+    expect(await this.mock.votingDelay()).to.be.bignumber.equal('4');
+    expect(await this.mock.votingPeriod()).to.be.bignumber.equal('16');
+    expect(await this.mock.quorum(0)).to.be.bignumber.equal('0');
+    expect(await this.mock.COUNTING_MODE()).to.be.equal('support=bravo&quorum=for,abstain');
+  });
+
+  describe('scenario', function () {
+    describe('nominal', function () {
+      beforeEach(async function () {
+        this.value = web3.utils.toWei('1');
+
+        await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value: this.value });
+        expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(this.value);
+        expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal('0');
+
+        this.settings = {
+          proposal: [
+            [ this.receiver.address ],
+            [ this.value ],
+            [ this.receiver.contract.methods.mockFunction().encodeABI() ],
+            '<proposal description>',
+          ],
+          proposer,
+          tokenHolder: owner,
+          voters: [
+            { voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.For, reason: 'This is nice' },
+            { voter: voter2, weight: web3.utils.toWei('10'), support: Enums.VoteType.For },
+            { voter: voter3, weight: web3.utils.toWei('5'), support: Enums.VoteType.Against },
+            { voter: voter4, weight: web3.utils.toWei('2'), support: Enums.VoteType.Abstain },
+          ],
+        };
+        this.votingDelay = await this.mock.votingDelay();
+        this.votingPeriod = await this.mock.votingPeriod();
+      });
+
+      afterEach(async function () {
+        expect(await this.mock.hasVoted(this.id, owner)).to.be.equal(false);
+        expect(await this.mock.hasVoted(this.id, voter1)).to.be.equal(true);
+        expect(await this.mock.hasVoted(this.id, voter2)).to.be.equal(true);
+
+        await this.mock.proposalVotes(this.id).then(result => {
+          for (const [key, value] of Object.entries(Enums.VoteType)) {
+            expect(result[`${key.toLowerCase()}Votes`]).to.be.bignumber.equal(
+              Object.values(this.settings.voters).filter(({ support }) => support === value).reduce(
+                (acc, { weight }) => acc.add(new BN(weight)),
+                new BN('0'),
+              ),
+            );
+          }
+        });
+
+        expectEvent(
+          this.receipts.propose,
+          'ProposalCreated',
+          {
+            proposalId: this.id,
+            proposer,
+            targets: this.settings.proposal[0],
+            // values: this.settings.proposal[1].map(value => new BN(value)),
+            signatures: this.settings.proposal[2].map(() => ''),
+            calldatas: this.settings.proposal[2],
+            startBlock: new BN(this.receipts.propose.blockNumber).add(this.votingDelay),
+            endBlock: new BN(this.receipts.propose.blockNumber).add(this.votingDelay).add(this.votingPeriod),
+            description: this.settings.proposal[3],
+          },
+        );
+
+        this.receipts.castVote.filter(Boolean).forEach(vote => {
+          const { voter } = vote.logs.find(Boolean).args;
+          expectEvent(
+            vote,
+            'VoteCast',
+            this.settings.voters.find(({ address }) => address === voter),
+          );
+        });
+        expectEvent(
+          this.receipts.execute,
+          'ProposalExecuted',
+          { proposalId: this.id },
+        );
+        await expectEvent.inTransaction(
+          this.receipts.execute.transactionHash,
+          this.receiver,
+          'MockFunctionCalled',
+        );
+
+        expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0');
+        expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal(this.value);
+      });
+      runGovernorWorkflow();
+    });
+
+    describe('vote with signature', function () {
+      beforeEach(async function () {
+        const chainId = await web3.eth.getChainId();
+        // generate voter by signature wallet
+        const voterBySig = Wallet.generate();
+        this.voter = web3.utils.toChecksumAddress(voterBySig.getAddressString());
+        // use delegateBySig to enable vote delegation for this wallet
+        const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage(
+          voterBySig.getPrivateKey(),
+          {
+            data: {
+              types: {
+                EIP712Domain,
+                Delegation: [
+                  { name: 'delegatee', type: 'address' },
+                  { name: 'nonce', type: 'uint256' },
+                  { name: 'expiry', type: 'uint256' },
+                ],
+              },
+              domain: { name: tokenName, version: '1', chainId, verifyingContract: this.token.address },
+              primaryType: 'Delegation',
+              message: { delegatee: this.voter, nonce: 0, expiry: constants.MAX_UINT256 },
+            },
+          },
+        ));
+        await this.token.delegateBySig(this.voter, 0, constants.MAX_UINT256, v, r, s);
+        // prepare signature for vote by signature
+        const signature = async (message) => {
+          return fromRpcSig(ethSigUtil.signTypedMessage(
+            voterBySig.getPrivateKey(),
+            {
+              data: {
+                types: {
+                  EIP712Domain,
+                  Ballot: [
+                    { name: 'proposalId', type: 'uint256' },
+                    { name: 'support', type: 'uint8' },
+                  ],
+                },
+                domain: { name, version, chainId, verifyingContract: this.mock.address },
+                primaryType: 'Ballot',
+                message,
+              },
+            },
+          ));
+        };
+
+        this.settings = {
+          proposal: [
+            [ this.receiver.address ],
+            [ web3.utils.toWei('0') ],
+            [ this.receiver.contract.methods.mockFunction().encodeABI() ],
+            '<proposal description>',
+          ],
+          tokenHolder: owner,
+          voters: [
+            { voter: this.voter, signature, weight: web3.utils.toWei('1'), support: Enums.VoteType.For },
+          ],
+        };
+      });
+      afterEach(async function () {
+        expect(await this.mock.hasVoted(this.id, owner)).to.be.equal(false);
+        expect(await this.mock.hasVoted(this.id, voter1)).to.be.equal(false);
+        expect(await this.mock.hasVoted(this.id, voter2)).to.be.equal(false);
+        expect(await this.mock.hasVoted(this.id, this.voter)).to.be.equal(true);
+
+        await this.mock.proposalVotes(this.id).then(result => {
+          for (const [key, value] of Object.entries(Enums.VoteType)) {
+            expect(result[`${key.toLowerCase()}Votes`]).to.be.bignumber.equal(
+              Object.values(this.settings.voters).filter(({ support }) => support === value).reduce(
+                (acc, { weight }) => acc.add(new BN(weight)),
+                new BN('0'),
+              ),
+            );
+          }
+        });
+
+        expectEvent(
+          this.receipts.propose,
+          'ProposalCreated',
+          { proposalId: this.id },
+        );
+        expectEvent(
+          this.receipts.execute,
+          'ProposalExecuted',
+          { proposalId: this.id },
+        );
+        await expectEvent.inTransaction(
+          this.receipts.execute.transactionHash,
+          this.receiver,
+          'MockFunctionCalled',
+        );
+      });
+      runGovernorWorkflow();
+    });
+
+    describe('send ethers', function () {
+      beforeEach(async function () {
+        this.receiver = { address: web3.utils.toChecksumAddress(web3.utils.randomHex(20)) };
+        this.value = web3.utils.toWei('1');
+
+        await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value: this.value });
+        expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(this.value);
+        expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal('0');
+
+        this.settings = {
+          proposal: [
+            [ this.receiver.address ],
+            [ this.value ],
+            [ '0x' ],
+            '<proposal description>',
+          ],
+          tokenHolder: owner,
+          voters: [
+            { voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.For },
+            { voter: voter2, weight: web3.utils.toWei('1'), support: Enums.VoteType.Abstain },
+          ],
+        };
+      });
+      afterEach(async function () {
+        expectEvent(
+          this.receipts.propose,
+          'ProposalCreated',
+          { proposalId: this.id },
+        );
+        expectEvent(
+          this.receipts.execute,
+          'ProposalExecuted',
+          { proposalId: this.id },
+        );
+        expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0');
+        expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal(this.value);
+      });
+      runGovernorWorkflow();
+    });
+
+    describe('receiver revert without reason', function () {
+      beforeEach(async function () {
+        this.settings = {
+          proposal: [
+            [ this.receiver.address ],
+            [ 0 ],
+            [ this.receiver.contract.methods.mockFunctionRevertsNoReason().encodeABI() ],
+            '<proposal description>',
+          ],
+          tokenHolder: owner,
+          voters: [
+            { voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.For },
+          ],
+          steps: {
+            execute: { error: 'Governor: call reverted without message' },
+          },
+        };
+      });
+      runGovernorWorkflow();
+    });
+
+    describe('receiver revert with reason', function () {
+      beforeEach(async function () {
+        this.settings = {
+          proposal: [
+            [ this.receiver.address ],
+            [ 0 ],
+            [ this.receiver.contract.methods.mockFunctionRevertsReason().encodeABI() ],
+            '<proposal description>',
+          ],
+          tokenHolder: owner,
+          voters: [
+            { voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.For },
+          ],
+          steps: {
+            execute: { error: 'CallReceiverMock: reverting' },
+          },
+        };
+      });
+      runGovernorWorkflow();
+    });
+
+    describe('missing proposal', function () {
+      beforeEach(async function () {
+        this.settings = {
+          proposal: [
+            [ this.receiver.address ],
+            [ web3.utils.toWei('0') ],
+            [ this.receiver.contract.methods.mockFunction().encodeABI() ],
+            '<proposal description>',
+          ],
+          tokenHolder: owner,
+          voters: [
+            {
+              voter: voter1,
+              weight: web3.utils.toWei('1'),
+              support: Enums.VoteType.For,
+              error: 'Governor: unknown proposal id',
+            },
+            {
+              voter: voter2,
+              weight: web3.utils.toWei('1'),
+              support: Enums.VoteType.Abstain,
+              error: 'Governor: unknown proposal id',
+            },
+          ],
+          steps: {
+            propose: { enable: false },
+            wait: { enable: false },
+            execute: { error: 'Governor: unknown proposal id' },
+          },
+        };
+      });
+      runGovernorWorkflow();
+    });
+
+    describe('duplicate pending proposal', function () {
+      beforeEach(async function () {
+        this.settings = {
+          proposal: [
+            [ this.receiver.address ],
+            [ web3.utils.toWei('0') ],
+            [ this.receiver.contract.methods.mockFunction().encodeABI() ],
+            '<proposal description>',
+          ],
+          steps: {
+            wait: { enable: false },
+            execute: { enable: false },
+          },
+        };
+      });
+      afterEach(async function () {
+        await expectRevert(this.mock.propose(...this.settings.proposal), 'Governor: proposal already exists');
+      });
+      runGovernorWorkflow();
+    });
+
+    describe('duplicate executed proposal', function () {
+      beforeEach(async function () {
+        this.settings = {
+          proposal: [
+            [ this.receiver.address ],
+            [ web3.utils.toWei('0') ],
+            [ this.receiver.contract.methods.mockFunction().encodeABI() ],
+            '<proposal description>',
+          ],
+          tokenHolder: owner,
+          voters: [
+            { voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.For },
+            { voter: voter2, weight: web3.utils.toWei('1'), support: Enums.VoteType.Abstain },
+          ],
+        };
+      });
+      afterEach(async function () {
+        await expectRevert(this.mock.propose(...this.settings.proposal), 'Governor: proposal already exists');
+      });
+      runGovernorWorkflow();
+    });
+
+    describe('Invalid vote type', function () {
+      beforeEach(async function () {
+        this.settings = {
+          proposal: [
+            [ this.receiver.address ],
+            [ web3.utils.toWei('0') ],
+            [ this.receiver.contract.methods.mockFunction().encodeABI() ],
+            '<proposal description>',
+          ],
+          tokenHolder: owner,
+          voters: [
+            {
+              voter: voter1,
+              weight: web3.utils.toWei('1'),
+              support: new BN('255'),
+              error: 'GovernorVotingSimple: invalid value for enum VoteType',
+            },
+          ],
+          steps: {
+            wait: { enable: false },
+            execute: { enable: false },
+          },
+        };
+      });
+      runGovernorWorkflow();
+    });
+
+    describe('double cast', function () {
+      beforeEach(async function () {
+        this.settings = {
+          proposal: [
+            [ this.receiver.address ],
+            [ web3.utils.toWei('0') ],
+            [ this.receiver.contract.methods.mockFunction().encodeABI() ],
+            '<proposal description>',
+          ],
+          tokenHolder: owner,
+          voters: [
+            {
+              voter: voter1,
+              weight: web3.utils.toWei('1'),
+              support: Enums.VoteType.For,
+            },
+            {
+              voter: voter1,
+              weight: web3.utils.toWei('1'),
+              support: Enums.VoteType.For,
+              error: 'GovernorVotingSimple: vote already casted',
+            },
+          ],
+        };
+      });
+      runGovernorWorkflow();
+    });
+
+    describe('quorum not reached', function () {
+      beforeEach(async function () {
+        this.settings = {
+          proposal: [
+            [ this.receiver.address ],
+            [ web3.utils.toWei('0') ],
+            [ this.receiver.contract.methods.mockFunction().encodeABI() ],
+            '<proposal description>',
+          ],
+          tokenHolder: owner,
+          voters: [
+            { voter: voter1, weight: web3.utils.toWei('0'), support: Enums.VoteType.For },
+          ],
+          steps: {
+            execute: { error: 'Governor: proposal not successful' },
+          },
+        };
+      });
+      runGovernorWorkflow();
+    });
+
+    describe('score not reached', function () {
+      beforeEach(async function () {
+        this.settings = {
+          proposal: [
+            [ this.receiver.address ],
+            [ web3.utils.toWei('0') ],
+            [ this.receiver.contract.methods.mockFunction().encodeABI() ],
+            '<proposal description>',
+          ],
+          tokenHolder: owner,
+          voters: [
+            { voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.Against },
+          ],
+          steps: {
+            execute: { error: 'Governor: proposal not successful' },
+          },
+        };
+      });
+      runGovernorWorkflow();
+    });
+
+    describe('vote not over', function () {
+      beforeEach(async function () {
+        this.settings = {
+          proposal: [
+            [ this.receiver.address ],
+            [ web3.utils.toWei('0') ],
+            [ this.receiver.contract.methods.mockFunction().encodeABI() ],
+            '<proposal description>',
+          ],
+          tokenHolder: owner,
+          voters: [
+            { voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.For },
+          ],
+          steps: {
+            wait: { enable: false },
+            execute: { error: 'Governor: proposal not successful' },
+          },
+        };
+      });
+      runGovernorWorkflow();
+    });
+  });
+
+  describe('state', function () {
+    describe('Unset', function () {
+      beforeEach(async function () {
+        this.settings = {
+          proposal: [
+            [ this.receiver.address ],
+            [ web3.utils.toWei('0') ],
+            [ this.receiver.contract.methods.mockFunction().encodeABI() ],
+            '<proposal description>',
+          ],
+          steps: {
+            propose: { enable: false },
+            wait: { enable: false },
+            execute: { enable: false },
+          },
+        };
+      });
+      afterEach(async function () {
+        await expectRevert(this.mock.state(this.id), 'Governor: unknown proposal id');
+      });
+      runGovernorWorkflow();
+    });
+
+    describe('Pending & Active', function () {
+      beforeEach(async function () {
+        this.settings = {
+          proposal: [
+            [ this.receiver.address ],
+            [ web3.utils.toWei('0') ],
+            [ this.receiver.contract.methods.mockFunction().encodeABI() ],
+            '<proposal description>',
+          ],
+          steps: {
+            propose: { noadvance: true },
+            wait: { enable: false },
+            execute: { enable: false },
+          },
+        };
+      });
+      afterEach(async function () {
+        expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Pending);
+
+        await time.advanceBlockTo(this.snapshot);
+
+        expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Active);
+      });
+      runGovernorWorkflow();
+    });
+
+    describe('Defeated', function () {
+      beforeEach(async function () {
+        this.settings = {
+          proposal: [
+            [ this.receiver.address ],
+            [ web3.utils.toWei('0') ],
+            [ this.receiver.contract.methods.mockFunction().encodeABI() ],
+            '<proposal description>',
+          ],
+          steps: {
+            execute: { enable: false },
+          },
+        };
+      });
+      afterEach(async function () {
+        expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Defeated);
+      });
+      runGovernorWorkflow();
+    });
+
+    describe('Succeeded', function () {
+      beforeEach(async function () {
+        this.settings = {
+          proposal: [
+            [ this.receiver.address ],
+            [ web3.utils.toWei('0') ],
+            [ this.receiver.contract.methods.mockFunction().encodeABI() ],
+            '<proposal description>',
+          ],
+          tokenHolder: owner,
+          voters: [
+            { voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.For },
+          ],
+          steps: {
+            execute: { enable: false },
+          },
+        };
+      });
+      afterEach(async function () {
+        expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded);
+      });
+      runGovernorWorkflow();
+    });
+
+    describe('Executed', function () {
+      beforeEach(async function () {
+        this.settings = {
+          proposal: [
+            [ this.receiver.address ],
+            [ web3.utils.toWei('0') ],
+            [ this.receiver.contract.methods.mockFunction().encodeABI() ],
+            '<proposal description>',
+          ],
+          tokenHolder: owner,
+          voters: [
+            { voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.For },
+          ],
+        };
+      });
+      afterEach(async function () {
+        expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Executed);
+      });
+      runGovernorWorkflow();
+    });
+  });
+
+  describe('Cancel', function () {
+    describe('Before proposal', function () {
+      beforeEach(async function () {
+        this.settings = {
+          proposal: [
+            [ this.receiver.address ],
+            [ web3.utils.toWei('0') ],
+            [ this.receiver.contract.methods.mockFunction().encodeABI() ],
+            '<proposal description>',
+          ],
+          steps: {
+            propose: { enable: false },
+            wait: { enable: false },
+            execute: { enable: false },
+          },
+        };
+      });
+      afterEach(async function () {
+        await expectRevert(
+          this.mock.cancel(...this.settings.proposal.slice(0, -1), this.descriptionHash),
+          'Governor: unknown proposal id',
+        );
+      });
+      runGovernorWorkflow();
+    });
+
+    describe('After proposal', function () {
+      beforeEach(async function () {
+        this.settings = {
+          proposal: [
+            [ this.receiver.address ],
+            [ web3.utils.toWei('0') ],
+            [ this.receiver.contract.methods.mockFunction().encodeABI() ],
+            '<proposal description>',
+          ],
+          steps: {
+            wait: { enable: false },
+            execute: { enable: false },
+          },
+        };
+      });
+      afterEach(async function () {
+        await this.mock.cancel(...this.settings.proposal.slice(0, -1), this.descriptionHash);
+        expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
+
+        await expectRevert(
+          this.mock.castVote(this.id, new BN('100'), { from: voter1 }),
+          'Governor: vote not currently active',
+        );
+      });
+      runGovernorWorkflow();
+    });
+
+    describe('After vote', function () {
+      beforeEach(async function () {
+        this.settings = {
+          proposal: [
+            [ this.receiver.address ],
+            [ web3.utils.toWei('0') ],
+            [ this.receiver.contract.methods.mockFunction().encodeABI() ],
+            '<proposal description>',
+          ],
+          tokenHolder: owner,
+          voters: [
+            { voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.For },
+          ],
+          steps: {
+            wait: { enable: false },
+            execute: { enable: false },
+          },
+        };
+      });
+      afterEach(async function () {
+        await this.mock.cancel(...this.settings.proposal.slice(0, -1), this.descriptionHash);
+        expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
+
+        await expectRevert(
+          this.mock.execute(...this.settings.proposal.slice(0, -1), this.descriptionHash),
+          'Governor: proposal not successful',
+        );
+      });
+      runGovernorWorkflow();
+    });
+
+    describe('After deadline', function () {
+      beforeEach(async function () {
+        this.settings = {
+          proposal: [
+            [ this.receiver.address ],
+            [ web3.utils.toWei('0') ],
+            [ this.receiver.contract.methods.mockFunction().encodeABI() ],
+            '<proposal description>',
+          ],
+          tokenHolder: owner,
+          voters: [
+            { voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.For },
+          ],
+          steps: {
+            execute: { enable: false },
+          },
+        };
+      });
+      afterEach(async function () {
+        await this.mock.cancel(...this.settings.proposal.slice(0, -1), this.descriptionHash);
+        expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
+
+        await expectRevert(
+          this.mock.execute(...this.settings.proposal.slice(0, -1), this.descriptionHash),
+          'Governor: proposal not successful',
+        );
+      });
+      runGovernorWorkflow();
+    });
+
+    describe('After execution', function () {
+      beforeEach(async function () {
+        this.settings = {
+          proposal: [
+            [ this.receiver.address ],
+            [ web3.utils.toWei('0') ],
+            [ this.receiver.contract.methods.mockFunction().encodeABI() ],
+            '<proposal description>',
+          ],
+          tokenHolder: owner,
+          voters: [
+            { voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.For },
+          ],
+        };
+      });
+      afterEach(async function () {
+        await expectRevert(
+          this.mock.cancel(...this.settings.proposal.slice(0, -1), this.descriptionHash),
+          'Governor: proposal not active',
+        );
+      });
+      runGovernorWorkflow();
+    });
+  });
+
+  describe('Proposal length', function () {
+    it('empty', async function () {
+      await expectRevert(
+        this.mock.propose(
+          [],
+          [],
+          [],
+          '<proposal description>',
+        ),
+        'Governor: empty proposal',
+      );
+    });
+
+    it('missmatch #1', async function () {
+      await expectRevert(
+        this.mock.propose(
+          [ ],
+          [ web3.utils.toWei('0') ],
+          [ this.receiver.contract.methods.mockFunction().encodeABI() ],
+          '<proposal description>',
+        ),
+        'Governor: invalid proposal length',
+      );
+    });
+
+    it('missmatch #2', async function () {
+      await expectRevert(
+        this.mock.propose(
+          [ this.receiver.address ],
+          [ ],
+          [ this.receiver.contract.methods.mockFunction().encodeABI() ],
+          '<proposal description>',
+        ),
+        'Governor: invalid proposal length',
+      );
+    });
+
+    it('missmatch #3', async function () {
+      await expectRevert(
+        this.mock.propose(
+          [ this.receiver.address ],
+          [ web3.utils.toWei('0') ],
+          [ ],
+          '<proposal description>',
+        ),
+        'Governor: invalid proposal length',
+      );
+    });
+  });
+});

+ 133 - 0
test/governance/GovernorWorkflow.behavior.js

@@ -0,0 +1,133 @@
+const { expectRevert, time } = require('@openzeppelin/test-helpers');
+
+async function getReceiptOrRevert (promise, error = undefined) {
+  if (error) {
+    await expectRevert(promise, error);
+    return undefined;
+  } else {
+    const { receipt } = await promise;
+    return receipt;
+  }
+}
+
+function tryGet (obj, path = '') {
+  try {
+    return path.split('.').reduce((o, k) => o[k], obj);
+  } catch (_) {
+    return undefined;
+  }
+}
+
+function runGovernorWorkflow () {
+  beforeEach(async function () {
+    this.receipts = {};
+    this.descriptionHash = web3.utils.keccak256(this.settings.proposal.slice(-1).find(Boolean));
+    this.id = await this.mock.hashProposal(...this.settings.proposal.slice(0, -1), this.descriptionHash);
+  });
+
+  it('run', async function () {
+    // transfer tokens
+    if (tryGet(this.settings, 'voters')) {
+      for (const voter of this.settings.voters) {
+        if (voter.weight) {
+          await this.token.transfer(voter.voter, voter.weight, { from: this.settings.tokenHolder });
+        }
+      }
+    }
+
+    // propose
+    if (this.mock.propose && tryGet(this.settings, 'steps.propose.enable') !== false) {
+      this.receipts.propose = await getReceiptOrRevert(
+        this.mock.methods['propose(address[],uint256[],bytes[],string)'](
+          ...this.settings.proposal,
+          { from: this.settings.proposer },
+        ),
+        tryGet(this.settings, 'steps.propose.error'),
+      );
+
+      if (tryGet(this.settings, 'steps.propose.error') === undefined) {
+        this.deadline = await this.mock.proposalDeadline(this.id);
+        this.snapshot = await this.mock.proposalSnapshot(this.id);
+      }
+
+      if (tryGet(this.settings, 'steps.propose.delay')) {
+        await time.increase(tryGet(this.settings, 'steps.propose.delay'));
+      }
+
+      if (
+        tryGet(this.settings, 'steps.propose.error') === undefined &&
+        tryGet(this.settings, 'steps.propose.noadvance') !== true
+      ) {
+        await time.advanceBlockTo(this.snapshot);
+      }
+    }
+
+    // vote
+    if (tryGet(this.settings, 'voters')) {
+      this.receipts.castVote = [];
+      for (const voter of this.settings.voters) {
+        if (!voter.signature) {
+          this.receipts.castVote.push(
+            await getReceiptOrRevert(
+              voter.reason
+                ? this.mock.castVoteWithReason(this.id, voter.support, voter.reason, { from: voter.voter })
+                : this.mock.castVote(this.id, voter.support, { from: voter.voter }),
+              voter.error,
+            ),
+          );
+        } else {
+          const { v, r, s } = await voter.signature({ proposalId: this.id, support: voter.support });
+          this.receipts.castVote.push(
+            await getReceiptOrRevert(
+              this.mock.castVoteBySig(this.id, voter.support, v, r, s),
+              voter.error,
+            ),
+          );
+        }
+        if (tryGet(voter, 'delay')) {
+          await time.increase(tryGet(voter, 'delay'));
+        }
+      }
+    }
+
+    // fast forward
+    if (tryGet(this.settings, 'steps.wait.enable') !== false) {
+      await time.advanceBlockTo(this.deadline);
+    }
+
+    // queue
+    if (this.mock.queue && tryGet(this.settings, 'steps.queue.enable') !== false) {
+      this.receipts.queue = await getReceiptOrRevert(
+        this.mock.methods['queue(address[],uint256[],bytes[],bytes32)'](
+          ...this.settings.proposal.slice(0, -1),
+          this.descriptionHash,
+          { from: this.settings.queuer },
+        ),
+        tryGet(this.settings, 'steps.queue.error'),
+      );
+      this.eta = await this.mock.proposalEta(this.id);
+      if (tryGet(this.settings, 'steps.queue.delay')) {
+        await time.increase(tryGet(this.settings, 'steps.queue.delay'));
+      }
+    }
+
+    // execute
+    if (this.mock.execute && tryGet(this.settings, 'steps.execute.enable') !== false) {
+      this.receipts.execute = await getReceiptOrRevert(
+        this.mock.methods['execute(address[],uint256[],bytes[],bytes32)'](
+          ...this.settings.proposal.slice(0, -1),
+          this.descriptionHash,
+          { from: this.settings.executer },
+        ),
+        tryGet(this.settings, 'steps.execute.error'),
+      );
+      if (tryGet(this.settings, 'steps.execute.delay')) {
+        await time.increase(tryGet(this.settings, 'steps.execute.delay'));
+      }
+    }
+  });
+}
+
+module.exports = {
+  runGovernorWorkflow,
+};

+ 430 - 0
test/governance/compatibility/GovernorCompatibilityBravo.test.js

@@ -0,0 +1,430 @@
+const { BN, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers');
+const Enums = require('../../helpers/enums');
+const RLP = require('rlp');
+
+const {
+  runGovernorWorkflow,
+} = require('../GovernorWorkflow.behavior');
+
+const Token = artifacts.require('ERC20VotesCompMock');
+const Timelock = artifacts.require('CompTimelock');
+const Governor = artifacts.require('GovernorCompatibilityBravoMock');
+const CallReceiver = artifacts.require('CallReceiverMock');
+
+async function getReceiptOrRevert (promise, error = undefined) {
+  if (error) {
+    await expectRevert(promise, error);
+    return undefined;
+  } else {
+    const { receipt } = await promise;
+    return receipt;
+  }
+}
+
+function tryGet (obj, path = '') {
+  try {
+    return path.split('.').reduce((o, k) => o[k], obj);
+  } catch (_) {
+    return undefined;
+  }
+}
+
+function makeContractAddress (creator, nonce) {
+  return web3.utils.toChecksumAddress(web3.utils.sha3(RLP.encode([creator, nonce])).slice(12).substring(14));
+}
+
+contract('GovernorCompatibilityBravo', function (accounts) {
+  const [ owner, proposer, voter1, voter2, voter3, voter4, other ] = accounts;
+
+  const name = 'OZ-Governor';
+  // const version = '1';
+  const tokenName = 'MockToken';
+  const tokenSymbol = 'MTKN';
+  const tokenSupply = web3.utils.toWei('100');
+  const proposalThreshold = web3.utils.toWei('10');
+
+  beforeEach(async function () {
+    const [ deployer ] = await web3.eth.getAccounts();
+
+    this.token = await Token.new(tokenName, tokenSymbol);
+
+    // Need to predict governance address to set it as timelock admin with a delayed transfer
+    const nonce = await web3.eth.getTransactionCount(deployer);
+    const predictGovernor = makeContractAddress(deployer, nonce + 1);
+
+    this.timelock = await Timelock.new(predictGovernor, 2 * 86400);
+    this.mock = await Governor.new(name, this.token.address, 4, 16, proposalThreshold, this.timelock.address);
+    this.receiver = await CallReceiver.new();
+    await this.token.mint(owner, tokenSupply);
+    await this.token.delegate(voter1, { from: voter1 });
+    await this.token.delegate(voter2, { from: voter2 });
+    await this.token.delegate(voter3, { from: voter3 });
+    await this.token.delegate(voter4, { from: voter4 });
+
+    await this.token.transfer(proposer, proposalThreshold, { from: owner });
+    await this.token.delegate(proposer, { from: proposer });
+  });
+
+  it('deployment check', async function () {
+    expect(await this.mock.name()).to.be.equal(name);
+    expect(await this.mock.token()).to.be.equal(this.token.address);
+    expect(await this.mock.votingDelay()).to.be.bignumber.equal('4');
+    expect(await this.mock.votingPeriod()).to.be.bignumber.equal('16');
+    expect(await this.mock.quorum(0)).to.be.bignumber.equal('0');
+    expect(await this.mock.quorumVotes()).to.be.bignumber.equal('0');
+    expect(await this.mock.COUNTING_MODE()).to.be.equal('support=bravo&quorum=bravo');
+  });
+
+  describe('nominal', function () {
+    beforeEach(async function () {
+      this.settings = {
+        proposal: [
+          [ this.receiver.address ], // targets
+          [ web3.utils.toWei('0') ], // values
+          [ this.receiver.contract.methods.mockFunction().encodeABI() ], // calldatas
+          '<proposal description>', // description
+        ],
+        proposer,
+        tokenHolder: owner,
+        voters: [
+          {
+            voter: voter1,
+            weight: web3.utils.toWei('1'),
+            support: Enums.VoteType.Abstain,
+          },
+          {
+            voter: voter2,
+            weight: web3.utils.toWei('10'),
+            support: Enums.VoteType.For,
+          },
+          {
+            voter: voter3,
+            weight: web3.utils.toWei('5'),
+            support: Enums.VoteType.Against,
+          },
+          {
+            voter: voter4,
+            support: '100',
+            error: 'GovernorCompatibilityBravo: invalid vote type',
+          },
+          {
+            voter: voter1,
+            support: Enums.VoteType.For,
+            error: 'GovernorCompatibilityBravo: vote already casted',
+            skip: true,
+          },
+        ],
+        steps: {
+          queue: { delay: 7 * 86400 },
+        },
+      };
+      this.votingDelay = await this.mock.votingDelay();
+      this.votingPeriod = await this.mock.votingPeriod();
+      this.receipts = {};
+    });
+    afterEach(async function () {
+      const proposal = await this.mock.proposals(this.id);
+      expect(proposal.id).to.be.bignumber.equal(this.id);
+      expect(proposal.proposer).to.be.equal(proposer);
+      expect(proposal.eta).to.be.bignumber.equal(this.eta);
+      expect(proposal.startBlock).to.be.bignumber.equal(this.snapshot);
+      expect(proposal.endBlock).to.be.bignumber.equal(this.deadline);
+      expect(proposal.canceled).to.be.equal(false);
+      expect(proposal.executed).to.be.equal(true);
+
+      for (const [key, value] of Object.entries(Enums.VoteType)) {
+        expect(proposal[`${key.toLowerCase()}Votes`]).to.be.bignumber.equal(
+          Object.values(this.settings.voters).filter(({ support }) => support === value).reduce(
+            (acc, { weight }) => acc.add(new BN(weight)),
+            new BN('0'),
+          ),
+        );
+      }
+
+      const action = await this.mock.getActions(this.id);
+      expect(action.targets).to.be.deep.equal(this.settings.proposal[0]);
+      // expect(action.values).to.be.deep.equal(this.settings.proposal[1]);
+      expect(action.signatures).to.be.deep.equal(Array(this.settings.proposal[2].length).fill(''));
+      expect(action.calldatas).to.be.deep.equal(this.settings.proposal[2]);
+
+      for (const voter of this.settings.voters.filter(({ skip }) => !skip)) {
+        expect(await this.mock.hasVoted(this.id, voter.voter)).to.be.equal(voter.error === undefined);
+
+        const receipt = await this.mock.getReceipt(this.id, voter.voter);
+        expect(receipt.hasVoted).to.be.equal(voter.error === undefined);
+        expect(receipt.support).to.be.bignumber.equal(voter.error === undefined ? voter.support : '0');
+        expect(receipt.votes).to.be.bignumber.equal(voter.error === undefined ? voter.weight : '0');
+      }
+
+      expectEvent(
+        this.receipts.propose,
+        'ProposalCreated',
+        {
+          proposalId: this.id,
+          proposer,
+          targets: this.settings.proposal[0],
+          // values: this.settings.proposal[1].map(value => new BN(value)),
+          signatures: this.settings.proposal[2].map(() => ''),
+          calldatas: this.settings.proposal[2],
+          startBlock: new BN(this.receipts.propose.blockNumber).add(this.votingDelay),
+          endBlock: new BN(this.receipts.propose.blockNumber).add(this.votingDelay).add(this.votingPeriod),
+          description: this.settings.proposal[3],
+        },
+      );
+
+      this.receipts.castVote.filter(Boolean).forEach(vote => {
+        const { voter } = vote.logs.find(Boolean).args;
+        expectEvent(
+          vote,
+          'VoteCast',
+          this.settings.voters.find(({ address }) => address === voter),
+        );
+      });
+      expectEvent(
+        this.receipts.execute,
+        'ProposalExecuted',
+        { proposalId: this.id },
+      );
+      await expectEvent.inTransaction(
+        this.receipts.execute.transactionHash,
+        this.receiver,
+        'MockFunctionCalled',
+      );
+    });
+    runGovernorWorkflow();
+  });
+
+  describe('proposalThreshold not reached', function () {
+    beforeEach(async function () {
+      this.settings = {
+        proposal: [
+          [ this.receiver.address ], // targets
+          [ web3.utils.toWei('0') ], // values
+          [ this.receiver.contract.methods.mockFunction().encodeABI() ], // calldatas
+          '<proposal description>', // description
+        ],
+        proposer: other,
+        steps: {
+          propose: { error: 'GovernorCompatibilityBravo: proposer votes below proposal threshold' },
+          wait: { enable: false },
+          queue: { enable: false },
+          execute: { enable: false },
+        },
+      };
+    });
+    runGovernorWorkflow();
+  });
+
+  describe('with compatibility interface', function () {
+    beforeEach(async function () {
+      this.settings = {
+        proposal: [
+          [ this.receiver.address ], // targets
+          [ web3.utils.toWei('0') ], // values
+          [ '' ], // signatures
+          [ this.receiver.contract.methods.mockFunction().encodeABI() ], // calldatas
+          '<proposal description>', // description
+        ],
+        proposer,
+        tokenHolder: owner,
+        voters: [
+          {
+            voter: voter1,
+            weight: web3.utils.toWei('1'),
+            support: Enums.VoteType.Abstain,
+          },
+          {
+            voter: voter2,
+            weight: web3.utils.toWei('10'),
+            support: Enums.VoteType.For,
+          },
+          {
+            voter: voter3,
+            weight: web3.utils.toWei('5'),
+            support: Enums.VoteType.Against,
+          },
+          {
+            voter: voter4,
+            support: '100',
+            error: 'GovernorCompatibilityBravo: invalid vote type',
+          },
+          {
+            voter: voter1,
+            support: Enums.VoteType.For,
+            error: 'GovernorCompatibilityBravo: vote already casted',
+            skip: true,
+          },
+        ],
+        steps: {
+          queue: { delay: 7 * 86400 },
+        },
+      };
+      this.votingDelay = await this.mock.votingDelay();
+      this.votingPeriod = await this.mock.votingPeriod();
+      this.receipts = {};
+    });
+
+    afterEach(async function () {
+      const proposal = await this.mock.proposals(this.id);
+      expect(proposal.id).to.be.bignumber.equal(this.id);
+      expect(proposal.proposer).to.be.equal(proposer);
+      expect(proposal.eta).to.be.bignumber.equal(this.eta);
+      expect(proposal.startBlock).to.be.bignumber.equal(this.snapshot);
+      expect(proposal.endBlock).to.be.bignumber.equal(this.deadline);
+      expect(proposal.canceled).to.be.equal(false);
+      expect(proposal.executed).to.be.equal(true);
+
+      for (const [key, value] of Object.entries(Enums.VoteType)) {
+        expect(proposal[`${key.toLowerCase()}Votes`]).to.be.bignumber.equal(
+          Object.values(this.settings.voters).filter(({ support }) => support === value).reduce(
+            (acc, { weight }) => acc.add(new BN(weight)),
+            new BN('0'),
+          ),
+        );
+      }
+
+      const action = await this.mock.getActions(this.id);
+      expect(action.targets).to.be.deep.equal(this.settings.proposal[0]);
+      // expect(action.values).to.be.deep.equal(this.settings.proposal[1]);
+      expect(action.signatures).to.be.deep.equal(this.settings.proposal[2]);
+      expect(action.calldatas).to.be.deep.equal(this.settings.proposal[3]);
+
+      for (const voter of this.settings.voters.filter(({ skip }) => !skip)) {
+        expect(await this.mock.hasVoted(this.id, voter.voter)).to.be.equal(voter.error === undefined);
+
+        const receipt = await this.mock.getReceipt(this.id, voter.voter);
+        expect(receipt.hasVoted).to.be.equal(voter.error === undefined);
+        expect(receipt.support).to.be.bignumber.equal(voter.error === undefined ? voter.support : '0');
+        expect(receipt.votes).to.be.bignumber.equal(voter.error === undefined ? voter.weight : '0');
+      }
+
+      expectEvent(
+        this.receipts.propose,
+        'ProposalCreated',
+        {
+          proposalId: this.id,
+          proposer,
+          targets: this.settings.proposal[0],
+          // values: this.settings.proposal[1].map(value => new BN(value)),
+          signatures: this.settings.proposal[2],
+          calldatas: this.settings.proposal[3],
+          startBlock: new BN(this.receipts.propose.blockNumber).add(this.votingDelay),
+          endBlock: new BN(this.receipts.propose.blockNumber).add(this.votingDelay).add(this.votingPeriod),
+          description: this.settings.proposal[4],
+        },
+      );
+
+      this.receipts.castVote.filter(Boolean).forEach(vote => {
+        const { voter } = vote.logs.find(Boolean).args;
+        expectEvent(
+          vote,
+          'VoteCast',
+          this.settings.voters.find(({ address }) => address === voter),
+        );
+      });
+      expectEvent(
+        this.receipts.execute,
+        'ProposalExecuted',
+        { proposalId: this.id },
+      );
+      await expectEvent.inTransaction(
+        this.receipts.execute.transactionHash,
+        this.receiver,
+        'MockFunctionCalled',
+      );
+    });
+
+    it('run', async function () {
+      // transfer tokens
+      if (tryGet(this.settings, 'voters')) {
+        for (const voter of this.settings.voters) {
+          if (voter.weight) {
+            await this.token.transfer(voter.voter, voter.weight, { from: this.settings.tokenHolder });
+          }
+        }
+      }
+
+      // propose
+      if (this.mock.propose && tryGet(this.settings, 'steps.propose.enable') !== false) {
+        this.receipts.propose = await getReceiptOrRevert(
+          this.mock.methods['propose(address[],uint256[],string[],bytes[],string)'](
+            ...this.settings.proposal,
+            { from: this.settings.proposer },
+          ),
+          tryGet(this.settings, 'steps.propose.error'),
+        );
+
+        if (tryGet(this.settings, 'steps.propose.error') === undefined) {
+          this.id = this.receipts.propose.logs.find(({ event }) => event === 'ProposalCreated').args.proposalId;
+          this.snapshot = await this.mock.proposalSnapshot(this.id);
+          this.deadline = await this.mock.proposalDeadline(this.id);
+        }
+
+        if (tryGet(this.settings, 'steps.propose.delay')) {
+          await time.increase(tryGet(this.settings, 'steps.propose.delay'));
+        }
+
+        if (
+          tryGet(this.settings, 'steps.propose.error') === undefined &&
+          tryGet(this.settings, 'steps.propose.noadvance') !== true
+        ) {
+          await time.advanceBlockTo(this.snapshot);
+        }
+      }
+
+      // vote
+      if (tryGet(this.settings, 'voters')) {
+        this.receipts.castVote = [];
+        for (const voter of this.settings.voters) {
+          if (!voter.signature) {
+            this.receipts.castVote.push(
+              await getReceiptOrRevert(
+                this.mock.castVote(this.id, voter.support, { from: voter.voter }),
+                voter.error,
+              ),
+            );
+          } else {
+            const { v, r, s } = await voter.signature({ proposalId: this.id, support: voter.support });
+            this.receipts.castVote.push(
+              await getReceiptOrRevert(
+                this.mock.castVoteBySig(this.id, voter.support, v, r, s),
+                voter.error,
+              ),
+            );
+          }
+          if (tryGet(voter, 'delay')) {
+            await time.increase(tryGet(voter, 'delay'));
+          }
+        }
+      }
+
+      // fast forward
+      if (tryGet(this.settings, 'steps.wait.enable') !== false) {
+        await time.advanceBlockTo(this.deadline);
+      }
+
+      // queue
+      if (this.mock.queue && tryGet(this.settings, 'steps.queue.enable') !== false) {
+        this.receipts.queue = await getReceiptOrRevert(
+          this.mock.methods['queue(uint256)'](this.id, { from: this.settings.queuer }),
+          tryGet(this.settings, 'steps.queue.error'),
+        );
+        this.eta = await this.mock.proposalEta(this.id);
+        if (tryGet(this.settings, 'steps.queue.delay')) {
+          await time.increase(tryGet(this.settings, 'steps.queue.delay'));
+        }
+      }
+
+      // execute
+      if (this.mock.execute && tryGet(this.settings, 'steps.execute.enable') !== false) {
+        this.receipts.execute = await getReceiptOrRevert(
+          this.mock.methods['execute(uint256)'](this.id, { from: this.settings.executer }),
+          tryGet(this.settings, 'steps.execute.error'),
+        );
+        if (tryGet(this.settings, 'steps.execute.delay')) {
+          await time.increase(tryGet(this.settings, 'steps.execute.delay'));
+        }
+      }
+    });
+  });
+});

+ 87 - 0
test/governance/extensions/GovernorComp.test.js

@@ -0,0 +1,87 @@
+const { BN, expectEvent } = require('@openzeppelin/test-helpers');
+const Enums = require('../../helpers/enums');
+
+const {
+  runGovernorWorkflow,
+} = require('./../GovernorWorkflow.behavior');
+
+const Token = artifacts.require('ERC20VotesCompMock');
+const Governor = artifacts.require('GovernorCompMock');
+const CallReceiver = artifacts.require('CallReceiverMock');
+
+contract('GovernorComp', function (accounts) {
+  const [ owner, voter1, voter2, voter3, voter4 ] = accounts;
+
+  const name = 'OZ-Governor';
+  // const version = '1';
+  const tokenName = 'MockToken';
+  const tokenSymbol = 'MTKN';
+  const tokenSupply = web3.utils.toWei('100');
+
+  beforeEach(async function () {
+    this.owner = owner;
+    this.token = await Token.new(tokenName, tokenSymbol);
+    this.mock = await Governor.new(name, this.token.address, 4, 16);
+    this.receiver = await CallReceiver.new();
+    await this.token.mint(owner, tokenSupply);
+    await this.token.delegate(voter1, { from: voter1 });
+    await this.token.delegate(voter2, { from: voter2 });
+    await this.token.delegate(voter3, { from: voter3 });
+    await this.token.delegate(voter4, { from: voter4 });
+  });
+
+  it('deployment check', async function () {
+    expect(await this.mock.name()).to.be.equal(name);
+    expect(await this.mock.token()).to.be.equal(this.token.address);
+    expect(await this.mock.votingDelay()).to.be.bignumber.equal('4');
+    expect(await this.mock.votingPeriod()).to.be.bignumber.equal('16');
+    expect(await this.mock.quorum(0)).to.be.bignumber.equal('0');
+  });
+
+  describe('voting with comp token', function () {
+    beforeEach(async function () {
+      this.settings = {
+        proposal: [
+          [ this.receiver.address ],
+          [ web3.utils.toWei('0') ],
+          [ this.receiver.contract.methods.mockFunction().encodeABI() ],
+          '<proposal description>',
+        ],
+        tokenHolder: owner,
+        voters: [
+          { voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.For },
+          { voter: voter2, weight: web3.utils.toWei('10'), support: Enums.VoteType.For },
+          { voter: voter3, weight: web3.utils.toWei('5'), support: Enums.VoteType.Against },
+          { voter: voter4, weight: web3.utils.toWei('2'), support: Enums.VoteType.Abstain },
+        ],
+      };
+    });
+    afterEach(async function () {
+      expect(await this.mock.hasVoted(this.id, owner)).to.be.equal(false);
+      expect(await this.mock.hasVoted(this.id, voter1)).to.be.equal(true);
+      expect(await this.mock.hasVoted(this.id, voter2)).to.be.equal(true);
+      expect(await this.mock.hasVoted(this.id, voter3)).to.be.equal(true);
+      expect(await this.mock.hasVoted(this.id, voter4)).to.be.equal(true);
+
+      this.receipts.castVote.filter(Boolean).forEach(vote => {
+        const { voter } = vote.logs.find(Boolean).args;
+        expectEvent(
+          vote,
+          'VoteCast',
+          this.settings.voters.find(({ address }) => address === voter),
+        );
+      });
+      await this.mock.proposalVotes(this.id).then(result => {
+        for (const [key, value] of Object.entries(Enums.VoteType)) {
+          expect(result[`${key.toLowerCase()}Votes`]).to.be.bignumber.equal(
+            Object.values(this.settings.voters).filter(({ support }) => support === value).reduce(
+              (acc, { weight }) => acc.add(new BN(weight)),
+              new BN('0'),
+            ),
+          );
+        }
+      });
+    });
+    runGovernorWorkflow();
+  });
+});

+ 432 - 0
test/governance/extensions/GovernorTimelockCompound.test.js

@@ -0,0 +1,432 @@
+const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
+const { expect } = require('chai');
+const Enums = require('../../helpers/enums');
+const RLP = require('rlp');
+
+const {
+  runGovernorWorkflow,
+} = require('../GovernorWorkflow.behavior');
+
+const {
+  shouldSupportInterfaces,
+} = require('../../utils/introspection/SupportsInterface.behavior');
+
+const Token = artifacts.require('ERC20VotesMock');
+const Timelock = artifacts.require('CompTimelock');
+const Governor = artifacts.require('GovernorTimelockCompoundMock');
+const CallReceiver = artifacts.require('CallReceiverMock');
+
+function makeContractAddress (creator, nonce) {
+  return web3.utils.toChecksumAddress(web3.utils.sha3(RLP.encode([creator, nonce])).slice(12).substring(14));
+}
+
+contract('GovernorTimelockCompound', function (accounts) {
+  const [ admin, voter ] = accounts;
+
+  const name = 'OZ-Governor';
+  // const version = '1';
+  const tokenName = 'MockToken';
+  const tokenSymbol = 'MTKN';
+  const tokenSupply = web3.utils.toWei('100');
+
+  beforeEach(async function () {
+    const [ deployer ] = await web3.eth.getAccounts();
+
+    this.token = await Token.new(tokenName, tokenSymbol);
+
+    // Need to predict governance address to set it as timelock admin with a delayed transfer
+    const nonce = await web3.eth.getTransactionCount(deployer);
+    const predictGovernor = makeContractAddress(deployer, nonce + 1);
+
+    this.timelock = await Timelock.new(predictGovernor, 2 * 86400);
+    this.mock = await Governor.new(name, this.token.address, 4, 16, this.timelock.address, 0);
+    this.receiver = await CallReceiver.new();
+    await this.token.mint(voter, tokenSupply);
+    await this.token.delegate(voter, { from: voter });
+  });
+
+  shouldSupportInterfaces([
+    'ERC165',
+    'Governor',
+    'GovernorTimelock',
+  ]);
+
+  it('post deployment check', async function () {
+    expect(await this.mock.name()).to.be.equal(name);
+    expect(await this.mock.token()).to.be.equal(this.token.address);
+    expect(await this.mock.votingDelay()).to.be.bignumber.equal('4');
+    expect(await this.mock.votingPeriod()).to.be.bignumber.equal('16');
+    expect(await this.mock.quorum(0)).to.be.bignumber.equal('0');
+
+    expect(await this.mock.timelock()).to.be.equal(this.timelock.address);
+    expect(await this.timelock.admin()).to.be.equal(this.mock.address);
+  });
+
+  describe('nominal', function () {
+    beforeEach(async function () {
+      this.settings = {
+        proposal: [
+          [ this.receiver.address ],
+          [ web3.utils.toWei('0') ],
+          [ this.receiver.contract.methods.mockFunction().encodeABI() ],
+          '<proposal description>',
+        ],
+        voters: [
+          { voter: voter, support: Enums.VoteType.For },
+        ],
+        steps: {
+          queue: { delay: 7 * 86400 },
+        },
+      };
+    });
+    afterEach(async function () {
+      expectEvent(
+        this.receipts.propose,
+        'ProposalCreated',
+        { proposalId: this.id },
+      );
+      expectEvent(
+        this.receipts.queue,
+        'ProposalQueued',
+        { proposalId: this.id },
+      );
+      await expectEvent.inTransaction(
+        this.receipts.queue.transactionHash,
+        this.timelock,
+        'QueueTransaction',
+        { eta: this.eta },
+      );
+      expectEvent(
+        this.receipts.execute,
+        'ProposalExecuted',
+        { proposalId: this.id },
+      );
+      await expectEvent.inTransaction(
+        this.receipts.execute.transactionHash,
+        this.timelock,
+        'ExecuteTransaction',
+        { eta: this.eta },
+      );
+      await expectEvent.inTransaction(
+        this.receipts.execute.transactionHash,
+        this.receiver,
+        'MockFunctionCalled',
+      );
+    });
+    runGovernorWorkflow();
+  });
+
+  describe('not queued', function () {
+    beforeEach(async function () {
+      this.settings = {
+        proposal: [
+          [ this.receiver.address ],
+          [ web3.utils.toWei('0') ],
+          [ this.receiver.contract.methods.mockFunction().encodeABI() ],
+          '<proposal description>',
+        ],
+        voters: [
+          { voter: voter, support: Enums.VoteType.For },
+        ],
+        steps: {
+          queue: { enable: false },
+          execute: { error: 'GovernorTimelockCompound: proposal not yet queued' },
+        },
+      };
+    });
+    afterEach(async function () {
+      expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded);
+    });
+    runGovernorWorkflow();
+  });
+
+  describe('to early', function () {
+    beforeEach(async function () {
+      this.settings = {
+        proposal: [
+          [ this.receiver.address ],
+          [ web3.utils.toWei('0') ],
+          [ this.receiver.contract.methods.mockFunction().encodeABI() ],
+          '<proposal description>',
+        ],
+        voters: [
+          { voter: voter, support: Enums.VoteType.For },
+        ],
+        steps: {
+          execute: { error: 'Timelock::executeTransaction: Transaction hasn\'t surpassed time lock' },
+        },
+      };
+    });
+    afterEach(async function () {
+      expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Queued);
+    });
+    runGovernorWorkflow();
+  });
+
+  describe('to late', function () {
+    beforeEach(async function () {
+      this.settings = {
+        proposal: [
+          [ this.receiver.address ],
+          [ web3.utils.toWei('0') ],
+          [ this.receiver.contract.methods.mockFunction().encodeABI() ],
+          '<proposal description>',
+        ],
+        voters: [
+          { voter: voter, support: Enums.VoteType.For },
+        ],
+        steps: {
+          queue: { delay: 30 * 86400 },
+          execute: { error: 'Governor: proposal not successful' },
+        },
+      };
+    });
+    afterEach(async function () {
+      expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Expired);
+    });
+    runGovernorWorkflow();
+  });
+
+  describe('deplicated underlying call', function () {
+    beforeEach(async function () {
+      this.settings = {
+        proposal: [
+          Array(2).fill(this.token.address),
+          Array(2).fill(web3.utils.toWei('0')),
+          Array(2).fill(this.token.contract.methods.approve(this.receiver.address, constants.MAX_UINT256).encodeABI()),
+          '<proposal description>',
+        ],
+        voters: [
+          { voter: voter, support: Enums.VoteType.For },
+        ],
+        steps: {
+          queue: {
+            error: 'GovernorTimelockCompound: identical proposal action already queued',
+          },
+          execute: {
+            error: 'GovernorTimelockCompound: proposal not yet queued',
+          },
+        },
+      };
+    });
+    runGovernorWorkflow();
+  });
+
+  describe('re-queue / re-execute', function () {
+    beforeEach(async function () {
+      this.settings = {
+        proposal: [
+          [ this.receiver.address ],
+          [ web3.utils.toWei('0') ],
+          [ this.receiver.contract.methods.mockFunction().encodeABI() ],
+          '<proposal description>',
+        ],
+        voters: [
+          { voter: voter, support: Enums.VoteType.For },
+        ],
+        steps: {
+          queue: { delay: 7 * 86400 },
+        },
+      };
+    });
+    afterEach(async function () {
+      expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Executed);
+
+      await expectRevert(
+        this.mock.queue(...this.settings.proposal.slice(0, -1), this.descriptionHash),
+        'Governor: proposal not successful',
+      );
+      await expectRevert(
+        this.mock.execute(...this.settings.proposal.slice(0, -1), this.descriptionHash),
+        'Governor: proposal not successful',
+      );
+    });
+    runGovernorWorkflow();
+  });
+
+  describe('cancel before queue prevents scheduling', function () {
+    beforeEach(async function () {
+      this.settings = {
+        proposal: [
+          [ this.receiver.address ],
+          [ web3.utils.toWei('0') ],
+          [ this.receiver.contract.methods.mockFunction().encodeABI() ],
+          '<proposal description>',
+        ],
+        voters: [
+          { voter: voter, support: Enums.VoteType.For },
+        ],
+        steps: {
+          queue: { enable: false },
+          execute: { enable: false },
+        },
+      };
+    });
+    afterEach(async function () {
+      expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded);
+
+      expectEvent(
+        await this.mock.cancel(...this.settings.proposal.slice(0, -1), this.descriptionHash),
+        'ProposalCanceled',
+        { proposalId: this.id },
+      );
+
+      expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
+
+      await expectRevert(
+        this.mock.queue(...this.settings.proposal.slice(0, -1), this.descriptionHash),
+        'Governor: proposal not successful',
+      );
+    });
+    runGovernorWorkflow();
+  });
+
+  describe('cancel after queue prevents executing', function () {
+    beforeEach(async function () {
+      this.settings = {
+        proposal: [
+          [ this.receiver.address ],
+          [ web3.utils.toWei('0') ],
+          [ this.receiver.contract.methods.mockFunction().encodeABI() ],
+          '<proposal description>',
+        ],
+        voters: [
+          { voter: voter, support: Enums.VoteType.For },
+        ],
+        steps: {
+          queue: { delay: 7 * 86400 },
+          execute: { enable: false },
+        },
+      };
+    });
+    afterEach(async function () {
+      expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Queued);
+
+      const receipt = await this.mock.cancel(...this.settings.proposal.slice(0, -1), this.descriptionHash);
+      expectEvent(
+        receipt,
+        'ProposalCanceled',
+        { proposalId: this.id },
+      );
+      await expectEvent.inTransaction(
+        receipt.receipt.transactionHash,
+        this.timelock,
+        'CancelTransaction',
+      );
+
+      expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
+
+      await expectRevert(
+        this.mock.execute(...this.settings.proposal.slice(0, -1), this.descriptionHash),
+        'Governor: proposal not successful',
+      );
+    });
+    runGovernorWorkflow();
+  });
+
+  describe('updateTimelock', function () {
+    beforeEach(async function () {
+      this.newTimelock = await Timelock.new(this.mock.address, 7 * 86400);
+    });
+
+    it('protected', async function () {
+      await expectRevert(
+        this.mock.updateTimelock(this.newTimelock.address),
+        'Governor: onlyGovernance',
+      );
+    });
+
+    describe('using workflow', function () {
+      beforeEach(async function () {
+        this.settings = {
+          proposal: [
+            [
+              this.timelock.address,
+              this.mock.address,
+            ],
+            [
+              web3.utils.toWei('0'),
+              web3.utils.toWei('0'),
+            ],
+            [
+              this.timelock.contract.methods.setPendingAdmin(admin).encodeABI(),
+              this.mock.contract.methods.updateTimelock(this.newTimelock.address).encodeABI(),
+            ],
+            '<proposal description>',
+          ],
+          voters: [
+            { voter: voter, support: Enums.VoteType.For },
+          ],
+          steps: {
+            queue: { delay: 7 * 86400 },
+          },
+        };
+      });
+      afterEach(async function () {
+        expectEvent(
+          this.receipts.propose,
+          'ProposalCreated',
+          { proposalId: this.id },
+        );
+        expectEvent(
+          this.receipts.execute,
+          'ProposalExecuted',
+          { proposalId: this.id },
+        );
+        expectEvent(
+          this.receipts.execute,
+          'TimelockChange',
+          { oldTimelock: this.timelock.address, newTimelock: this.newTimelock.address },
+        );
+        expect(await this.mock.timelock()).to.be.bignumber.equal(this.newTimelock.address);
+      });
+      runGovernorWorkflow();
+    });
+  });
+
+  describe('transfer timelock to new governor', function () {
+    beforeEach(async function () {
+      this.newGovernor = await Governor.new(name, this.token.address, 8, 32, this.timelock.address, 0);
+    });
+
+    describe('using workflow', function () {
+      beforeEach(async function () {
+        this.settings = {
+          proposal: [
+            [ this.timelock.address ],
+            [ web3.utils.toWei('0') ],
+            [ this.timelock.contract.methods.setPendingAdmin(this.newGovernor.address).encodeABI() ],
+            '<proposal description>',
+          ],
+          voters: [
+            { voter: voter, support: Enums.VoteType.For },
+          ],
+          steps: {
+            queue: { delay: 7 * 86400 },
+          },
+        };
+      });
+      afterEach(async function () {
+        expectEvent(
+          this.receipts.propose,
+          'ProposalCreated',
+          { proposalId: this.id },
+        );
+        expectEvent(
+          this.receipts.execute,
+          'ProposalExecuted',
+          { proposalId: this.id },
+        );
+        await expectEvent.inTransaction(
+          this.receipts.execute.transactionHash,
+          this.timelock,
+          'NewPendingAdmin',
+          { newPendingAdmin: this.newGovernor.address },
+        );
+        await this.newGovernor.__acceptAdmin();
+        expect(await this.timelock.admin()).to.be.bignumber.equal(this.newGovernor.address);
+      });
+      runGovernorWorkflow();
+    });
+  });
+});

+ 369 - 0
test/governance/extensions/GovernorTimelockControl.test.js

@@ -0,0 +1,369 @@
+const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
+const { expect } = require('chai');
+const Enums = require('../../helpers/enums');
+
+const {
+  runGovernorWorkflow,
+} = require('../GovernorWorkflow.behavior');
+
+const {
+  shouldSupportInterfaces,
+} = require('../../utils/introspection/SupportsInterface.behavior');
+
+const Token = artifacts.require('ERC20VotesMock');
+const Timelock = artifacts.require('TimelockController');
+const Governor = artifacts.require('GovernorTimelockControlMock');
+const CallReceiver = artifacts.require('CallReceiverMock');
+
+contract('GovernorTimelockControl', function (accounts) {
+  const [ voter ] = accounts;
+
+  const name = 'OZ-Governor';
+  // const version = '1';
+  const tokenName = 'MockToken';
+  const tokenSymbol = 'MTKN';
+  const tokenSupply = web3.utils.toWei('100');
+
+  beforeEach(async function () {
+    const [ deployer ] = await web3.eth.getAccounts();
+
+    this.token = await Token.new(tokenName, tokenSymbol);
+    this.timelock = await Timelock.new(3600, [], []);
+    this.mock = await Governor.new(name, this.token.address, 4, 16, this.timelock.address, 0);
+    this.receiver = await CallReceiver.new();
+    // normal setup: governor is proposer, everyone is executor, timelock is its own admin
+    await this.timelock.grantRole(await this.timelock.PROPOSER_ROLE(), this.mock.address);
+    await this.timelock.grantRole(await this.timelock.EXECUTOR_ROLE(), constants.ZERO_ADDRESS);
+    await this.timelock.revokeRole(await this.timelock.TIMELOCK_ADMIN_ROLE(), deployer);
+    await this.token.mint(voter, tokenSupply);
+    await this.token.delegate(voter, { from: voter });
+  });
+
+  shouldSupportInterfaces([
+    'ERC165',
+    'Governor',
+    'GovernorTimelock',
+  ]);
+
+  it('post deployment check', async function () {
+    expect(await this.mock.name()).to.be.equal(name);
+    expect(await this.mock.token()).to.be.equal(this.token.address);
+    expect(await this.mock.votingDelay()).to.be.bignumber.equal('4');
+    expect(await this.mock.votingPeriod()).to.be.bignumber.equal('16');
+    expect(await this.mock.quorum(0)).to.be.bignumber.equal('0');
+
+    expect(await this.mock.timelock()).to.be.equal(this.timelock.address);
+  });
+
+  describe('nominal', function () {
+    beforeEach(async function () {
+      this.settings = {
+        proposal: [
+          [ this.receiver.address ],
+          [ web3.utils.toWei('0') ],
+          [ this.receiver.contract.methods.mockFunction().encodeABI() ],
+          '<proposal description>',
+        ],
+        voters: [
+          { voter: voter, support: Enums.VoteType.For },
+        ],
+        steps: {
+          queue: { delay: 3600 },
+        },
+      };
+    });
+    afterEach(async function () {
+      const timelockid = await this.timelock.hashOperationBatch(
+        ...this.settings.proposal.slice(0, 3),
+        '0x0',
+        this.descriptionHash,
+      );
+
+      expectEvent(
+        this.receipts.propose,
+        'ProposalCreated',
+        { proposalId: this.id },
+      );
+      expectEvent(
+        this.receipts.queue,
+        'ProposalQueued',
+        { proposalId: this.id },
+      );
+      await expectEvent.inTransaction(
+        this.receipts.queue.transactionHash,
+        this.timelock,
+        'CallScheduled',
+        { id: timelockid },
+      );
+      expectEvent(
+        this.receipts.execute,
+        'ProposalExecuted',
+        { proposalId: this.id },
+      );
+      await expectEvent.inTransaction(
+        this.receipts.execute.transactionHash,
+        this.timelock,
+        'CallExecuted',
+        { id: timelockid },
+      );
+      await expectEvent.inTransaction(
+        this.receipts.execute.transactionHash,
+        this.receiver,
+        'MockFunctionCalled',
+      );
+    });
+    runGovernorWorkflow();
+  });
+
+  describe('executed by other proposer', function () {
+    beforeEach(async function () {
+      this.settings = {
+        proposal: [
+          [ this.receiver.address ],
+          [ web3.utils.toWei('0') ],
+          [ this.receiver.contract.methods.mockFunction().encodeABI() ],
+          '<proposal description>',
+        ],
+        voters: [
+          { voter: voter, support: Enums.VoteType.For },
+        ],
+        steps: {
+          queue: { delay: 3600 },
+          execute: { enable: false },
+        },
+      };
+    });
+    afterEach(async function () {
+      await this.timelock.executeBatch(
+        ...this.settings.proposal.slice(0, 3),
+        '0x0',
+        this.descriptionHash,
+      );
+
+      expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Executed);
+
+      await expectRevert(
+        this.mock.execute(...this.settings.proposal.slice(0, -1), this.descriptionHash),
+        'Governor: proposal not successful',
+      );
+    });
+    runGovernorWorkflow();
+  });
+
+  describe('not queued', function () {
+    beforeEach(async function () {
+      this.settings = {
+        proposal: [
+          [ this.receiver.address ],
+          [ web3.utils.toWei('0') ],
+          [ this.receiver.contract.methods.mockFunction().encodeABI() ],
+          '<proposal description>',
+        ],
+        voters: [
+          { voter: voter, support: Enums.VoteType.For },
+        ],
+        steps: {
+          queue: { enable: false },
+          execute: { error: 'TimelockController: operation is not ready' },
+        },
+      };
+    });
+    afterEach(async function () {
+      expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded);
+    });
+    runGovernorWorkflow();
+  });
+
+  describe('to early', function () {
+    beforeEach(async function () {
+      this.settings = {
+        proposal: [
+          [ this.receiver.address ],
+          [ web3.utils.toWei('0') ],
+          [ this.receiver.contract.methods.mockFunction().encodeABI() ],
+          '<proposal description>',
+        ],
+        voters: [
+          { voter: voter, support: Enums.VoteType.For },
+        ],
+        steps: {
+          execute: { error: 'TimelockController: operation is not ready' },
+        },
+      };
+    });
+    afterEach(async function () {
+      expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Queued);
+    });
+    runGovernorWorkflow();
+  });
+
+  describe('re-queue / re-execute', function () {
+    beforeEach(async function () {
+      this.settings = {
+        proposal: [
+          [ this.receiver.address ],
+          [ web3.utils.toWei('0') ],
+          [ this.receiver.contract.methods.mockFunction().encodeABI() ],
+          '<proposal description>',
+        ],
+        voters: [
+          { voter: voter, support: Enums.VoteType.For },
+        ],
+        steps: {
+          queue: { delay: 3600 },
+        },
+      };
+    });
+    afterEach(async function () {
+      expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Executed);
+
+      await expectRevert(
+        this.mock.queue(...this.settings.proposal.slice(0, -1), this.descriptionHash),
+        'Governor: proposal not successful',
+      );
+      await expectRevert(
+        this.mock.execute(...this.settings.proposal.slice(0, -1), this.descriptionHash),
+        'Governor: proposal not successful',
+      );
+    });
+    runGovernorWorkflow();
+  });
+
+  describe('cancel before queue prevents scheduling', function () {
+    beforeEach(async function () {
+      this.settings = {
+        proposal: [
+          [ this.receiver.address ],
+          [ web3.utils.toWei('0') ],
+          [ this.receiver.contract.methods.mockFunction().encodeABI() ],
+          '<proposal description>',
+        ],
+        voters: [
+          { voter: voter, support: Enums.VoteType.For },
+        ],
+        steps: {
+          queue: { enable: false },
+          execute: { enable: false },
+        },
+      };
+    });
+    afterEach(async function () {
+      expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded);
+
+      expectEvent(
+        await this.mock.cancel(...this.settings.proposal.slice(0, -1), this.descriptionHash),
+        'ProposalCanceled',
+        { proposalId: this.id },
+      );
+
+      expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
+
+      await expectRevert(
+        this.mock.queue(...this.settings.proposal.slice(0, -1), this.descriptionHash),
+        'Governor: proposal not successful',
+      );
+    });
+    runGovernorWorkflow();
+  });
+
+  describe('cancel after queue prevents execution', function () {
+    beforeEach(async function () {
+      this.settings = {
+        proposal: [
+          [ this.receiver.address ],
+          [ web3.utils.toWei('0') ],
+          [ this.receiver.contract.methods.mockFunction().encodeABI() ],
+          '<proposal description>',
+        ],
+        voters: [
+          { voter: voter, support: Enums.VoteType.For },
+        ],
+        steps: {
+          queue: { delay: 3600 },
+          execute: { enable: false },
+        },
+      };
+    });
+    afterEach(async function () {
+      const timelockid = await this.timelock.hashOperationBatch(
+        ...this.settings.proposal.slice(0, 3),
+        '0x0',
+        this.descriptionHash,
+      );
+
+      expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Queued);
+
+      const receipt = await this.mock.cancel(...this.settings.proposal.slice(0, -1), this.descriptionHash);
+      expectEvent(
+        receipt,
+        'ProposalCanceled',
+        { proposalId: this.id },
+      );
+      await expectEvent.inTransaction(
+        receipt.receipt.transactionHash,
+        this.timelock,
+        'Cancelled',
+        { id: timelockid },
+      );
+
+      expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
+
+      await expectRevert(
+        this.mock.execute(...this.settings.proposal.slice(0, -1), this.descriptionHash),
+        'Governor: proposal not successful',
+      );
+    });
+    runGovernorWorkflow();
+  });
+
+  describe('updateTimelock', function () {
+    beforeEach(async function () {
+      this.newTimelock = await Timelock.new(3600, [], []);
+    });
+
+    it('protected', async function () {
+      await expectRevert(
+        this.mock.updateTimelock(this.newTimelock.address),
+        'Governor: onlyGovernance',
+      );
+    });
+
+    describe('using workflow', function () {
+      beforeEach(async function () {
+        this.settings = {
+          proposal: [
+            [ this.mock.address ],
+            [ web3.utils.toWei('0') ],
+            [ this.mock.contract.methods.updateTimelock(this.newTimelock.address).encodeABI() ],
+            '<proposal description>',
+          ],
+          voters: [
+            { voter: voter, support: Enums.VoteType.For },
+          ],
+          steps: {
+            queue: { delay: 3600 },
+          },
+        };
+      });
+      afterEach(async function () {
+        expectEvent(
+          this.receipts.propose,
+          'ProposalCreated',
+          { proposalId: this.id },
+        );
+        expectEvent(
+          this.receipts.execute,
+          'ProposalExecuted',
+          { proposalId: this.id },
+        );
+        expectEvent(
+          this.receipts.execute,
+          'TimelockChange',
+          { oldTimelock: this.timelock.address, newTimelock: this.newTimelock.address },
+        );
+        expect(await this.mock.timelock()).to.be.bignumber.equal(this.newTimelock.address);
+      });
+      runGovernorWorkflow();
+    });
+  });
+});

+ 122 - 0
test/governance/extensions/GovernorWeightQuorumFraction.test.js

@@ -0,0 +1,122 @@
+const { BN, expectEvent, time } = require('@openzeppelin/test-helpers');
+const Enums = require('../../helpers/enums');
+
+const {
+  runGovernorWorkflow,
+} = require('./../GovernorWorkflow.behavior');
+
+const Token = artifacts.require('ERC20VotesMock');
+const Governor = artifacts.require('GovernorMock');
+const CallReceiver = artifacts.require('CallReceiverMock');
+
+contract('GovernorVotesQuorumFraction', function (accounts) {
+  const [ owner, voter1, voter2, voter3, voter4 ] = accounts;
+
+  const name = 'OZ-Governor';
+  // const version = '1';
+  const tokenName = 'MockToken';
+  const tokenSymbol = 'MTKN';
+  const tokenSupply = new BN(web3.utils.toWei('100'));
+  const ratio = new BN(8); // percents
+  const newRatio = new BN(6); // percents
+
+  beforeEach(async function () {
+    this.owner = owner;
+    this.token = await Token.new(tokenName, tokenSymbol);
+    this.mock = await Governor.new(name, this.token.address, 4, 16, ratio);
+    this.receiver = await CallReceiver.new();
+    await this.token.mint(owner, tokenSupply);
+    await this.token.delegate(voter1, { from: voter1 });
+    await this.token.delegate(voter2, { from: voter2 });
+    await this.token.delegate(voter3, { from: voter3 });
+    await this.token.delegate(voter4, { from: voter4 });
+  });
+
+  it('deployment check', async function () {
+    expect(await this.mock.name()).to.be.equal(name);
+    expect(await this.mock.token()).to.be.equal(this.token.address);
+    expect(await this.mock.votingDelay()).to.be.bignumber.equal('4');
+    expect(await this.mock.votingPeriod()).to.be.bignumber.equal('16');
+    expect(await this.mock.quorum(0)).to.be.bignumber.equal('0');
+    expect(await this.mock.quorumNumerator()).to.be.bignumber.equal(ratio);
+    expect(await this.mock.quorumDenominator()).to.be.bignumber.equal('100');
+    expect(await time.latestBlock().then(blockNumber => this.mock.quorum(blockNumber.subn(1))))
+      .to.be.bignumber.equal(tokenSupply.mul(ratio).divn(100));
+  });
+
+  describe('quroum not reached', function () {
+    beforeEach(async function () {
+      this.settings = {
+        proposal: [
+          [ this.receiver.address ],
+          [ web3.utils.toWei('0') ],
+          [ this.receiver.contract.methods.mockFunction().encodeABI() ],
+          '<proposal description>',
+        ],
+        tokenHolder: owner,
+        voters: [
+          { voter: voter1, weight: web3.utils.toWei('1'), support: Enums.VoteType.For },
+        ],
+        steps: {
+          execute: { error: 'Governor: proposal not successful' },
+        },
+      };
+    });
+    runGovernorWorkflow();
+  });
+
+  describe('update quorum ratio through proposal', function () {
+    beforeEach(async function () {
+      this.settings = {
+        proposal: [
+          [ this.mock.address ],
+          [ web3.utils.toWei('0') ],
+          [ this.mock.contract.methods.updateQuorumNumerator(newRatio).encodeABI() ],
+          '<proposal description>',
+        ],
+        tokenHolder: owner,
+        voters: [
+          { voter: voter1, weight: tokenSupply, support: Enums.VoteType.For },
+        ],
+      };
+    });
+    afterEach(async function () {
+      await expectEvent.inTransaction(
+        this.receipts.execute.transactionHash,
+        this.mock,
+        'QuorumNumeratorUpdated',
+        {
+          oldQuorumNumerator: ratio,
+          newQuorumNumerator: newRatio,
+        },
+      );
+
+      expect(await this.mock.quorumNumerator()).to.be.bignumber.equal(newRatio);
+      expect(await this.mock.quorumDenominator()).to.be.bignumber.equal('100');
+      expect(await time.latestBlock().then(blockNumber => this.mock.quorum(blockNumber.subn(1))))
+        .to.be.bignumber.equal(tokenSupply.mul(newRatio).divn(100));
+    });
+    runGovernorWorkflow();
+  });
+
+  describe('update quorum over the maximum', function () {
+    beforeEach(async function () {
+      this.settings = {
+        proposal: [
+          [ this.mock.address ],
+          [ web3.utils.toWei('0') ],
+          [ this.mock.contract.methods.updateQuorumNumerator(new BN(101)).encodeABI() ],
+          '<proposal description>',
+        ],
+        tokenHolder: owner,
+        voters: [
+          { voter: voter1, weight: tokenSupply, support: Enums.VoteType.For },
+        ],
+        steps: {
+          execute: { error: 'GovernorVotesQuorumFraction: quorumNumerator over quorumDenominator' },
+        },
+      };
+    });
+    runGovernorWorkflow();
+  });
+});

+ 24 - 0
test/helpers/enums.js

@@ -0,0 +1,24 @@
+const { BN } = require('@openzeppelin/test-helpers');
+
+function Enum (...options) {
+  return Object.fromEntries(options.map((key, i) => [ key, new BN(i) ]));
+}
+
+module.exports = {
+  Enum,
+  ProposalState: Enum(
+    'Pending',
+    'Active',
+    'Canceled',
+    'Defeated',
+    'Succeeded',
+    'Queued',
+    'Expired',
+    'Executed',
+  ),
+  VoteType: Enum(
+    'Against',
+    'For',
+    'Abstain',
+  ),
+};

+ 55 - 0
test/utils/TimersBlockNumberImpl.test.js

@@ -0,0 +1,55 @@
+const { BN, time } = require('@openzeppelin/test-helpers');
+const { expect } = require('chai');
+
+const TimersBlockNumberImpl = artifacts.require('TimersBlockNumberImpl');
+
+contract('TimersBlockNumber', function (accounts) {
+  beforeEach(async function () {
+    this.instance = await TimersBlockNumberImpl.new();
+    this.now = await web3.eth.getBlock('latest').then(({ number }) => number);
+  });
+
+  it('unset', async function () {
+    expect(await this.instance.getDeadline()).to.be.bignumber.equal('0');
+    expect(await this.instance.isUnset()).to.be.equal(true);
+    expect(await this.instance.isStarted()).to.be.equal(false);
+    expect(await this.instance.isPending()).to.be.equal(false);
+    expect(await this.instance.isExpired()).to.be.equal(false);
+  });
+
+  it('pending', async function () {
+    await this.instance.setDeadline(this.now + 3);
+    expect(await this.instance.getDeadline()).to.be.bignumber.equal(new BN(this.now + 3));
+    expect(await this.instance.isUnset()).to.be.equal(false);
+    expect(await this.instance.isStarted()).to.be.equal(true);
+    expect(await this.instance.isPending()).to.be.equal(true);
+    expect(await this.instance.isExpired()).to.be.equal(false);
+  });
+
+  it('expired', async function () {
+    await this.instance.setDeadline(this.now - 3);
+    expect(await this.instance.getDeadline()).to.be.bignumber.equal(new BN(this.now - 3));
+    expect(await this.instance.isUnset()).to.be.equal(false);
+    expect(await this.instance.isStarted()).to.be.equal(true);
+    expect(await this.instance.isPending()).to.be.equal(false);
+    expect(await this.instance.isExpired()).to.be.equal(true);
+  });
+
+  it('reset', async function () {
+    await this.instance.reset();
+    expect(await this.instance.getDeadline()).to.be.bignumber.equal(new BN(0));
+    expect(await this.instance.isUnset()).to.be.equal(true);
+    expect(await this.instance.isStarted()).to.be.equal(false);
+    expect(await this.instance.isPending()).to.be.equal(false);
+    expect(await this.instance.isExpired()).to.be.equal(false);
+  });
+
+  it('fast forward', async function () {
+    await this.instance.setDeadline(this.now + 3);
+    expect(await this.instance.isPending()).to.be.equal(true);
+    expect(await this.instance.isExpired()).to.be.equal(false);
+    await time.advanceBlockTo(this.now + 3);
+    expect(await this.instance.isPending()).to.be.equal(false);
+    expect(await this.instance.isExpired()).to.be.equal(true);
+  });
+});

+ 55 - 0
test/utils/TimersTimestamp.test.js

@@ -0,0 +1,55 @@
+const { BN, time } = require('@openzeppelin/test-helpers');
+const { expect } = require('chai');
+
+const TimersTimestampImpl = artifacts.require('TimersTimestampImpl');
+
+contract('TimersTimestamp', function (accounts) {
+  beforeEach(async function () {
+    this.instance = await TimersTimestampImpl.new();
+    this.now = await web3.eth.getBlock('latest').then(({ timestamp }) => timestamp);
+  });
+
+  it('unset', async function () {
+    expect(await this.instance.getDeadline()).to.be.bignumber.equal('0');
+    expect(await this.instance.isUnset()).to.be.equal(true);
+    expect(await this.instance.isStarted()).to.be.equal(false);
+    expect(await this.instance.isPending()).to.be.equal(false);
+    expect(await this.instance.isExpired()).to.be.equal(false);
+  });
+
+  it('pending', async function () {
+    await this.instance.setDeadline(this.now + 100);
+    expect(await this.instance.getDeadline()).to.be.bignumber.equal(new BN(this.now + 100));
+    expect(await this.instance.isUnset()).to.be.equal(false);
+    expect(await this.instance.isStarted()).to.be.equal(true);
+    expect(await this.instance.isPending()).to.be.equal(true);
+    expect(await this.instance.isExpired()).to.be.equal(false);
+  });
+
+  it('expired', async function () {
+    await this.instance.setDeadline(this.now - 100);
+    expect(await this.instance.getDeadline()).to.be.bignumber.equal(new BN(this.now - 100));
+    expect(await this.instance.isUnset()).to.be.equal(false);
+    expect(await this.instance.isStarted()).to.be.equal(true);
+    expect(await this.instance.isPending()).to.be.equal(false);
+    expect(await this.instance.isExpired()).to.be.equal(true);
+  });
+
+  it('reset', async function () {
+    await this.instance.reset();
+    expect(await this.instance.getDeadline()).to.be.bignumber.equal(new BN(0));
+    expect(await this.instance.isUnset()).to.be.equal(true);
+    expect(await this.instance.isStarted()).to.be.equal(false);
+    expect(await this.instance.isPending()).to.be.equal(false);
+    expect(await this.instance.isExpired()).to.be.equal(false);
+  });
+
+  it('fast forward', async function () {
+    await this.instance.setDeadline(this.now + 100);
+    expect(await this.instance.isPending()).to.be.equal(true);
+    expect(await this.instance.isExpired()).to.be.equal(false);
+    await time.increaseTo(this.now + 100);
+    expect(await this.instance.isPending()).to.be.equal(false);
+    expect(await this.instance.isExpired()).to.be.equal(true);
+  });
+});

+ 24 - 0
test/utils/introspection/SupportsInterface.behavior.js

@@ -50,6 +50,30 @@ const INTERFACES = {
     'getRoleMember(bytes32,uint256)',
     'getRoleMemberCount(bytes32)',
   ],
+  Governor: [
+    'name()',
+    'version()',
+    'COUNTING_MODE()',
+    'hashProposal(address[],uint256[],bytes[],bytes32)',
+    'state(uint256)',
+    'proposalSnapshot(uint256)',
+    'proposalDeadline(uint256)',
+    'votingDelay()',
+    'votingPeriod()',
+    'quorum(uint256)',
+    'getVotes(address,uint256)',
+    'hasVoted(uint256,address)',
+    'propose(address[],uint256[],bytes[],string)',
+    'execute(address[],uint256[],bytes[],bytes32)',
+    'castVote(uint256,uint8)',
+    'castVoteWithReason(uint256,uint8,string)',
+    'castVoteBySig(uint256,uint8,uint8,bytes32,bytes32)',
+  ],
+  GovernorTimelock: [
+    'timelock()',
+    'proposalEta(uint256)',
+    'queue(address[],uint256[],bytes[],bytes32)',
+  ],
 };
 
 const INTERFACE_IDS = {};

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff