Procházet zdrojové kódy

Add timestamp based governor with EIP-6372 and EIP-5805 (#3934)

Co-authored-by: Francisco Giordano <fg@frang.io>
Co-authored-by: Ernesto García <ernestognw@gmail.com>
Co-authored-by: Francisco <frangio.1@gmail.com>
Hadrien Croubois před 2 roky
rodič
revize
790cc5b65a
42 změnil soubory, kde provedl 4021 přidání a 3149 odebrání
  1. 5 0
      .changeset/four-bats-sniff.md
  2. 5 0
      .changeset/ninety-hornets-kick.md
  3. 1 1
      .github/actions/storage-layout/action.yml
  4. 51 34
      contracts/governance/Governor.sol
  5. 40 22
      contracts/governance/IGovernor.sol
  6. 3 3
      contracts/governance/README.adoc
  7. 3 3
      contracts/governance/compatibility/GovernorCompatibilityBravo.sol
  8. 9 10
      contracts/governance/extensions/GovernorPreventLateQuorum.sol
  9. 8 9
      contracts/governance/extensions/GovernorTimelockCompound.sol
  10. 29 5
      contracts/governance/extensions/GovernorVotes.sol
  11. 27 3
      contracts/governance/extensions/GovernorVotesComp.sol
  12. 16 13
      contracts/governance/extensions/GovernorVotesQuorumFraction.sol
  13. 6 4
      contracts/governance/utils/IVotes.sol
  14. 61 20
      contracts/governance/utils/Votes.sol
  15. 9 0
      contracts/interfaces/IERC5805.sol
  16. 17 0
      contracts/interfaces/IERC6372.sol
  17. 11 0
      contracts/mocks/VotesMock.sol
  18. 262 0
      contracts/mocks/token/ERC20VotesLegacyMock.sol
  19. 40 0
      contracts/mocks/token/VotesTimestamp.sol
  20. 39 24
      contracts/token/ERC20/extensions/ERC20Votes.sol
  21. 50 0
      contracts/utils/Checkpoints.sol
  22. 0 1
      scripts/checks/compare-layout.js
  23. 13 3
      scripts/checks/compareGasReports.js
  24. 46 20
      scripts/generate/templates/Checkpoints.js
  25. 610 592
      test/governance/Governor.test.js
  26. 227 202
      test/governance/compatibility/GovernorCompatibilityBravo.test.js
  27. 61 54
      test/governance/extensions/GovernorComp.test.js
  28. 97 89
      test/governance/extensions/GovernorERC721.test.js
  29. 164 147
      test/governance/extensions/GovernorPreventLateQuorum.test.js
  30. 285 266
      test/governance/extensions/GovernorTimelockCompound.test.js
  31. 386 335
      test/governance/extensions/GovernorTimelockControl.test.js
  32. 131 119
      test/governance/extensions/GovernorVotesQuorumFraction.test.js
  33. 144 136
      test/governance/extensions/GovernorWithParams.test.js
  34. 23 0
      test/governance/utils/EIP6372.behavior.js
  35. 56 35
      test/governance/utils/Votes.behavior.js
  36. 54 41
      test/governance/utils/Votes.test.js
  37. 6 9
      test/helpers/governance.js
  38. 16 0
      test/helpers/time.js
  39. 508 490
      test/token/ERC20/extensions/ERC20Votes.test.js
  40. 474 457
      test/token/ERC20/extensions/ERC20VotesComp.test.js
  41. 1 0
      test/token/ERC721/extensions/ERC721Votes.test.js
  42. 27 2
      test/utils/Checkpoints.test.js

+ 5 - 0
.changeset/four-bats-sniff.md

@@ -0,0 +1,5 @@
+---
+'openzeppelin-solidity': minor
+---
+
+`Governor`: Enable timestamp operation for blockchains without a stable block time. This is achieved by connecting a Governor's internal clock to match a voting token's EIP-6372 interface.

+ 5 - 0
.changeset/ninety-hornets-kick.md

@@ -0,0 +1,5 @@
+---
+'openzeppelin-solidity': minor
+---
+
+`Votes`, `ERC20Votes`, `ERC721Votes`: support timestamp checkpointing using EIP-6372.

+ 1 - 1
.github/actions/storage-layout/action.yml

@@ -40,7 +40,7 @@ runs:
     - name: Compare layouts
       if: steps.reference.outcome == 'success' && github.event_name == 'pull_request'
       run: |
-        node scripts/checks/compare-layout.js --head ${{ inputs.layout }} --ref ${{ inputs.ref_layout }} >> $GITHUB_STEP_SUMMARY
+        node scripts/checks/compare-layout.js --head ${{ inputs.layout }} --ref ${{ inputs.ref_layout }}
       shell: bash
     - name: Rename artifacts for upload
       if: github.event_name != 'pull_request'

+ 51 - 34
contracts/governance/Governor.sol

@@ -12,7 +12,6 @@ import "../utils/math/SafeCast.sol";
 import "../utils/structs/DoubleEndedQueue.sol";
 import "../utils/Address.sol";
 import "../utils/Context.sol";
-import "../utils/Timers.sol";
 import "./IGovernor.sol";
 
 /**
@@ -29,22 +28,29 @@ import "./IGovernor.sol";
 abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receiver, IERC1155Receiver {
     using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque;
     using SafeCast for uint256;
-    using Timers for Timers.BlockNumber;
 
     bytes32 public constant BALLOT_TYPEHASH = keccak256("Ballot(uint256 proposalId,uint8 support)");
     bytes32 public constant EXTENDED_BALLOT_TYPEHASH =
         keccak256("ExtendedBallot(uint256 proposalId,uint8 support,string reason,bytes params)");
 
+    // solhint-disable var-name-mixedcase
     struct ProposalCore {
-        Timers.BlockNumber voteStart;
-        Timers.BlockNumber voteEnd;
+        // --- start retyped from Timers.BlockNumber at offset 0x00 ---
+        uint64 voteStart;
+        address proposer;
+        bytes4 __gap_unused0;
+        // --- start retyped from Timers.BlockNumber at offset 0x20 ---
+        uint64 voteEnd;
+        bytes24 __gap_unused1;
+        // --- Remaining fields starting at offset 0x40 ---------------
         bool executed;
         bool canceled;
-        address proposer;
     }
+    // solhint-enable var-name-mixedcase
 
     string private _name;
 
+    /// @custom:oz-retyped-from mapping(uint256 => Governor.ProposalCore)
     mapping(uint256 => ProposalCore) private _proposals;
 
     // This queue keeps track of the governor operating on itself. Calls to functions protected by the
@@ -96,12 +102,13 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive
         return
             interfaceId ==
             (type(IGovernor).interfaceId ^
+                type(IERC6372).interfaceId ^
                 this.cancel.selector ^
                 this.castVoteWithReasonAndParams.selector ^
                 this.castVoteWithReasonAndParamsBySig.selector ^
                 this.getVotesWithParams.selector) ||
-            interfaceId == (type(IGovernor).interfaceId ^ this.cancel.selector) ||
-            interfaceId == type(IGovernor).interfaceId ||
+            // Previous interface for backwards compatibility
+            interfaceId == (type(IGovernor).interfaceId ^ type(IERC6372).interfaceId ^ this.cancel.selector) ||
             interfaceId == type(IERC1155Receiver).interfaceId ||
             super.supportsInterface(interfaceId);
     }
@@ -162,13 +169,15 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive
             revert("Governor: unknown proposal id");
         }
 
-        if (snapshot >= block.number) {
+        uint256 currentTimepoint = clock();
+
+        if (snapshot >= currentTimepoint) {
             return ProposalState.Pending;
         }
 
         uint256 deadline = proposalDeadline(proposalId);
 
-        if (deadline >= block.number) {
+        if (deadline >= currentTimepoint) {
             return ProposalState.Active;
         }
 
@@ -179,25 +188,32 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive
         }
     }
 
+    /**
+     * @dev Part of the Governor Bravo's interface: _"The number of votes required in order for a voter to become a proposer"_.
+     */
+    function proposalThreshold() public view virtual returns (uint256) {
+        return 0;
+    }
+
     /**
      * @dev See {IGovernor-proposalSnapshot}.
      */
     function proposalSnapshot(uint256 proposalId) public view virtual override returns (uint256) {
-        return _proposals[proposalId].voteStart.getDeadline();
+        return _proposals[proposalId].voteStart;
     }
 
     /**
      * @dev See {IGovernor-proposalDeadline}.
      */
     function proposalDeadline(uint256 proposalId) public view virtual override returns (uint256) {
-        return _proposals[proposalId].voteEnd.getDeadline();
+        return _proposals[proposalId].voteEnd;
     }
 
     /**
-     * @dev Part of the Governor Bravo's interface: _"The number of votes required in order for a voter to become a proposer"_.
+     * @dev Address of the proposer
      */
-    function proposalThreshold() public view virtual returns (uint256) {
-        return 0;
+    function _proposalProposer(uint256 proposalId) internal view virtual returns (address) {
+        return _proposals[proposalId].proposer;
     }
 
     /**
@@ -211,13 +227,9 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive
     function _voteSucceeded(uint256 proposalId) internal view virtual returns (bool);
 
     /**
-     * @dev Get the voting weight of `account` at a specific `blockNumber`, for a vote as described by `params`.
+     * @dev Get the voting weight of `account` at a specific `timepoint`, for a vote as described by `params`.
      */
-    function _getVotes(
-        address account,
-        uint256 blockNumber,
-        bytes memory params
-    ) internal view virtual returns (uint256);
+    function _getVotes(address account, uint256 timepoint, bytes memory params) internal view virtual returns (uint256);
 
     /**
      * @dev Register a vote for `proposalId` by `account` with a given `support`, voting `weight` and voting `params`.
@@ -252,9 +264,10 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive
         string memory description
     ) public virtual override returns (uint256) {
         address proposer = _msgSender();
+        uint256 currentTimepoint = clock();
 
         require(
-            getVotes(proposer, block.number - 1) >= proposalThreshold(),
+            getVotes(proposer, currentTimepoint - 1) >= proposalThreshold(),
             "Governor: proposer votes below proposal threshold"
         );
 
@@ -263,16 +276,20 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive
         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");
+        require(_proposals[proposalId].proposer == address(0), "Governor: proposal already exists");
 
-        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();
+        uint256 snapshot = currentTimepoint + votingDelay();
+        uint256 deadline = snapshot + votingPeriod();
 
-        proposal.voteStart.setDeadline(snapshot);
-        proposal.voteEnd.setDeadline(deadline);
-        proposal.proposer = proposer;
+        _proposals[proposalId] = ProposalCore({
+            proposer: proposer,
+            voteStart: snapshot.toUint64(),
+            voteEnd: deadline.toUint64(),
+            executed: false,
+            canceled: false,
+            __gap_unused0: 0,
+            __gap_unused1: 0
+        });
 
         emit ProposalCreated(
             proposalId,
@@ -416,8 +433,8 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive
     /**
      * @dev See {IGovernor-getVotes}.
      */
-    function getVotes(address account, uint256 blockNumber) public view virtual override returns (uint256) {
-        return _getVotes(account, blockNumber, _defaultParams());
+    function getVotes(address account, uint256 timepoint) public view virtual override returns (uint256) {
+        return _getVotes(account, timepoint, _defaultParams());
     }
 
     /**
@@ -425,10 +442,10 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive
      */
     function getVotesWithParams(
         address account,
-        uint256 blockNumber,
+        uint256 timepoint,
         bytes memory params
     ) public view virtual override returns (uint256) {
-        return _getVotes(account, blockNumber, params);
+        return _getVotes(account, timepoint, params);
     }
 
     /**
@@ -546,7 +563,7 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive
         ProposalCore storage proposal = _proposals[proposalId];
         require(state(proposalId) == ProposalState.Active, "Governor: vote not currently active");
 
-        uint256 weight = _getVotes(account, proposal.voteStart.getDeadline(), params);
+        uint256 weight = _getVotes(account, proposal.voteStart, params);
         _countVote(proposalId, account, support, weight, params);
 
         if (params.length == 0) {

+ 40 - 22
contracts/governance/IGovernor.sol

@@ -3,14 +3,15 @@
 
 pragma solidity ^0.8.0;
 
-import "../utils/introspection/ERC165.sol";
+import "../interfaces/IERC165.sol";
+import "../interfaces/IERC6372.sol";
 
 /**
  * @dev Interface of the {Governor} core.
  *
  * _Available since v4.3._
  */
-abstract contract IGovernor is IERC165 {
+abstract contract IGovernor is IERC165, IERC6372 {
     enum ProposalState {
         Pending,
         Active,
@@ -32,8 +33,8 @@ abstract contract IGovernor is IERC165 {
         uint256[] values,
         string[] signatures,
         bytes[] calldatas,
-        uint256 startBlock,
-        uint256 endBlock,
+        uint256 voteStart,
+        uint256 voteEnd,
         string description
     );
 
@@ -81,6 +82,19 @@ abstract contract IGovernor is IERC165 {
      */
     function version() public view virtual returns (string memory);
 
+    /**
+     * @notice module:core
+     * @dev See {IERC6372}
+     */
+    function clock() public view virtual override returns (uint48);
+
+    /**
+     * @notice module:core
+     * @dev See EIP-6372.
+     */
+    // solhint-disable-next-line func-name-mixedcase
+    function CLOCK_MODE() public view virtual override 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
@@ -104,7 +118,7 @@ abstract contract IGovernor is IERC165 {
      * JavaScript class.
      */
     // solhint-disable-next-line func-name-mixedcase
-    function COUNTING_MODE() public pure virtual returns (string memory);
+    function COUNTING_MODE() public view virtual returns (string memory);
 
     /**
      * @notice module:core
@@ -125,29 +139,33 @@ abstract contract IGovernor is IERC165 {
 
     /**
      * @notice module:core
-     * @dev Block number used to retrieve user's votes and quorum. As per Compound's Comp and OpenZeppelin's
-     * ERC20Votes, the snapshot is performed at the end of this block. Hence, voting for this proposal starts at the
-     * beginning of the following block.
+     * @dev Timepoint used to retrieve user's votes and quorum. If using block number (as per Compound's Comp), the
+     * snapshot is performed at the end of this block. Hence, voting for this proposal starts at the beginning of the
+     * following block.
      */
     function proposalSnapshot(uint256 proposalId) public view virtual returns (uint256);
 
     /**
      * @notice module:core
-     * @dev Block number at which votes close. Votes close at the end of this block, so it is possible to cast a vote
-     * during this block.
+     * @dev Timepoint at which votes close. If using block number, votes close at the end of this block, so it is
+     * possible to cast a vote during this block.
      */
     function proposalDeadline(uint256 proposalId) public view virtual returns (uint256);
 
     /**
      * @notice module:user-config
-     * @dev Delay, in number of block, between the proposal is created and the vote starts. This can be increased to
-     * leave time for users to buy voting power, or delegate it, before the voting of a proposal starts.
+     * @dev Delay, between the proposal is created and the vote starts. The unit this duration is expressed in depends
+     * on the clock (see EIP-6372) this contract uses.
+     *
+     * This can be increased to leave time for users to buy voting power, or delegate it, before the voting of a
+     * proposal starts.
      */
     function votingDelay() public view virtual returns (uint256);
 
     /**
      * @notice module:user-config
-     * @dev Delay, in number of blocks, between the vote start and vote ends.
+     * @dev Delay, between the vote start and vote ends. The unit this duration is expressed in depends on the clock
+     * (see EIP-6372) this contract uses.
      *
      * NOTE: The {votingDelay} can delay the start of the vote. This must be considered when setting the voting
      * duration compared to the voting delay.
@@ -158,27 +176,27 @@ abstract contract IGovernor is IERC165 {
      * @notice module:user-config
      * @dev Minimum number of cast voted required for a proposal to be successful.
      *
-     * Note: The `blockNumber` parameter corresponds to the snapshot used for counting vote. This allows to scale the
-     * quorum depending on values such as the totalSupply of a token at this block (see {ERC20Votes}).
+     * NOTE: The `timepoint` parameter corresponds to the snapshot used for counting vote. This allows to scale the
+     * quorum depending on values such as the totalSupply of a token at this timepoint (see {ERC20Votes}).
      */
-    function quorum(uint256 blockNumber) public view virtual returns (uint256);
+    function quorum(uint256 timepoint) public view virtual returns (uint256);
 
     /**
      * @notice module:reputation
-     * @dev Voting power of an `account` at a specific `blockNumber`.
+     * @dev Voting power of an `account` at a specific `timepoint`.
      *
      * 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) public view virtual returns (uint256);
+    function getVotes(address account, uint256 timepoint) public view virtual returns (uint256);
 
     /**
      * @notice module:reputation
-     * @dev Voting power of an `account` at a specific `blockNumber` given additional encoded parameters.
+     * @dev Voting power of an `account` at a specific `timepoint` given additional encoded parameters.
      */
     function getVotesWithParams(
         address account,
-        uint256 blockNumber,
+        uint256 timepoint,
         bytes memory params
     ) public view virtual returns (uint256);
 
@@ -189,8 +207,8 @@ abstract contract IGovernor is IERC165 {
     function hasVoted(uint256 proposalId, address account) public view virtual 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.
+     * @dev Create a new proposal. Vote start after a delay specified by {IGovernor-votingDelay} and lasts for a
+     * duration specified by {IGovernor-votingPeriod}.
      *
      * Emits a {ProposalCreated} event.
      */

+ 3 - 3
contracts/governance/README.adoc

@@ -46,9 +46,9 @@ Other extensions can customize the behavior or interface in multiple ways.
 
 In addition to modules and extensions, the core contract requires a few virtual functions to be implemented to your particular specifications:
 
-* <<Governor-votingDelay-,`votingDelay()`>>: Delay (in number of blocks) since the proposal is submitted until voting power is fixed and voting starts. This can be used to enforce a delay after a proposal is published for users to buy tokens, or delegate their votes.
-* <<Governor-votingPeriod-,`votingPeriod()`>>: Delay (in number of blocks) since the proposal starts until voting 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`.
+* <<Governor-votingDelay-,`votingDelay()`>>: Delay (in EIP-6372 clock) since the proposal is submitted until voting power is fixed and voting starts. This can be used to enforce a delay after a proposal is published for users to buy tokens, or delegate their votes.
+* <<Governor-votingPeriod-,`votingPeriod()`>>: Delay (in EIP-6372 clock) since the proposal starts until voting ends.
+* <<Governor-quorum-uint256-,`quorum(uint256 timepoint)`>>: Quorum required for a proposal to be successful. This function includes a `timepoint` argument (see EIP-6372) so the quorum can adapt through time, for example, to follow a token's `totalSupply`.
 
 NOTE: Functions of the `Governor` contract do not include access control. If you want to restrict access, you should add these checks by overloading the particular functions. Among these, {Governor-_cancel} is internal by default, and you will have to expose it (with the right access control mechanism) yourself if this function is needed.
 

+ 3 - 3
contracts/governance/compatibility/GovernorCompatibilityBravo.sol

@@ -100,10 +100,10 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp
     }
 
     function cancel(uint256 proposalId) public virtual override(IGovernor, Governor) {
-        ProposalDetails storage details = _proposalDetails[proposalId];
+        address proposer = _proposalDetails[proposalId].proposer;
 
         require(
-            _msgSender() == details.proposer || getVotes(details.proposer, block.number - 1) < proposalThreshold(),
+            _msgSender() == proposer || getVotes(proposer, clock() - 1) < proposalThreshold(),
             "GovernorBravo: proposer above threshold"
         );
 
@@ -225,7 +225,7 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp
      * @dev See {IGovernorCompatibilityBravo-quorumVotes}.
      */
     function quorumVotes() public view virtual override returns (uint256) {
-        return quorum(block.number - 1);
+        return quorum(clock() - 1);
     }
 
     // ==================================================== Voting ====================================================

+ 9 - 10
contracts/governance/extensions/GovernorPreventLateQuorum.sol

@@ -19,10 +19,11 @@ import "../../utils/math/Math.sol";
  */
 abstract contract GovernorPreventLateQuorum is Governor {
     using SafeCast for uint256;
-    using Timers for Timers.BlockNumber;
 
     uint64 private _voteExtension;
-    mapping(uint256 => Timers.BlockNumber) private _extendedDeadlines;
+
+    /// @custom:oz-retyped-from mapping(uint256 => Timers.BlockNumber)
+    mapping(uint256 => uint64) private _extendedDeadlines;
 
     /// @dev Emitted when a proposal deadline is pushed back due to reaching quorum late in its voting period.
     event ProposalExtended(uint256 indexed proposalId, uint64 extendedDeadline);
@@ -44,7 +45,7 @@ abstract contract GovernorPreventLateQuorum is Governor {
      * proposal reached quorum late in the voting period. See {Governor-proposalDeadline}.
      */
     function proposalDeadline(uint256 proposalId) public view virtual override returns (uint256) {
-        return Math.max(super.proposalDeadline(proposalId), _extendedDeadlines[proposalId].getDeadline());
+        return Math.max(super.proposalDeadline(proposalId), _extendedDeadlines[proposalId]);
     }
 
     /**
@@ -62,16 +63,14 @@ abstract contract GovernorPreventLateQuorum is Governor {
     ) internal virtual override returns (uint256) {
         uint256 result = super._castVote(proposalId, account, support, reason, params);
 
-        Timers.BlockNumber storage extendedDeadline = _extendedDeadlines[proposalId];
-
-        if (extendedDeadline.isUnset() && _quorumReached(proposalId)) {
-            uint64 extendedDeadlineValue = block.number.toUint64() + lateQuorumVoteExtension();
+        if (_extendedDeadlines[proposalId] == 0 && _quorumReached(proposalId)) {
+            uint64 extendedDeadline = clock() + lateQuorumVoteExtension();
 
-            if (extendedDeadlineValue > proposalDeadline(proposalId)) {
-                emit ProposalExtended(proposalId, extendedDeadlineValue);
+            if (extendedDeadline > proposalDeadline(proposalId)) {
+                emit ProposalExtended(proposalId, extendedDeadline);
             }
 
-            extendedDeadline.setDeadline(extendedDeadlineValue);
+            _extendedDeadlines[proposalId] = extendedDeadline;
         }
 
         return result;

+ 8 - 9
contracts/governance/extensions/GovernorTimelockCompound.sol

@@ -22,15 +22,11 @@ import "../../vendor/compound/ICompoundTimelock.sol";
  */
 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;
+    /// @custom:oz-retyped-from mapping(uint256 => GovernorTimelockCompound.ProposalTimelock)
+    mapping(uint256 => uint64) private _proposalTimelocks;
 
     /**
      * @dev Emitted when the timelock controller used for proposal execution is modified.
@@ -82,7 +78,7 @@ abstract contract GovernorTimelockCompound is IGovernorTimelock, Governor {
      * @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();
+        return _proposalTimelocks[proposalId];
     }
 
     /**
@@ -99,7 +95,8 @@ abstract contract GovernorTimelockCompound is IGovernorTimelock, Governor {
         require(state(proposalId) == ProposalState.Succeeded, "Governor: proposal not successful");
 
         uint256 eta = block.timestamp + _timelock.delay();
-        _proposalTimelocks[proposalId].timer.setDeadline(eta.toUint64());
+        _proposalTimelocks[proposalId] = eta.toUint64();
+
         for (uint256 i = 0; i < targets.length; ++i) {
             require(
                 !_timelock.queuedTransactions(keccak256(abi.encode(targets[i], values[i], "", calldatas[i], eta))),
@@ -145,10 +142,12 @@ abstract contract GovernorTimelockCompound is IGovernorTimelock, Governor {
 
         uint256 eta = proposalEta(proposalId);
         if (eta > 0) {
+            // update state first
+            delete _proposalTimelocks[proposalId];
+            // do external call later
             for (uint256 i = 0; i < targets.length; ++i) {
                 _timelock.cancelTransaction(targets[i], values[i], "", calldatas[i], eta);
             }
-            _proposalTimelocks[proposalId].timer.reset();
         }
 
         return proposalId;

+ 29 - 5
contracts/governance/extensions/GovernorVotes.sol

@@ -4,7 +4,7 @@
 pragma solidity ^0.8.0;
 
 import "../Governor.sol";
-import "../utils/IVotes.sol";
+import "../../interfaces/IERC5805.sol";
 
 /**
  * @dev Extension of {Governor} for voting weight extraction from an {ERC20Votes} token, or since v4.5 an {ERC721Votes} token.
@@ -12,10 +12,34 @@ import "../utils/IVotes.sol";
  * _Available since v4.3._
  */
 abstract contract GovernorVotes is Governor {
-    IVotes public immutable token;
+    IERC5805 public immutable token;
 
     constructor(IVotes tokenAddress) {
-        token = tokenAddress;
+        token = IERC5805(address(tokenAddress));
+    }
+
+    /**
+     * @dev Clock (as specified in EIP-6372) is set to match the token's clock. Fallback to block numbers if the token
+     * does not implement EIP-6372.
+     */
+    function clock() public view virtual override returns (uint48) {
+        try token.clock() returns (uint48 timepoint) {
+            return timepoint;
+        } catch {
+            return SafeCast.toUint48(block.number);
+        }
+    }
+
+    /**
+     * @dev Machine-readable description of the clock as specified in EIP-6372.
+     */
+    // solhint-disable-next-line func-name-mixedcase
+    function CLOCK_MODE() public view virtual override returns (string memory) {
+        try token.CLOCK_MODE() returns (string memory clockmode) {
+            return clockmode;
+        } catch {
+            return "mode=blocknumber&from=default";
+        }
     }
 
     /**
@@ -23,9 +47,9 @@ abstract contract GovernorVotes is Governor {
      */
     function _getVotes(
         address account,
-        uint256 blockNumber,
+        uint256 timepoint,
         bytes memory /*params*/
     ) internal view virtual override returns (uint256) {
-        return token.getPastVotes(account, blockNumber);
+        return token.getPastVotes(account, timepoint);
     }
 }

+ 27 - 3
contracts/governance/extensions/GovernorVotesComp.sol

@@ -19,13 +19,37 @@ abstract contract GovernorVotesComp is Governor {
     }
 
     /**
-     * Read the voting weight from the token's built in snapshot mechanism (see {Governor-_getVotes}).
+     * @dev Clock (as specified in EIP-6372) is set to match the token's clock. Fallback to block numbers if the token
+     * does not implement EIP-6372.
+     */
+    function clock() public view virtual override returns (uint48) {
+        try token.clock() returns (uint48 timepoint) {
+            return timepoint;
+        } catch {
+            return SafeCast.toUint48(block.number);
+        }
+    }
+
+    /**
+     * @dev Machine-readable description of the clock as specified in EIP-6372.
+     */
+    // solhint-disable-next-line func-name-mixedcase
+    function CLOCK_MODE() public view virtual override returns (string memory) {
+        try token.CLOCK_MODE() returns (string memory clockmode) {
+            return clockmode;
+        } catch {
+            return "mode=blocknumber&from=default";
+        }
+    }
+
+    /**
+     * Read the voting weight from the token's built-in snapshot mechanism (see {Governor-_getVotes}).
      */
     function _getVotes(
         address account,
-        uint256 blockNumber,
+        uint256 timepoint,
         bytes memory /*params*/
     ) internal view virtual override returns (uint256) {
-        return token.getPriorVotes(account, blockNumber);
+        return token.getPriorVotes(account, timepoint);
     }
 }

+ 16 - 13
contracts/governance/extensions/GovernorVotesQuorumFraction.sol

@@ -14,10 +14,13 @@ import "../../utils/math/SafeCast.sol";
  * _Available since v4.3._
  */
 abstract contract GovernorVotesQuorumFraction is GovernorVotes {
-    using Checkpoints for Checkpoints.History;
+    using SafeCast for *;
+    using Checkpoints for Checkpoints.Trace224;
 
-    uint256 private _quorumNumerator; // DEPRECATED
-    Checkpoints.History private _quorumNumeratorHistory;
+    uint256 private _quorumNumerator; // DEPRECATED in favor of _quorumNumeratorHistory
+
+    /// @custom:oz-retyped-from Checkpoints.History
+    Checkpoints.Trace224 private _quorumNumeratorHistory;
 
     event QuorumNumeratorUpdated(uint256 oldQuorumNumerator, uint256 newQuorumNumerator);
 
@@ -40,9 +43,9 @@ abstract contract GovernorVotesQuorumFraction is GovernorVotes {
     }
 
     /**
-     * @dev Returns the quorum numerator at a specific block number. See {quorumDenominator}.
+     * @dev Returns the quorum numerator at a specific timepoint. See {quorumDenominator}.
      */
-    function quorumNumerator(uint256 blockNumber) public view virtual returns (uint256) {
+    function quorumNumerator(uint256 timepoint) public view virtual returns (uint256) {
         // If history is empty, fallback to old storage
         uint256 length = _quorumNumeratorHistory._checkpoints.length;
         if (length == 0) {
@@ -50,13 +53,13 @@ abstract contract GovernorVotesQuorumFraction is GovernorVotes {
         }
 
         // Optimistic search, check the latest checkpoint
-        Checkpoints.Checkpoint memory latest = _quorumNumeratorHistory._checkpoints[length - 1];
-        if (latest._blockNumber <= blockNumber) {
+        Checkpoints.Checkpoint224 memory latest = _quorumNumeratorHistory._checkpoints[length - 1];
+        if (latest._key <= timepoint) {
             return latest._value;
         }
 
         // Otherwise, do the binary search
-        return _quorumNumeratorHistory.getAtBlock(blockNumber);
+        return _quorumNumeratorHistory.upperLookupRecent(timepoint.toUint32());
     }
 
     /**
@@ -67,10 +70,10 @@ abstract contract GovernorVotesQuorumFraction is GovernorVotes {
     }
 
     /**
-     * @dev Returns the quorum for a block number, in terms of number of votes: `supply * numerator / denominator`.
+     * @dev Returns the quorum for a timepoint, in terms of number of votes: `supply * numerator / denominator`.
      */
-    function quorum(uint256 blockNumber) public view virtual override returns (uint256) {
-        return (token.getPastTotalSupply(blockNumber) * quorumNumerator(blockNumber)) / quorumDenominator();
+    function quorum(uint256 timepoint) public view virtual override returns (uint256) {
+        return (token.getPastTotalSupply(timepoint) * quorumNumerator(timepoint)) / quorumDenominator();
     }
 
     /**
@@ -107,12 +110,12 @@ abstract contract GovernorVotesQuorumFraction is GovernorVotes {
         // Make sure we keep track of the original numerator in contracts upgraded from a version without checkpoints.
         if (oldQuorumNumerator != 0 && _quorumNumeratorHistory._checkpoints.length == 0) {
             _quorumNumeratorHistory._checkpoints.push(
-                Checkpoints.Checkpoint({_blockNumber: 0, _value: SafeCast.toUint224(oldQuorumNumerator)})
+                Checkpoints.Checkpoint224({_key: 0, _value: oldQuorumNumerator.toUint224()})
             );
         }
 
         // Set new quorum for future proposals
-        _quorumNumeratorHistory.push(newQuorumNumerator);
+        _quorumNumeratorHistory.push(clock().toUint32(), newQuorumNumerator.toUint224());
 
         emit QuorumNumeratorUpdated(oldQuorumNumerator, newQuorumNumerator);
     }

+ 6 - 4
contracts/governance/utils/IVotes.sol

@@ -24,18 +24,20 @@ interface IVotes {
     function getVotes(address account) external view returns (uint256);
 
     /**
-     * @dev Returns the amount of votes that `account` had at the end of a past block (`blockNumber`).
+     * @dev Returns the amount of votes that `account` had at a specific moment in the past. If the `clock()` is
+     * configured to use block numbers, this will return the value the end of the corresponding block.
      */
-    function getPastVotes(address account, uint256 blockNumber) external view returns (uint256);
+    function getPastVotes(address account, uint256 timepoint) external view returns (uint256);
 
     /**
-     * @dev Returns the total supply of votes available at the end of a past block (`blockNumber`).
+     * @dev Returns the total supply of votes available at a specific moment in the past. If the `clock()` is
+     * configured to use block numbers, this will return the value the end of the corresponding block.
      *
      * NOTE: This value is the sum of all available votes, which is not necessarily the sum of all delegated votes.
      * Votes that have not been delegated are still part of total supply, even though they would not participate in a
      * vote.
      */
-    function getPastTotalSupply(uint256 blockNumber) external view returns (uint256);
+    function getPastTotalSupply(uint256 timepoint) external view returns (uint256);
 
     /**
      * @dev Returns the delegate that `account` has chosen.

+ 61 - 20
contracts/governance/utils/Votes.sol

@@ -2,11 +2,11 @@
 // OpenZeppelin Contracts (last updated v4.8.0) (governance/utils/Votes.sol)
 pragma solidity ^0.8.0;
 
+import "../../interfaces/IERC5805.sol";
 import "../../utils/Context.sol";
 import "../../utils/Counters.sol";
 import "../../utils/Checkpoints.sol";
 import "../../utils/cryptography/EIP712.sol";
-import "./IVotes.sol";
 
 /**
  * @dev This is a base abstract contract that tracks voting units, which are a measure of voting power that can be
@@ -28,19 +28,41 @@ import "./IVotes.sol";
  *
  * _Available since v4.5._
  */
-abstract contract Votes is IVotes, Context, EIP712 {
-    using Checkpoints for Checkpoints.History;
+abstract contract Votes is Context, EIP712, IERC5805 {
+    using Checkpoints for Checkpoints.Trace224;
     using Counters for Counters.Counter;
 
     bytes32 private constant _DELEGATION_TYPEHASH =
         keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)");
 
     mapping(address => address) private _delegation;
-    mapping(address => Checkpoints.History) private _delegateCheckpoints;
-    Checkpoints.History private _totalCheckpoints;
+
+    /// @custom:oz-retyped-from mapping(address => Checkpoints.History)
+    mapping(address => Checkpoints.Trace224) private _delegateCheckpoints;
+
+    /// @custom:oz-retyped-from Checkpoints.History
+    Checkpoints.Trace224 private _totalCheckpoints;
 
     mapping(address => Counters.Counter) private _nonces;
 
+    /**
+     * @dev Clock used for flagging checkpoints. Can be overridden to implement timestamp based
+     * checkpoints (and voting), in which case {CLOCK_MODE} should be overridden as well to match.
+     */
+    function clock() public view virtual override returns (uint48) {
+        return SafeCast.toUint48(block.number);
+    }
+
+    /**
+     * @dev Machine-readable description of the clock as specified in EIP-6372.
+     */
+    // solhint-disable-next-line func-name-mixedcase
+    function CLOCK_MODE() public view virtual override returns (string memory) {
+        // Check that the clock was not modified
+        require(clock() == block.number);
+        return "mode=blocknumber&from=default";
+    }
+
     /**
      * @dev Returns the current amount of votes that `account` has.
      */
@@ -49,18 +71,21 @@ abstract contract Votes is IVotes, Context, EIP712 {
     }
 
     /**
-     * @dev Returns the amount of votes that `account` had at the end of a past block (`blockNumber`).
+     * @dev Returns the amount of votes that `account` had at a specific moment in the past. If the `clock()` is
+     * configured to use block numbers, this will return the value the end of the corresponding block.
      *
      * Requirements:
      *
-     * - `blockNumber` must have been already mined
+     * - `timepoint` must be in the past. If operating using block numbers, the block must be already mined.
      */
-    function getPastVotes(address account, uint256 blockNumber) public view virtual override returns (uint256) {
-        return _delegateCheckpoints[account].getAtProbablyRecentBlock(blockNumber);
+    function getPastVotes(address account, uint256 timepoint) public view virtual override returns (uint256) {
+        require(timepoint < clock(), "Votes: future lookup");
+        return _delegateCheckpoints[account].upperLookupRecent(SafeCast.toUint32(timepoint));
     }
 
     /**
-     * @dev Returns the total supply of votes available at the end of a past block (`blockNumber`).
+     * @dev Returns the total supply of votes available at a specific moment in the past. If the `clock()` is
+     * configured to use block numbers, this will return the value the end of the corresponding block.
      *
      * NOTE: This value is the sum of all available votes, which is not necessarily the sum of all delegated votes.
      * Votes that have not been delegated are still part of total supply, even though they would not participate in a
@@ -68,11 +93,11 @@ abstract contract Votes is IVotes, Context, EIP712 {
      *
      * Requirements:
      *
-     * - `blockNumber` must have been already mined
+     * - `timepoint` must be in the past. If operating using block numbers, the block must be already mined.
      */
-    function getPastTotalSupply(uint256 blockNumber) public view virtual override returns (uint256) {
-        require(blockNumber < block.number, "Votes: block not yet mined");
-        return _totalCheckpoints.getAtProbablyRecentBlock(blockNumber);
+    function getPastTotalSupply(uint256 timepoint) public view virtual override returns (uint256) {
+        require(timepoint < clock(), "Votes: future lookup");
+        return _totalCheckpoints.upperLookupRecent(SafeCast.toUint32(timepoint));
     }
 
     /**
@@ -138,10 +163,10 @@ abstract contract Votes is IVotes, Context, EIP712 {
      */
     function _transferVotingUnits(address from, address to, uint256 amount) internal virtual {
         if (from == address(0)) {
-            _totalCheckpoints.push(_add, amount);
+            _push(_totalCheckpoints, _add, SafeCast.toUint224(amount));
         }
         if (to == address(0)) {
-            _totalCheckpoints.push(_subtract, amount);
+            _push(_totalCheckpoints, _subtract, SafeCast.toUint224(amount));
         }
         _moveDelegateVotes(delegates(from), delegates(to), amount);
     }
@@ -152,21 +177,37 @@ abstract contract Votes is IVotes, Context, EIP712 {
     function _moveDelegateVotes(address from, address to, uint256 amount) private {
         if (from != to && amount > 0) {
             if (from != address(0)) {
-                (uint256 oldValue, uint256 newValue) = _delegateCheckpoints[from].push(_subtract, amount);
+                (uint256 oldValue, uint256 newValue) = _push(
+                    _delegateCheckpoints[from],
+                    _subtract,
+                    SafeCast.toUint224(amount)
+                );
                 emit DelegateVotesChanged(from, oldValue, newValue);
             }
             if (to != address(0)) {
-                (uint256 oldValue, uint256 newValue) = _delegateCheckpoints[to].push(_add, amount);
+                (uint256 oldValue, uint256 newValue) = _push(
+                    _delegateCheckpoints[to],
+                    _add,
+                    SafeCast.toUint224(amount)
+                );
                 emit DelegateVotesChanged(to, oldValue, newValue);
             }
         }
     }
 
-    function _add(uint256 a, uint256 b) private pure returns (uint256) {
+    function _push(
+        Checkpoints.Trace224 storage store,
+        function(uint224, uint224) view returns (uint224) op,
+        uint224 delta
+    ) private returns (uint224, uint224) {
+        return store.push(SafeCast.toUint32(clock()), op(store.latest(), delta));
+    }
+
+    function _add(uint224 a, uint224 b) private pure returns (uint224) {
         return a + b;
     }
 
-    function _subtract(uint256 a, uint256 b) private pure returns (uint256) {
+    function _subtract(uint224 a, uint224 b) private pure returns (uint224) {
         return a - b;
     }
 

+ 9 - 0
contracts/interfaces/IERC5805.sol

@@ -0,0 +1,9 @@
+// SPDX-License-Identifier: MIT
+// OpenZeppelin Contracts (interfaces/IERC5805.sol)
+
+pragma solidity ^0.8.0;
+
+import "../governance/utils/IVotes.sol";
+import "./IERC6372.sol";
+
+interface IERC5805 is IERC6372, IVotes {}

+ 17 - 0
contracts/interfaces/IERC6372.sol

@@ -0,0 +1,17 @@
+// SPDX-License-Identifier: MIT
+// OpenZeppelin Contracts (interfaces/IERC6372.sol)
+
+pragma solidity ^0.8.0;
+
+interface IERC6372 {
+    /**
+     * @dev Clock used for flagging checkpoints. Can be overridden to implement timestamp based checkpoints (and voting).
+     */
+    function clock() external view returns (uint48);
+
+    /**
+     * @dev Description of the clock
+     */
+    // solhint-disable-next-line func-name-mixedcase
+    function CLOCK_MODE() external view returns (string memory);
+}

+ 11 - 0
contracts/mocks/VotesMock.sol

@@ -32,3 +32,14 @@ abstract contract VotesMock is Votes {
         _transferVotingUnits(owner, address(0), 1);
     }
 }
+
+abstract contract VotesTimestampMock is VotesMock {
+    function clock() public view override returns (uint48) {
+        return uint48(block.timestamp);
+    }
+
+    // solhint-disable-next-line func-name-mixedcase
+    function CLOCK_MODE() public view virtual override returns (string memory) {
+        return "mode=timestamp";
+    }
+}

+ 262 - 0
contracts/mocks/token/ERC20VotesLegacyMock.sol

@@ -0,0 +1,262 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.0;
+
+import "../../token/ERC20/extensions/ERC20Permit.sol";
+import "../../utils/math/Math.sol";
+import "../../governance/utils/IVotes.sol";
+import "../../utils/math/SafeCast.sol";
+import "../../utils/cryptography/ECDSA.sol";
+
+/**
+ * @dev Copied from the master branch at commit 86de1e8b6c3fa6b4efa4a5435869d2521be0f5f5
+ */
+abstract contract ERC20VotesLegacyMock is IVotes, ERC20Permit {
+    struct Checkpoint {
+        uint32 fromBlock;
+        uint224 votes;
+    }
+
+    bytes32 private constant _DELEGATION_TYPEHASH =
+        keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)");
+
+    mapping(address => address) private _delegates;
+    mapping(address => Checkpoint[]) private _checkpoints;
+    Checkpoint[] private _totalSupplyCheckpoints;
+
+    /**
+     * @dev Get the `pos`-th checkpoint for `account`.
+     */
+    function checkpoints(address account, uint32 pos) public view virtual returns (Checkpoint memory) {
+        return _checkpoints[account][pos];
+    }
+
+    /**
+     * @dev Get number of checkpoints for `account`.
+     */
+    function numCheckpoints(address account) public view virtual returns (uint32) {
+        return SafeCast.toUint32(_checkpoints[account].length);
+    }
+
+    /**
+     * @dev Get the address `account` is currently delegating to.
+     */
+    function delegates(address account) public view virtual override returns (address) {
+        return _delegates[account];
+    }
+
+    /**
+     * @dev Gets the current votes balance for `account`
+     */
+    function getVotes(address account) public view virtual override returns (uint256) {
+        uint256 pos = _checkpoints[account].length;
+        unchecked {
+            return pos == 0 ? 0 : _checkpoints[account][pos - 1].votes;
+        }
+    }
+
+    /**
+     * @dev Retrieve the number of votes for `account` at the end of `blockNumber`.
+     *
+     * Requirements:
+     *
+     * - `blockNumber` must have been already mined
+     */
+    function getPastVotes(address account, uint256 blockNumber) public view virtual override returns (uint256) {
+        require(blockNumber < block.number, "ERC20Votes: block not yet mined");
+        return _checkpointsLookup(_checkpoints[account], blockNumber);
+    }
+
+    /**
+     * @dev Retrieve the `totalSupply` at the end of `blockNumber`. Note, this value is the sum of all balances.
+     * It is NOT the sum of all the delegated votes!
+     *
+     * Requirements:
+     *
+     * - `blockNumber` must have been already mined
+     */
+    function getPastTotalSupply(uint256 blockNumber) public view virtual override returns (uint256) {
+        require(blockNumber < block.number, "ERC20Votes: block not yet mined");
+        return _checkpointsLookup(_totalSupplyCheckpoints, blockNumber);
+    }
+
+    /**
+     * @dev Lookup a value in a list of (sorted) checkpoints.
+     */
+    function _checkpointsLookup(Checkpoint[] storage ckpts, uint256 blockNumber) private view returns (uint256) {
+        // We run a binary search to look for the earliest checkpoint taken after `blockNumber`.
+        //
+        // Initially we check if the block is recent to narrow the search range.
+        // During the loop, the index of the wanted checkpoint remains in the range [low-1, high).
+        // With each iteration, either `low` or `high` is moved towards the middle of the range to maintain the invariant.
+        // - If the middle checkpoint is after `blockNumber`, we look in [low, mid)
+        // - If the middle checkpoint is before or equal to `blockNumber`, we look in [mid+1, high)
+        // Once we reach a single value (when low == high), we've found the right checkpoint at the index high-1, if not
+        // out of bounds (in which case we're looking too far in the past and the result is 0).
+        // Note that if the latest checkpoint available is exactly for `blockNumber`, we end up with an index that is
+        // past the end of the array, so we technically don't find a checkpoint after `blockNumber`, but it works out
+        // the same.
+        uint256 length = ckpts.length;
+
+        uint256 low = 0;
+        uint256 high = length;
+
+        if (length > 5) {
+            uint256 mid = length - Math.sqrt(length);
+            if (_unsafeAccess(ckpts, mid).fromBlock > blockNumber) {
+                high = mid;
+            } else {
+                low = mid + 1;
+            }
+        }
+
+        while (low < high) {
+            uint256 mid = Math.average(low, high);
+            if (_unsafeAccess(ckpts, mid).fromBlock > blockNumber) {
+                high = mid;
+            } else {
+                low = mid + 1;
+            }
+        }
+
+        unchecked {
+            return high == 0 ? 0 : _unsafeAccess(ckpts, high - 1).votes;
+        }
+    }
+
+    /**
+     * @dev Delegate votes from the sender to `delegatee`.
+     */
+    function delegate(address delegatee) public virtual override {
+        _delegate(_msgSender(), delegatee);
+    }
+
+    /**
+     * @dev Delegates votes from signer to `delegatee`
+     */
+    function delegateBySig(
+        address delegatee,
+        uint256 nonce,
+        uint256 expiry,
+        uint8 v,
+        bytes32 r,
+        bytes32 s
+    ) public virtual override {
+        require(block.timestamp <= expiry, "ERC20Votes: signature expired");
+        address signer = ECDSA.recover(
+            _hashTypedDataV4(keccak256(abi.encode(_DELEGATION_TYPEHASH, delegatee, nonce, expiry))),
+            v,
+            r,
+            s
+        );
+        require(nonce == _useNonce(signer), "ERC20Votes: invalid nonce");
+        _delegate(signer, delegatee);
+    }
+
+    /**
+     * @dev Maximum token supply. Defaults to `type(uint224).max` (2^224^ - 1).
+     */
+    function _maxSupply() internal view virtual returns (uint224) {
+        return type(uint224).max;
+    }
+
+    /**
+     * @dev Snapshots the totalSupply after it has been increased.
+     */
+    function _mint(address account, uint256 amount) internal virtual override {
+        super._mint(account, amount);
+        require(totalSupply() <= _maxSupply(), "ERC20Votes: total supply risks overflowing votes");
+
+        _writeCheckpoint(_totalSupplyCheckpoints, _add, amount);
+    }
+
+    /**
+     * @dev Snapshots the totalSupply after it has been decreased.
+     */
+    function _burn(address account, uint256 amount) internal virtual override {
+        super._burn(account, amount);
+
+        _writeCheckpoint(_totalSupplyCheckpoints, _subtract, amount);
+    }
+
+    /**
+     * @dev Move voting power when tokens are transferred.
+     *
+     * Emits a {IVotes-DelegateVotesChanged} event.
+     */
+    function _afterTokenTransfer(address from, address to, uint256 amount) internal virtual override {
+        super._afterTokenTransfer(from, to, amount);
+
+        _moveVotingPower(delegates(from), delegates(to), amount);
+    }
+
+    /**
+     * @dev Change delegation for `delegator` to `delegatee`.
+     *
+     * Emits events {IVotes-DelegateChanged} and {IVotes-DelegateVotesChanged}.
+     */
+    function _delegate(address delegator, address delegatee) internal virtual {
+        address currentDelegate = delegates(delegator);
+        uint256 delegatorBalance = balanceOf(delegator);
+        _delegates[delegator] = delegatee;
+
+        emit DelegateChanged(delegator, currentDelegate, delegatee);
+
+        _moveVotingPower(currentDelegate, delegatee, delegatorBalance);
+    }
+
+    function _moveVotingPower(address src, address dst, uint256 amount) private {
+        if (src != dst && amount > 0) {
+            if (src != address(0)) {
+                (uint256 oldWeight, uint256 newWeight) = _writeCheckpoint(_checkpoints[src], _subtract, amount);
+                emit DelegateVotesChanged(src, oldWeight, newWeight);
+            }
+
+            if (dst != address(0)) {
+                (uint256 oldWeight, uint256 newWeight) = _writeCheckpoint(_checkpoints[dst], _add, amount);
+                emit DelegateVotesChanged(dst, oldWeight, newWeight);
+            }
+        }
+    }
+
+    function _writeCheckpoint(
+        Checkpoint[] storage ckpts,
+        function(uint256, uint256) view returns (uint256) op,
+        uint256 delta
+    ) private returns (uint256 oldWeight, uint256 newWeight) {
+        uint256 pos = ckpts.length;
+
+        unchecked {
+            Checkpoint memory oldCkpt = pos == 0 ? Checkpoint(0, 0) : _unsafeAccess(ckpts, pos - 1);
+
+            oldWeight = oldCkpt.votes;
+            newWeight = op(oldWeight, delta);
+
+            if (pos > 0 && oldCkpt.fromBlock == block.number) {
+                _unsafeAccess(ckpts, pos - 1).votes = SafeCast.toUint224(newWeight);
+            } else {
+                ckpts.push(
+                    Checkpoint({fromBlock: SafeCast.toUint32(block.number), votes: SafeCast.toUint224(newWeight)})
+                );
+            }
+        }
+    }
+
+    function _add(uint256 a, uint256 b) private pure returns (uint256) {
+        return a + b;
+    }
+
+    function _subtract(uint256 a, uint256 b) private pure returns (uint256) {
+        return a - b;
+    }
+
+    /**
+     * @dev Access an element of the array without performing bounds check. The position is assumed to be within bounds.
+     */
+    function _unsafeAccess(Checkpoint[] storage ckpts, uint256 pos) private pure returns (Checkpoint storage result) {
+        assembly {
+            mstore(0, ckpts.slot)
+            result.slot := add(keccak256(0, 0x20), pos)
+        }
+    }
+}

+ 40 - 0
contracts/mocks/token/VotesTimestamp.sol

@@ -0,0 +1,40 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.0;
+
+import "../../token/ERC20/extensions/ERC20Votes.sol";
+import "../../token/ERC20/extensions/ERC20VotesComp.sol";
+import "../../token/ERC721/extensions/ERC721Votes.sol";
+
+abstract contract ERC20VotesTimestampMock is ERC20Votes {
+    function clock() public view virtual override returns (uint48) {
+        return SafeCast.toUint48(block.timestamp);
+    }
+
+    // solhint-disable-next-line func-name-mixedcase
+    function CLOCK_MODE() public view virtual override returns (string memory) {
+        return "mode=timestamp";
+    }
+}
+
+abstract contract ERC20VotesCompTimestampMock is ERC20VotesComp {
+    function clock() public view virtual override returns (uint48) {
+        return SafeCast.toUint48(block.timestamp);
+    }
+
+    // solhint-disable-next-line func-name-mixedcase
+    function CLOCK_MODE() public view virtual override returns (string memory) {
+        return "mode=timestamp";
+    }
+}
+
+abstract contract ERC721VotesTimestampMock is ERC721Votes {
+    function clock() public view virtual override returns (uint48) {
+        return SafeCast.toUint48(block.timestamp);
+    }
+
+    // solhint-disable-next-line func-name-mixedcase
+    function CLOCK_MODE() public view virtual override returns (string memory) {
+        return "mode=timestamp";
+    }
+}

+ 39 - 24
contracts/token/ERC20/extensions/ERC20Votes.sol

@@ -4,8 +4,8 @@
 pragma solidity ^0.8.0;
 
 import "./ERC20Permit.sol";
+import "../../../interfaces/IERC5805.sol";
 import "../../../utils/math/Math.sol";
-import "../../../governance/utils/IVotes.sol";
 import "../../../utils/math/SafeCast.sol";
 import "../../../utils/cryptography/ECDSA.sol";
 
@@ -24,7 +24,7 @@ import "../../../utils/cryptography/ECDSA.sol";
  *
  * _Available since v4.2._
  */
-abstract contract ERC20Votes is IVotes, ERC20Permit {
+abstract contract ERC20Votes is ERC20Permit, IERC5805 {
     struct Checkpoint {
         uint32 fromBlock;
         uint224 votes;
@@ -37,6 +37,23 @@ abstract contract ERC20Votes is IVotes, ERC20Permit {
     mapping(address => Checkpoint[]) private _checkpoints;
     Checkpoint[] private _totalSupplyCheckpoints;
 
+    /**
+     * @dev Clock used for flagging checkpoints. Can be overridden to implement timestamp based checkpoints (and voting).
+     */
+    function clock() public view virtual override returns (uint48) {
+        return SafeCast.toUint48(block.number);
+    }
+
+    /**
+     * @dev Description of the clock
+     */
+    // solhint-disable-next-line func-name-mixedcase
+    function CLOCK_MODE() public view virtual override returns (string memory) {
+        // Check that the clock was not modified
+        require(clock() == block.number);
+        return "mode=blocknumber&from=default";
+    }
+
     /**
      * @dev Get the `pos`-th checkpoint for `account`.
      */
@@ -69,45 +86,45 @@ abstract contract ERC20Votes is IVotes, ERC20Permit {
     }
 
     /**
-     * @dev Retrieve the number of votes for `account` at the end of `blockNumber`.
+     * @dev Retrieve the number of votes for `account` at the end of `timepoint`.
      *
      * Requirements:
      *
-     * - `blockNumber` must have been already mined
+     * - `timepoint` must be in the past
      */
-    function getPastVotes(address account, uint256 blockNumber) public view virtual override returns (uint256) {
-        require(blockNumber < block.number, "ERC20Votes: block not yet mined");
-        return _checkpointsLookup(_checkpoints[account], blockNumber);
+    function getPastVotes(address account, uint256 timepoint) public view virtual override returns (uint256) {
+        require(timepoint < clock(), "ERC20Votes: future lookup");
+        return _checkpointsLookup(_checkpoints[account], timepoint);
     }
 
     /**
-     * @dev Retrieve the `totalSupply` at the end of `blockNumber`. Note, this value is the sum of all balances.
+     * @dev Retrieve the `totalSupply` at the end of `timepoint`. Note, this value is the sum of all balances.
      * It is NOT the sum of all the delegated votes!
      *
      * Requirements:
      *
-     * - `blockNumber` must have been already mined
+     * - `timepoint` must be in the past
      */
-    function getPastTotalSupply(uint256 blockNumber) public view virtual override returns (uint256) {
-        require(blockNumber < block.number, "ERC20Votes: block not yet mined");
-        return _checkpointsLookup(_totalSupplyCheckpoints, blockNumber);
+    function getPastTotalSupply(uint256 timepoint) public view virtual override returns (uint256) {
+        require(timepoint < clock(), "ERC20Votes: future lookup");
+        return _checkpointsLookup(_totalSupplyCheckpoints, timepoint);
     }
 
     /**
      * @dev Lookup a value in a list of (sorted) checkpoints.
      */
-    function _checkpointsLookup(Checkpoint[] storage ckpts, uint256 blockNumber) private view returns (uint256) {
-        // We run a binary search to look for the earliest checkpoint taken after `blockNumber`.
+    function _checkpointsLookup(Checkpoint[] storage ckpts, uint256 timepoint) private view returns (uint256) {
+        // We run a binary search to look for the earliest checkpoint taken after `timepoint`.
         //
         // Initially we check if the block is recent to narrow the search range.
         // During the loop, the index of the wanted checkpoint remains in the range [low-1, high).
         // With each iteration, either `low` or `high` is moved towards the middle of the range to maintain the invariant.
-        // - If the middle checkpoint is after `blockNumber`, we look in [low, mid)
-        // - If the middle checkpoint is before or equal to `blockNumber`, we look in [mid+1, high)
+        // - If the middle checkpoint is after `timepoint`, we look in [low, mid)
+        // - If the middle checkpoint is before or equal to `timepoint`, we look in [mid+1, high)
         // Once we reach a single value (when low == high), we've found the right checkpoint at the index high-1, if not
         // out of bounds (in which case we're looking too far in the past and the result is 0).
-        // Note that if the latest checkpoint available is exactly for `blockNumber`, we end up with an index that is
-        // past the end of the array, so we technically don't find a checkpoint after `blockNumber`, but it works out
+        // Note that if the latest checkpoint available is exactly for `timepoint`, we end up with an index that is
+        // past the end of the array, so we technically don't find a checkpoint after `timepoint`, but it works out
         // the same.
         uint256 length = ckpts.length;
 
@@ -116,7 +133,7 @@ abstract contract ERC20Votes is IVotes, ERC20Permit {
 
         if (length > 5) {
             uint256 mid = length - Math.sqrt(length);
-            if (_unsafeAccess(ckpts, mid).fromBlock > blockNumber) {
+            if (_unsafeAccess(ckpts, mid).fromBlock > timepoint) {
                 high = mid;
             } else {
                 low = mid + 1;
@@ -125,7 +142,7 @@ abstract contract ERC20Votes is IVotes, ERC20Permit {
 
         while (low < high) {
             uint256 mid = Math.average(low, high);
-            if (_unsafeAccess(ckpts, mid).fromBlock > blockNumber) {
+            if (_unsafeAccess(ckpts, mid).fromBlock > timepoint) {
                 high = mid;
             } else {
                 low = mid + 1;
@@ -245,12 +262,10 @@ abstract contract ERC20Votes is IVotes, ERC20Permit {
             oldWeight = oldCkpt.votes;
             newWeight = op(oldWeight, delta);
 
-            if (pos > 0 && oldCkpt.fromBlock == block.number) {
+            if (pos > 0 && oldCkpt.fromBlock == clock()) {
                 _unsafeAccess(ckpts, pos - 1).votes = SafeCast.toUint224(newWeight);
             } else {
-                ckpts.push(
-                    Checkpoint({fromBlock: SafeCast.toUint32(block.number), votes: SafeCast.toUint224(newWeight)})
-                );
+                ckpts.push(Checkpoint({fromBlock: SafeCast.toUint32(clock()), votes: SafeCast.toUint224(newWeight)}));
             }
         }
     }

+ 50 - 0
contracts/utils/Checkpoints.sol

@@ -242,6 +242,31 @@ library Checkpoints {
         return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value;
     }
 
+    /**
+     * @dev Returns the value in the most recent checkpoint with key lower or equal than the search key.
+     *
+     * NOTE: This is a variant of {upperLookup} that is optimised to find "recent" checkpoint (checkpoints with high keys).
+     */
+    function upperLookupRecent(Trace224 storage self, uint32 key) internal view returns (uint224) {
+        uint256 len = self._checkpoints.length;
+
+        uint256 low = 0;
+        uint256 high = len;
+
+        if (len > 5) {
+            uint256 mid = len - Math.sqrt(len);
+            if (key < _unsafeAccess(self._checkpoints, mid)._key) {
+                high = mid;
+            } else {
+                low = mid + 1;
+            }
+        }
+
+        uint256 pos = _upperBinaryLookup(self._checkpoints, key, low, high);
+
+        return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value;
+    }
+
     /**
      * @dev Returns the value in the most recent checkpoint, or zero if there are no checkpoints.
      */
@@ -393,6 +418,31 @@ library Checkpoints {
         return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value;
     }
 
+    /**
+     * @dev Returns the value in the most recent checkpoint with key lower or equal than the search key.
+     *
+     * NOTE: This is a variant of {upperLookup} that is optimised to find "recent" checkpoint (checkpoints with high keys).
+     */
+    function upperLookupRecent(Trace160 storage self, uint96 key) internal view returns (uint160) {
+        uint256 len = self._checkpoints.length;
+
+        uint256 low = 0;
+        uint256 high = len;
+
+        if (len > 5) {
+            uint256 mid = len - Math.sqrt(len);
+            if (key < _unsafeAccess(self._checkpoints, mid)._key) {
+                high = mid;
+            } else {
+                low = mid + 1;
+            }
+        }
+
+        uint256 pos = _upperBinaryLookup(self._checkpoints, key, low, high);
+
+        return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value;
+    }
+
     /**
      * @dev Returns the value in the most recent checkpoint, or zero if there are no checkpoints.
      */

+ 0 - 1
scripts/checks/compare-layout.js

@@ -10,7 +10,6 @@ for (const name in oldLayout) {
   if (name in newLayout) {
     const report = getStorageUpgradeReport(oldLayout[name], newLayout[name], {});
     if (!report.ok) {
-      console.log(`ERROR: Storage incompatibility in ${name}`);
       console.log(report.explain());
       process.exitCode = 1;
     }

+ 13 - 3
scripts/checks/compareGasReports.js

@@ -10,6 +10,14 @@ const { argv } = require('yargs')
       choices: ['shell', 'markdown'],
       default: 'shell',
     },
+    hideEqual: {
+      type: 'boolean',
+      default: true,
+    },
+    strictTesting: {
+      type: 'boolean',
+      default: false,
+    },
   });
 
 // Deduce base tx cost from the percentage denominator
@@ -40,7 +48,7 @@ class Report {
   }
 
   // Compare two reports
-  static compare(update, ref, opts = { hideEqual: true }) {
+  static compare(update, ref, opts = { hideEqual: true, strictTesting: false }) {
     if (JSON.stringify(update.config.metadata) !== JSON.stringify(ref.config.metadata)) {
       throw new Error('Reports produced with non matching metadata');
     }
@@ -70,7 +78,9 @@ class Report {
     const methods = Object.keys(update.info.methods)
       .filter(key => ref.info.methods[key])
       .filter(key => update.info.methods[key].numberOfCalls > 0)
-      .filter(key => update.info.methods[key].numberOfCalls === ref.info.methods[key].numberOfCalls)
+      .filter(
+        key => !opts.strictTesting || update.info.methods[key].numberOfCalls === ref.info.methods[key].numberOfCalls,
+      )
       .map(key => ({
         contract: ref.info.methods[key].contract,
         method: ref.info.methods[key].fnSig,
@@ -220,7 +230,7 @@ function formatCmpMarkdown(rows) {
 }
 
 // MAIN
-const report = Report.compare(Report.load(argv._[0]), Report.load(argv._[1]));
+const report = Report.compare(Report.load(argv._[0]), Report.load(argv._[1]), argv);
 
 switch (argv.style) {
   case 'markdown':

+ 46 - 20
scripts/generate/templates/Checkpoints.js

@@ -1,7 +1,28 @@
 const format = require('../format-lines');
 
+// OPTIONS
+const defaultOpts = size => ({
+  historyTypeName: `Trace${size}`,
+  checkpointTypeName: `Checkpoint${size}`,
+  checkpointFieldName: '_checkpoints',
+  keyTypeName: `uint${256 - size}`,
+  keyFieldName: '_key',
+  valueTypeName: `uint${size}`,
+  valueFieldName: '_value',
+});
+
 const VALUE_SIZES = [224, 160];
 
+const OPTS = VALUE_SIZES.map(size => defaultOpts(size));
+
+const LEGACY_OPTS = {
+  ...defaultOpts(224),
+  historyTypeName: 'History',
+  checkpointTypeName: 'Checkpoint',
+  keyFieldName: '_blockNumber',
+};
+
+// TEMPLATE
 const header = `\
 pragma solidity ^0.8.0;
 
@@ -62,6 +83,31 @@ function upperLookup(${opts.historyTypeName} storage self, ${opts.keyTypeName} k
     uint256 pos = _upperBinaryLookup(self.${opts.checkpointFieldName}, key, 0, len);
     return pos == 0 ? 0 : _unsafeAccess(self.${opts.checkpointFieldName}, pos - 1).${opts.valueFieldName};
 }
+
+/**
+ * @dev Returns the value in the most recent checkpoint with key lower or equal than the search key.
+ *
+ * NOTE: This is a variant of {upperLookup} that is optimised to find "recent" checkpoint (checkpoints with high keys).
+ */
+function upperLookupRecent(${opts.historyTypeName} storage self, ${opts.keyTypeName} key) internal view returns (${opts.valueTypeName}) {
+    uint256 len = self.${opts.checkpointFieldName}.length;
+
+    uint256 low = 0;
+    uint256 high = len;
+
+    if (len > 5) {
+        uint256 mid = len - Math.sqrt(len);
+        if (key < _unsafeAccess(self.${opts.checkpointFieldName}, mid)._key) {
+            high = mid;
+        } else {
+            low = mid + 1;
+        }
+    }
+
+    uint256 pos = _upperBinaryLookup(self.${opts.checkpointFieldName}, key, low, high);
+
+    return pos == 0 ? 0 : _unsafeAccess(self.${opts.checkpointFieldName}, pos - 1).${opts.valueFieldName};
+}
 `;
 
 const legacyOperations = opts => `\
@@ -263,26 +309,6 @@ function _unsafeAccess(${opts.checkpointTypeName}[] storage self, uint256 pos)
 `;
 /* eslint-enable max-len */
 
-// OPTIONS
-const defaultOpts = size => ({
-  historyTypeName: `Trace${size}`,
-  checkpointTypeName: `Checkpoint${size}`,
-  checkpointFieldName: '_checkpoints',
-  keyTypeName: `uint${256 - size}`,
-  keyFieldName: '_key',
-  valueTypeName: `uint${size}`,
-  valueFieldName: '_value',
-});
-
-const OPTS = VALUE_SIZES.map(size => defaultOpts(size));
-
-const LEGACY_OPTS = {
-  ...defaultOpts(224),
-  historyTypeName: 'History',
-  checkpointTypeName: 'Checkpoint',
-  keyFieldName: '_blockNumber',
-};
-
 // GENERATE
 module.exports = format(
   header.trimEnd(),

+ 610 - 592
test/governance/Governor.test.js

@@ -1,4 +1,4 @@
-const { BN, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
+const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
 const { expect } = require('chai');
 const ethSigUtil = require('eth-sig-util');
 const Wallet = require('ethereumjs-wallet').default;
@@ -6,676 +6,694 @@ const { fromRpcSig } = require('ethereumjs-util');
 const Enums = require('../helpers/enums');
 const { getDomain, domainType } = require('../helpers/eip712');
 const { GovernorHelper } = require('../helpers/governance');
+const { clockFromReceipt } = require('../helpers/time');
 
 const { shouldSupportInterfaces } = require('../utils/introspection/SupportsInterface.behavior');
+const { shouldBehaveLikeEIP6372 } = require('./utils/EIP6372.behavior');
 
-const Token = artifacts.require('$ERC20Votes');
 const Governor = artifacts.require('$GovernorMock');
 const CallReceiver = artifacts.require('CallReceiverMock');
 const ERC721 = artifacts.require('$ERC721');
 const ERC1155 = artifacts.require('$ERC1155');
 
+const TOKENS = [
+  { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' },
+  { Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' },
+  { Token: artifacts.require('$ERC20VotesLegacyMock'), mode: 'blocknumber' },
+];
+
 contract('Governor', function (accounts) {
   const [owner, proposer, voter1, voter2, voter3, voter4] = accounts;
-  const empty = web3.utils.toChecksumAddress(web3.utils.randomHex(20));
 
   const name = 'OZ-Governor';
   const tokenName = 'MockToken';
   const tokenSymbol = 'MTKN';
   const tokenSupply = web3.utils.toWei('100');
-  const votingDelay = new BN(4);
-  const votingPeriod = new BN(16);
+  const votingDelay = web3.utils.toBN(4);
+  const votingPeriod = web3.utils.toBN(16);
   const value = web3.utils.toWei('1');
 
-  beforeEach(async function () {
-    this.chainId = await web3.eth.getChainId();
-    this.token = await Token.new(tokenName, tokenSymbol, tokenName);
-    this.mock = await Governor.new(
-      name, // name
-      votingDelay, // initialVotingDelay
-      votingPeriod, // initialVotingPeriod
-      0, // initialProposalThreshold
-      this.token.address, // tokenAddress
-      10, // quorumNumeratorValue
-    );
-    this.receiver = await CallReceiver.new();
-
-    this.helper = new GovernorHelper(this.mock);
-
-    await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value });
-
-    await this.token.$_mint(owner, tokenSupply);
-    await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner });
-    await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner });
-    await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner });
-    await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner });
-
-    this.proposal = this.helper.setProposal(
-      [
-        {
-          target: this.receiver.address,
-          data: this.receiver.contract.methods.mockFunction().encodeABI(),
-          value,
-        },
-      ],
-      '<proposal description>',
-    );
-  });
-
-  shouldSupportInterfaces(['ERC165', 'ERC1155Receiver', 'Governor', 'GovernorWithParams']);
-
-  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(votingDelay);
-    expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod);
-    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');
-  });
-
-  it('nominal workflow', async function () {
-    // Before
-    expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false);
-    expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(false);
-    expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(false);
-    expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(value);
-    expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal('0');
-
-    // Run proposal
-    const txPropose = await this.helper.propose({ from: proposer });
-
-    expectEvent(txPropose, 'ProposalCreated', {
-      proposalId: this.proposal.id,
-      proposer,
-      targets: this.proposal.targets,
-      // values: this.proposal.values,
-      signatures: this.proposal.signatures,
-      calldatas: this.proposal.data,
-      startBlock: new BN(txPropose.receipt.blockNumber).add(votingDelay),
-      endBlock: new BN(txPropose.receipt.blockNumber).add(votingDelay).add(votingPeriod),
-      description: this.proposal.description,
-    });
-
-    await this.helper.waitForSnapshot();
-
-    expectEvent(
-      await this.helper.vote({ support: Enums.VoteType.For, reason: 'This is nice' }, { from: voter1 }),
-      'VoteCast',
-      {
-        voter: voter1,
-        support: Enums.VoteType.For,
-        reason: 'This is nice',
-        weight: web3.utils.toWei('10'),
-      },
-    );
-
-    expectEvent(await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }), 'VoteCast', {
-      voter: voter2,
-      support: Enums.VoteType.For,
-      weight: web3.utils.toWei('7'),
-    });
-
-    expectEvent(await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }), 'VoteCast', {
-      voter: voter3,
-      support: Enums.VoteType.Against,
-      weight: web3.utils.toWei('5'),
-    });
-
-    expectEvent(await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }), 'VoteCast', {
-      voter: voter4,
-      support: Enums.VoteType.Abstain,
-      weight: web3.utils.toWei('2'),
-    });
-
-    await this.helper.waitForDeadline();
-
-    const txExecute = await this.helper.execute();
-
-    expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id });
-
-    await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled');
-
-    // After
-    expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false);
-    expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true);
-    expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true);
-    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(value);
-  });
-
-  it('vote with signature', async function () {
-    const voterBySig = Wallet.generate();
-    const voterBySigAddress = web3.utils.toChecksumAddress(voterBySig.getAddressString());
-
-    const signature = (contract, message) =>
-      getDomain(contract)
-        .then(domain => ({
-          primaryType: 'Ballot',
-          types: {
-            EIP712Domain: domainType(domain),
-            Ballot: [
-              { name: 'proposalId', type: 'uint256' },
-              { name: 'support', type: 'uint8' },
-            ],
-          },
-          domain,
-          message,
-        }))
-        .then(data => ethSigUtil.signTypedMessage(voterBySig.getPrivateKey(), { data }))
-        .then(fromRpcSig);
-
-    await this.token.delegate(voterBySigAddress, { from: voter1 });
-
-    // Run proposal
-    await this.helper.propose();
-    await this.helper.waitForSnapshot();
-    expectEvent(await this.helper.vote({ support: Enums.VoteType.For, signature }), 'VoteCast', {
-      voter: voterBySigAddress,
-      support: Enums.VoteType.For,
-    });
-    await this.helper.waitForDeadline();
-    await this.helper.execute();
-
-    // After
-    expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false);
-    expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(false);
-    expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(false);
-    expect(await this.mock.hasVoted(this.proposal.id, voterBySigAddress)).to.be.equal(true);
-  });
-
-  it('send ethers', async function () {
-    this.proposal = this.helper.setProposal(
-      [
-        {
-          target: empty,
-          value,
-        },
-      ],
-      '<proposal description>',
-    );
-
-    // Before
-    expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(value);
-    expect(await web3.eth.getBalance(empty)).to.be.bignumber.equal('0');
-
-    // Run proposal
-    await this.helper.propose();
-    await this.helper.waitForSnapshot();
-    await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-    await this.helper.waitForDeadline();
-    await this.helper.execute();
-
-    // After
-    expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0');
-    expect(await web3.eth.getBalance(empty)).to.be.bignumber.equal(value);
-  });
-
-  describe('should revert', function () {
-    describe('on propose', function () {
-      it('if proposal already exists', async function () {
-        await this.helper.propose();
-        await expectRevert(this.helper.propose(), 'Governor: proposal already exists');
-      });
-    });
-
-    describe('on vote', function () {
-      it('if proposal does not exist', async function () {
-        await expectRevert(
-          this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
-          'Governor: unknown proposal id',
-        );
-      });
-
-      it('if voting has not started', async function () {
-        await this.helper.propose();
-        await expectRevert(
-          this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
-          'Governor: vote not currently active',
+  for (const { mode, Token } of TOKENS) {
+    describe(`using ${Token._json.contractName}`, function () {
+      beforeEach(async function () {
+        this.chainId = await web3.eth.getChainId();
+        this.token = await Token.new(tokenName, tokenSymbol, tokenName);
+        this.mock = await Governor.new(
+          name, // name
+          votingDelay, // initialVotingDelay
+          votingPeriod, // initialVotingPeriod
+          0, // initialProposalThreshold
+          this.token.address, // tokenAddress
+          10, // quorumNumeratorValue
         );
-      });
+        this.receiver = await CallReceiver.new();
 
-      it('if support value is invalid', async function () {
-        await this.helper.propose();
-        await this.helper.waitForSnapshot();
-        await expectRevert(
-          this.helper.vote({ support: new BN('255') }),
-          'GovernorVotingSimple: invalid value for enum VoteType',
-        );
-      });
+        this.helper = new GovernorHelper(this.mock, mode);
 
-      it('if vote was already casted', async function () {
-        await this.helper.propose();
-        await this.helper.waitForSnapshot();
-        await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-        await expectRevert(
-          this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
-          'GovernorVotingSimple: vote already cast',
-        );
-      });
+        await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value });
 
-      it('if voting is over', async function () {
-        await this.helper.propose();
-        await this.helper.waitForDeadline();
-        await expectRevert(
-          this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
-          'Governor: vote not currently active',
-        );
-      });
-    });
-
-    describe('on execute', function () {
-      it('if proposal does not exist', async function () {
-        await expectRevert(this.helper.execute(), 'Governor: unknown proposal id');
-      });
+        await this.token.$_mint(owner, tokenSupply);
+        await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner });
+        await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner });
+        await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner });
+        await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner });
 
-      it('if quorum is not reached', async function () {
-        await this.helper.propose();
-        await this.helper.waitForSnapshot();
-        await this.helper.vote({ support: Enums.VoteType.For }, { from: voter3 });
-        await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
-      });
-
-      it('if score not reached', async function () {
-        await this.helper.propose();
-        await this.helper.waitForSnapshot();
-        await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter1 });
-        await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
-      });
-
-      it('if voting is not over', async function () {
-        await this.helper.propose();
-        await this.helper.waitForSnapshot();
-        await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-        await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
-      });
-
-      it('if receiver revert without reason', async function () {
         this.proposal = this.helper.setProposal(
           [
             {
               target: this.receiver.address,
-              data: this.receiver.contract.methods.mockFunctionRevertsNoReason().encodeABI(),
+              data: this.receiver.contract.methods.mockFunction().encodeABI(),
+              value,
             },
           ],
           '<proposal description>',
         );
-
-        await this.helper.propose();
-        await this.helper.waitForSnapshot();
-        await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-        await this.helper.waitForDeadline();
-        await expectRevert(this.helper.execute(), 'Governor: call reverted without message');
       });
 
-      it('if receiver revert with reason', async function () {
-        this.proposal = this.helper.setProposal(
-          [
-            {
-              target: this.receiver.address,
-              data: this.receiver.contract.methods.mockFunctionRevertsReason().encodeABI(),
-            },
-          ],
-          '<proposal description>',
-        );
+      shouldSupportInterfaces(['ERC165', 'ERC1155Receiver', 'Governor', 'GovernorWithParams']);
+      shouldBehaveLikeEIP6372(mode);
 
-        await this.helper.propose();
-        await this.helper.waitForSnapshot();
-        await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-        await this.helper.waitForDeadline();
-        await expectRevert(this.helper.execute(), 'CallReceiverMock: reverting');
+      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(votingDelay);
+        expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod);
+        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');
       });
 
-      it('if proposal was already executed', async function () {
-        await this.helper.propose();
+      it('nominal workflow', async function () {
+        // Before
+        expect(await this.mock.$_proposalProposer(this.proposal.id)).to.be.equal(constants.ZERO_ADDRESS);
+        expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false);
+        expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(false);
+        expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(false);
+        expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(value);
+        expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal('0');
+
+        // Run proposal
+        const txPropose = await this.helper.propose({ from: proposer });
+
+        expectEvent(txPropose, 'ProposalCreated', {
+          proposalId: this.proposal.id,
+          proposer,
+          targets: this.proposal.targets,
+          // values: this.proposal.values,
+          signatures: this.proposal.signatures,
+          calldatas: this.proposal.data,
+          voteStart: web3.utils.toBN(await clockFromReceipt[mode](txPropose.receipt)).add(votingDelay),
+          voteEnd: web3.utils
+            .toBN(await clockFromReceipt[mode](txPropose.receipt))
+            .add(votingDelay)
+            .add(votingPeriod),
+          description: this.proposal.description,
+        });
+
         await this.helper.waitForSnapshot();
-        await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-        await this.helper.waitForDeadline();
-        await this.helper.execute();
-        await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
-      });
-    });
-  });
 
-  describe('state', function () {
-    it('Unset', async function () {
-      await expectRevert(this.mock.state(this.proposal.id), 'Governor: unknown proposal id');
-    });
+        expectEvent(
+          await this.helper.vote({ support: Enums.VoteType.For, reason: 'This is nice' }, { from: voter1 }),
+          'VoteCast',
+          {
+            voter: voter1,
+            support: Enums.VoteType.For,
+            reason: 'This is nice',
+            weight: web3.utils.toWei('10'),
+          },
+        );
 
-    it('Pending & Active', async function () {
-      await this.helper.propose();
-      expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Pending);
-      await this.helper.waitForSnapshot();
-      expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Pending);
-      await this.helper.waitForSnapshot(+1);
-      expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active);
-    });
+        expectEvent(await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }), 'VoteCast', {
+          voter: voter2,
+          support: Enums.VoteType.For,
+          weight: web3.utils.toWei('7'),
+        });
 
-    it('Defeated', async function () {
-      await this.helper.propose();
-      await this.helper.waitForDeadline();
-      expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active);
-      await this.helper.waitForDeadline(+1);
-      expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Defeated);
-    });
+        expectEvent(await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }), 'VoteCast', {
+          voter: voter3,
+          support: Enums.VoteType.Against,
+          weight: web3.utils.toWei('5'),
+        });
 
-    it('Succeeded', async function () {
-      await this.helper.propose();
-      await this.helper.waitForSnapshot();
-      await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-      await this.helper.waitForDeadline();
-      expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active);
-      await this.helper.waitForDeadline(+1);
-      expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded);
-    });
+        expectEvent(await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }), 'VoteCast', {
+          voter: voter4,
+          support: Enums.VoteType.Abstain,
+          weight: web3.utils.toWei('2'),
+        });
 
-    it('Executed', async function () {
-      await this.helper.propose();
-      await this.helper.waitForSnapshot();
-      await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-      await this.helper.waitForDeadline();
-      await this.helper.execute();
-      expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Executed);
-    });
-  });
+        await this.helper.waitForDeadline();
 
-  describe('cancel', function () {
-    describe('internal', function () {
-      it('before proposal', async function () {
-        await expectRevert(this.helper.cancel('internal'), 'Governor: unknown proposal id');
-      });
+        const txExecute = await this.helper.execute();
 
-      it('after proposal', async function () {
-        await this.helper.propose();
+        expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id });
 
-        await this.helper.cancel('internal');
-        expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
+        await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled');
 
-        await this.helper.waitForSnapshot();
-        await expectRevert(
-          this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
-          'Governor: vote not currently active',
-        );
+        // After
+        expect(await this.mock.$_proposalProposer(this.proposal.id)).to.be.equal(proposer);
+        expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false);
+        expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true);
+        expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true);
+        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(value);
       });
 
-      it('after vote', async function () {
+      it('vote with signature', async function () {
+        const voterBySig = Wallet.generate();
+        const voterBySigAddress = web3.utils.toChecksumAddress(voterBySig.getAddressString());
+
+        const signature = (contract, message) =>
+          getDomain(contract)
+            .then(domain => ({
+              primaryType: 'Ballot',
+              types: {
+                EIP712Domain: domainType(domain),
+                Ballot: [
+                  { name: 'proposalId', type: 'uint256' },
+                  { name: 'support', type: 'uint8' },
+                ],
+              },
+              domain,
+              message,
+            }))
+            .then(data => ethSigUtil.signTypedMessage(voterBySig.getPrivateKey(), { data }))
+            .then(fromRpcSig);
+
+        await this.token.delegate(voterBySigAddress, { from: voter1 });
+
+        // Run proposal
         await this.helper.propose();
         await this.helper.waitForSnapshot();
-        await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-
-        await this.helper.cancel('internal');
-        expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
-
+        expectEvent(await this.helper.vote({ support: Enums.VoteType.For, signature }), 'VoteCast', {
+          voter: voterBySigAddress,
+          support: Enums.VoteType.For,
+        });
         await this.helper.waitForDeadline();
-        await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
+        await this.helper.execute();
+
+        // After
+        expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false);
+        expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(false);
+        expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(false);
+        expect(await this.mock.hasVoted(this.proposal.id, voterBySigAddress)).to.be.equal(true);
       });
 
-      it('after deadline', async function () {
-        await this.helper.propose();
-        await this.helper.waitForSnapshot();
-        await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-        await this.helper.waitForDeadline();
+      it('send ethers', async function () {
+        const empty = web3.utils.toChecksumAddress(web3.utils.randomHex(20));
 
-        await this.helper.cancel('internal');
-        expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
+        this.proposal = this.helper.setProposal(
+          [
+            {
+              target: empty,
+              value,
+            },
+          ],
+          '<proposal description>',
+        );
 
-        await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
-      });
+        // Before
+        expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(value);
+        expect(await web3.eth.getBalance(empty)).to.be.bignumber.equal('0');
 
-      it('after execution', async function () {
+        // Run proposal
         await this.helper.propose();
         await this.helper.waitForSnapshot();
         await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
         await this.helper.waitForDeadline();
         await this.helper.execute();
 
-        await expectRevert(this.helper.cancel('internal'), 'Governor: proposal not active');
+        // After
+        expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0');
+        expect(await web3.eth.getBalance(empty)).to.be.bignumber.equal(value);
       });
-    });
 
-    describe('public', function () {
-      it('before proposal', async function () {
-        await expectRevert(this.helper.cancel('external'), 'Governor: unknown proposal id');
+      describe('should revert', function () {
+        describe('on propose', function () {
+          it('if proposal already exists', async function () {
+            await this.helper.propose();
+            await expectRevert(this.helper.propose(), 'Governor: proposal already exists');
+          });
+        });
+
+        describe('on vote', function () {
+          it('if proposal does not exist', async function () {
+            await expectRevert(
+              this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
+              'Governor: unknown proposal id',
+            );
+          });
+
+          it('if voting has not started', async function () {
+            await this.helper.propose();
+            await expectRevert(
+              this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
+              'Governor: vote not currently active',
+            );
+          });
+
+          it('if support value is invalid', async function () {
+            await this.helper.propose();
+            await this.helper.waitForSnapshot();
+            await expectRevert(
+              this.helper.vote({ support: web3.utils.toBN('255') }),
+              'GovernorVotingSimple: invalid value for enum VoteType',
+            );
+          });
+
+          it('if vote was already casted', async function () {
+            await this.helper.propose();
+            await this.helper.waitForSnapshot();
+            await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+            await expectRevert(
+              this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
+              'GovernorVotingSimple: vote already cast',
+            );
+          });
+
+          it('if voting is over', async function () {
+            await this.helper.propose();
+            await this.helper.waitForDeadline();
+            await expectRevert(
+              this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
+              'Governor: vote not currently active',
+            );
+          });
+        });
+
+        describe('on execute', function () {
+          it('if proposal does not exist', async function () {
+            await expectRevert(this.helper.execute(), 'Governor: unknown proposal id');
+          });
+
+          it('if quorum is not reached', async function () {
+            await this.helper.propose();
+            await this.helper.waitForSnapshot();
+            await this.helper.vote({ support: Enums.VoteType.For }, { from: voter3 });
+            await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
+          });
+
+          it('if score not reached', async function () {
+            await this.helper.propose();
+            await this.helper.waitForSnapshot();
+            await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter1 });
+            await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
+          });
+
+          it('if voting is not over', async function () {
+            await this.helper.propose();
+            await this.helper.waitForSnapshot();
+            await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+            await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
+          });
+
+          it('if receiver revert without reason', async function () {
+            this.proposal = this.helper.setProposal(
+              [
+                {
+                  target: this.receiver.address,
+                  data: this.receiver.contract.methods.mockFunctionRevertsNoReason().encodeABI(),
+                },
+              ],
+              '<proposal description>',
+            );
+
+            await this.helper.propose();
+            await this.helper.waitForSnapshot();
+            await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+            await this.helper.waitForDeadline();
+            await expectRevert(this.helper.execute(), 'Governor: call reverted without message');
+          });
+
+          it('if receiver revert with reason', async function () {
+            this.proposal = this.helper.setProposal(
+              [
+                {
+                  target: this.receiver.address,
+                  data: this.receiver.contract.methods.mockFunctionRevertsReason().encodeABI(),
+                },
+              ],
+              '<proposal description>',
+            );
+
+            await this.helper.propose();
+            await this.helper.waitForSnapshot();
+            await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+            await this.helper.waitForDeadline();
+            await expectRevert(this.helper.execute(), 'CallReceiverMock: reverting');
+          });
+
+          it('if proposal was already executed', async function () {
+            await this.helper.propose();
+            await this.helper.waitForSnapshot();
+            await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+            await this.helper.waitForDeadline();
+            await this.helper.execute();
+            await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
+          });
+        });
       });
 
-      it('after proposal', async function () {
-        await this.helper.propose();
-
-        await this.helper.cancel('external');
-      });
-
-      it('after proposal - restricted to proposer', async function () {
-        await this.helper.propose();
-
-        await expectRevert(this.helper.cancel('external', { from: owner }), 'Governor: only proposer can cancel');
+      describe('state', function () {
+        it('Unset', async function () {
+          await expectRevert(this.mock.state(this.proposal.id), 'Governor: unknown proposal id');
+        });
+
+        it('Pending & Active', async function () {
+          await this.helper.propose();
+          expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Pending);
+          await this.helper.waitForSnapshot();
+          expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Pending);
+          await this.helper.waitForSnapshot(+1);
+          expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active);
+        });
+
+        it('Defeated', async function () {
+          await this.helper.propose();
+          await this.helper.waitForDeadline();
+          expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active);
+          await this.helper.waitForDeadline(+1);
+          expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Defeated);
+        });
+
+        it('Succeeded', async function () {
+          await this.helper.propose();
+          await this.helper.waitForSnapshot();
+          await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+          await this.helper.waitForDeadline();
+          expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active);
+          await this.helper.waitForDeadline(+1);
+          expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded);
+        });
+
+        it('Executed', async function () {
+          await this.helper.propose();
+          await this.helper.waitForSnapshot();
+          await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+          await this.helper.waitForDeadline();
+          await this.helper.execute();
+          expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Executed);
+        });
       });
 
-      it('after vote started', async function () {
-        await this.helper.propose();
-        await this.helper.waitForSnapshot(1); // snapshot + 1 block
-
-        await expectRevert(this.helper.cancel('external'), 'Governor: too late to cancel');
+      describe('cancel', function () {
+        describe('internal', function () {
+          it('before proposal', async function () {
+            await expectRevert(this.helper.cancel('internal'), 'Governor: unknown proposal id');
+          });
+
+          it('after proposal', async function () {
+            await this.helper.propose();
+
+            await this.helper.cancel('internal');
+            expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
+
+            await this.helper.waitForSnapshot();
+            await expectRevert(
+              this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
+              'Governor: vote not currently active',
+            );
+          });
+
+          it('after vote', async function () {
+            await this.helper.propose();
+            await this.helper.waitForSnapshot();
+            await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+
+            await this.helper.cancel('internal');
+            expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
+
+            await this.helper.waitForDeadline();
+            await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
+          });
+
+          it('after deadline', async function () {
+            await this.helper.propose();
+            await this.helper.waitForSnapshot();
+            await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+            await this.helper.waitForDeadline();
+
+            await this.helper.cancel('internal');
+            expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
+
+            await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
+          });
+
+          it('after execution', async function () {
+            await this.helper.propose();
+            await this.helper.waitForSnapshot();
+            await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+            await this.helper.waitForDeadline();
+            await this.helper.execute();
+
+            await expectRevert(this.helper.cancel('internal'), 'Governor: proposal not active');
+          });
+        });
+
+        describe('public', function () {
+          it('before proposal', async function () {
+            await expectRevert(this.helper.cancel('external'), 'Governor: unknown proposal id');
+          });
+
+          it('after proposal', async function () {
+            await this.helper.propose();
+
+            await this.helper.cancel('external');
+          });
+
+          it('after proposal - restricted to proposer', async function () {
+            await this.helper.propose();
+
+            await expectRevert(this.helper.cancel('external', { from: owner }), 'Governor: only proposer can cancel');
+          });
+
+          it('after vote started', async function () {
+            await this.helper.propose();
+            await this.helper.waitForSnapshot(1); // snapshot + 1 block
+
+            await expectRevert(this.helper.cancel('external'), 'Governor: too late to cancel');
+          });
+
+          it('after vote', async function () {
+            await this.helper.propose();
+            await this.helper.waitForSnapshot();
+            await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+
+            await expectRevert(this.helper.cancel('external'), 'Governor: too late to cancel');
+          });
+
+          it('after deadline', async function () {
+            await this.helper.propose();
+            await this.helper.waitForSnapshot();
+            await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+            await this.helper.waitForDeadline();
+
+            await expectRevert(this.helper.cancel('external'), 'Governor: too late to cancel');
+          });
+
+          it('after execution', async function () {
+            await this.helper.propose();
+            await this.helper.waitForSnapshot();
+            await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+            await this.helper.waitForDeadline();
+            await this.helper.execute();
+
+            await expectRevert(this.helper.cancel('external'), 'Governor: too late to cancel');
+          });
+        });
       });
 
-      it('after vote', async function () {
-        await this.helper.propose();
-        await this.helper.waitForSnapshot();
-        await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-
-        await expectRevert(this.helper.cancel('external'), 'Governor: too late to cancel');
-      });
-
-      it('after deadline', async function () {
-        await this.helper.propose();
-        await this.helper.waitForSnapshot();
-        await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-        await this.helper.waitForDeadline();
-
-        await expectRevert(this.helper.cancel('external'), 'Governor: too late to cancel');
-      });
-
-      it('after execution', async function () {
-        await this.helper.propose();
-        await this.helper.waitForSnapshot();
-        await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-        await this.helper.waitForDeadline();
-        await this.helper.execute();
-
-        await expectRevert(this.helper.cancel('external'), 'Governor: too late to cancel');
-      });
-    });
-  });
-
-  describe('proposal length', function () {
-    it('empty', async function () {
-      this.helper.setProposal([], '<proposal description>');
-      await expectRevert(this.helper.propose(), 'Governor: empty proposal');
-    });
-
-    it('mismatch #1', async function () {
-      this.helper.setProposal(
-        {
-          targets: [],
-          values: [web3.utils.toWei('0')],
-          data: [this.receiver.contract.methods.mockFunction().encodeABI()],
-        },
-        '<proposal description>',
-      );
-      await expectRevert(this.helper.propose(), 'Governor: invalid proposal length');
-    });
-
-    it('mismatch #2', async function () {
-      this.helper.setProposal(
-        {
-          targets: [this.receiver.address],
-          values: [],
-          data: [this.receiver.contract.methods.mockFunction().encodeABI()],
-        },
-        '<proposal description>',
-      );
-      await expectRevert(this.helper.propose(), 'Governor: invalid proposal length');
-    });
-
-    it('mismatch #3', async function () {
-      this.helper.setProposal(
-        {
-          targets: [this.receiver.address],
-          values: [web3.utils.toWei('0')],
-          data: [],
-        },
-        '<proposal description>',
-      );
-      await expectRevert(this.helper.propose(), 'Governor: invalid proposal length');
-    });
-  });
-
-  describe('onlyGovernance updates', function () {
-    it('setVotingDelay is protected', async function () {
-      await expectRevert(this.mock.setVotingDelay('0'), 'Governor: onlyGovernance');
-    });
-
-    it('setVotingPeriod is protected', async function () {
-      await expectRevert(this.mock.setVotingPeriod('32'), 'Governor: onlyGovernance');
-    });
-
-    it('setProposalThreshold is protected', async function () {
-      await expectRevert(this.mock.setProposalThreshold('1000000000000000000'), 'Governor: onlyGovernance');
-    });
-
-    it('can setVotingDelay through governance', async function () {
-      this.helper.setProposal(
-        [
-          {
-            target: this.mock.address,
-            data: this.mock.contract.methods.setVotingDelay('0').encodeABI(),
-          },
-        ],
-        '<proposal description>',
-      );
-
-      await this.helper.propose();
-      await this.helper.waitForSnapshot();
-      await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-      await this.helper.waitForDeadline();
-
-      expectEvent(await this.helper.execute(), 'VotingDelaySet', { oldVotingDelay: '4', newVotingDelay: '0' });
-
-      expect(await this.mock.votingDelay()).to.be.bignumber.equal('0');
-    });
-
-    it('can setVotingPeriod through governance', async function () {
-      this.helper.setProposal(
-        [
-          {
-            target: this.mock.address,
-            data: this.mock.contract.methods.setVotingPeriod('32').encodeABI(),
-          },
-        ],
-        '<proposal description>',
-      );
-
-      await this.helper.propose();
-      await this.helper.waitForSnapshot();
-      await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-      await this.helper.waitForDeadline();
-
-      expectEvent(await this.helper.execute(), 'VotingPeriodSet', { oldVotingPeriod: '16', newVotingPeriod: '32' });
-
-      expect(await this.mock.votingPeriod()).to.be.bignumber.equal('32');
-    });
-
-    it('cannot setVotingPeriod to 0 through governance', async function () {
-      this.helper.setProposal(
-        [
-          {
-            target: this.mock.address,
-            data: this.mock.contract.methods.setVotingPeriod('0').encodeABI(),
-          },
-        ],
-        '<proposal description>',
-      );
-
-      await this.helper.propose();
-      await this.helper.waitForSnapshot();
-      await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-      await this.helper.waitForDeadline();
+      describe('proposal length', function () {
+        it('empty', async function () {
+          this.helper.setProposal([], '<proposal description>');
+          await expectRevert(this.helper.propose(), 'Governor: empty proposal');
+        });
 
-      await expectRevert(this.helper.execute(), 'GovernorSettings: voting period too low');
-    });
-
-    it('can setProposalThreshold to 0 through governance', async function () {
-      this.helper.setProposal(
-        [
-          {
-            target: this.mock.address,
-            data: this.mock.contract.methods.setProposalThreshold('1000000000000000000').encodeABI(),
-          },
-        ],
-        '<proposal description>',
-      );
-
-      await this.helper.propose();
-      await this.helper.waitForSnapshot();
-      await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-      await this.helper.waitForDeadline();
-
-      expectEvent(await this.helper.execute(), 'ProposalThresholdSet', {
-        oldProposalThreshold: '0',
-        newProposalThreshold: '1000000000000000000',
-      });
-
-      expect(await this.mock.proposalThreshold()).to.be.bignumber.equal('1000000000000000000');
-    });
-  });
+        it('mismatch #1', async function () {
+          this.helper.setProposal(
+            {
+              targets: [],
+              values: [web3.utils.toWei('0')],
+              data: [this.receiver.contract.methods.mockFunction().encodeABI()],
+            },
+            '<proposal description>',
+          );
+          await expectRevert(this.helper.propose(), 'Governor: invalid proposal length');
+        });
 
-  describe('safe receive', function () {
-    describe('ERC721', function () {
-      const name = 'Non Fungible Token';
-      const symbol = 'NFT';
-      const tokenId = new BN(1);
+        it('mismatch #2', async function () {
+          this.helper.setProposal(
+            {
+              targets: [this.receiver.address],
+              values: [],
+              data: [this.receiver.contract.methods.mockFunction().encodeABI()],
+            },
+            '<proposal description>',
+          );
+          await expectRevert(this.helper.propose(), 'Governor: invalid proposal length');
+        });
 
-      beforeEach(async function () {
-        this.token = await ERC721.new(name, symbol);
-        await this.token.$_mint(owner, tokenId);
+        it('mismatch #3', async function () {
+          this.helper.setProposal(
+            {
+              targets: [this.receiver.address],
+              values: [web3.utils.toWei('0')],
+              data: [],
+            },
+            '<proposal description>',
+          );
+          await expectRevert(this.helper.propose(), 'Governor: invalid proposal length');
+        });
       });
 
-      it('can receive an ERC721 safeTransfer', async function () {
-        await this.token.safeTransferFrom(owner, this.mock.address, tokenId, { from: owner });
-      });
-    });
+      describe('onlyGovernance updates', function () {
+        it('setVotingDelay is protected', async function () {
+          await expectRevert(this.mock.setVotingDelay('0'), 'Governor: onlyGovernance');
+        });
+
+        it('setVotingPeriod is protected', async function () {
+          await expectRevert(this.mock.setVotingPeriod('32'), 'Governor: onlyGovernance');
+        });
+
+        it('setProposalThreshold is protected', async function () {
+          await expectRevert(this.mock.setProposalThreshold('1000000000000000000'), 'Governor: onlyGovernance');
+        });
+
+        it('can setVotingDelay through governance', async function () {
+          this.helper.setProposal(
+            [
+              {
+                target: this.mock.address,
+                data: this.mock.contract.methods.setVotingDelay('0').encodeABI(),
+              },
+            ],
+            '<proposal description>',
+          );
+
+          await this.helper.propose();
+          await this.helper.waitForSnapshot();
+          await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+          await this.helper.waitForDeadline();
+
+          expectEvent(await this.helper.execute(), 'VotingDelaySet', { oldVotingDelay: '4', newVotingDelay: '0' });
+
+          expect(await this.mock.votingDelay()).to.be.bignumber.equal('0');
+        });
+
+        it('can setVotingPeriod through governance', async function () {
+          this.helper.setProposal(
+            [
+              {
+                target: this.mock.address,
+                data: this.mock.contract.methods.setVotingPeriod('32').encodeABI(),
+              },
+            ],
+            '<proposal description>',
+          );
+
+          await this.helper.propose();
+          await this.helper.waitForSnapshot();
+          await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+          await this.helper.waitForDeadline();
+
+          expectEvent(await this.helper.execute(), 'VotingPeriodSet', { oldVotingPeriod: '16', newVotingPeriod: '32' });
+
+          expect(await this.mock.votingPeriod()).to.be.bignumber.equal('32');
+        });
+
+        it('cannot setVotingPeriod to 0 through governance', async function () {
+          this.helper.setProposal(
+            [
+              {
+                target: this.mock.address,
+                data: this.mock.contract.methods.setVotingPeriod('0').encodeABI(),
+              },
+            ],
+            '<proposal description>',
+          );
+
+          await this.helper.propose();
+          await this.helper.waitForSnapshot();
+          await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+          await this.helper.waitForDeadline();
+
+          await expectRevert(this.helper.execute(), 'GovernorSettings: voting period too low');
+        });
+
+        it('can setProposalThreshold to 0 through governance', async function () {
+          this.helper.setProposal(
+            [
+              {
+                target: this.mock.address,
+                data: this.mock.contract.methods.setProposalThreshold('1000000000000000000').encodeABI(),
+              },
+            ],
+            '<proposal description>',
+          );
 
-    describe('ERC1155', function () {
-      const uri = 'https://token-cdn-domain/{id}.json';
-      const tokenIds = {
-        1: new BN(1000),
-        2: new BN(2000),
-        3: new BN(3000),
-      };
+          await this.helper.propose();
+          await this.helper.waitForSnapshot();
+          await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+          await this.helper.waitForDeadline();
 
-      beforeEach(async function () {
-        this.token = await ERC1155.new(uri);
-        await this.token.$_mintBatch(owner, Object.keys(tokenIds), Object.values(tokenIds), '0x');
-      });
+          expectEvent(await this.helper.execute(), 'ProposalThresholdSet', {
+            oldProposalThreshold: '0',
+            newProposalThreshold: '1000000000000000000',
+          });
 
-      it('can receive ERC1155 safeTransfer', async function () {
-        await this.token.safeTransferFrom(
-          owner,
-          this.mock.address,
-          ...Object.entries(tokenIds)[0], // id + amount
-          '0x',
-          { from: owner },
-        );
+          expect(await this.mock.proposalThreshold()).to.be.bignumber.equal('1000000000000000000');
+        });
       });
 
-      it('can receive ERC1155 safeBatchTransfer', async function () {
-        await this.token.safeBatchTransferFrom(
-          owner,
-          this.mock.address,
-          Object.keys(tokenIds),
-          Object.values(tokenIds),
-          '0x',
-          { from: owner },
-        );
+      describe('safe receive', function () {
+        describe('ERC721', function () {
+          const name = 'Non Fungible Token';
+          const symbol = 'NFT';
+          const tokenId = web3.utils.toBN(1);
+
+          beforeEach(async function () {
+            this.token = await ERC721.new(name, symbol);
+            await this.token.$_mint(owner, tokenId);
+          });
+
+          it('can receive an ERC721 safeTransfer', async function () {
+            await this.token.safeTransferFrom(owner, this.mock.address, tokenId, { from: owner });
+          });
+        });
+
+        describe('ERC1155', function () {
+          const uri = 'https://token-cdn-domain/{id}.json';
+          const tokenIds = {
+            1: web3.utils.toBN(1000),
+            2: web3.utils.toBN(2000),
+            3: web3.utils.toBN(3000),
+          };
+
+          beforeEach(async function () {
+            this.token = await ERC1155.new(uri);
+            await this.token.$_mintBatch(owner, Object.keys(tokenIds), Object.values(tokenIds), '0x');
+          });
+
+          it('can receive ERC1155 safeTransfer', async function () {
+            await this.token.safeTransferFrom(
+              owner,
+              this.mock.address,
+              ...Object.entries(tokenIds)[0], // id + amount
+              '0x',
+              { from: owner },
+            );
+          });
+
+          it('can receive ERC1155 safeBatchTransfer', async function () {
+            await this.token.safeBatchTransferFrom(
+              owner,
+              this.mock.address,
+              Object.keys(tokenIds),
+              Object.values(tokenIds),
+              '0x',
+              { from: owner },
+            );
+          });
+        });
       });
     });
-  });
+  }
 });

+ 227 - 202
test/governance/compatibility/GovernorCompatibilityBravo.test.js

@@ -1,14 +1,16 @@
-const { BN, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
+const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
 const { expect } = require('chai');
 const RLP = require('rlp');
 const Enums = require('../../helpers/enums');
 const { GovernorHelper } = require('../../helpers/governance');
+const { clockFromReceipt } = require('../../helpers/time');
 
-const Token = artifacts.require('$ERC20VotesComp');
 const Timelock = artifacts.require('CompTimelock');
 const Governor = artifacts.require('$GovernorCompatibilityBravoMock');
 const CallReceiver = artifacts.require('CallReceiverMock');
 
+const { shouldBehaveLikeEIP6372 } = require('../utils/EIP6372.behavior');
+
 function makeContractAddress(creator, nonce) {
   return web3.utils.toChecksumAddress(
     web3.utils
@@ -18,6 +20,11 @@ function makeContractAddress(creator, nonce) {
   );
 }
 
+const TOKENS = [
+  { Token: artifacts.require('$ERC20VotesComp'), mode: 'blocknumber' },
+  { Token: artifacts.require('$ERC20VotesCompTimestampMock'), mode: 'timestamp' },
+];
+
 contract('GovernorCompatibilityBravo', function (accounts) {
   const [owner, proposer, voter1, voter2, voter3, voter4, other] = accounts;
 
@@ -26,218 +33,236 @@ contract('GovernorCompatibilityBravo', function (accounts) {
   const tokenName = 'MockToken';
   const tokenSymbol = 'MTKN';
   const tokenSupply = web3.utils.toWei('100');
-  const votingDelay = new BN(4);
-  const votingPeriod = new BN(16);
+  const votingDelay = web3.utils.toBN(4);
+  const votingPeriod = web3.utils.toBN(16);
   const proposalThreshold = web3.utils.toWei('10');
   const value = web3.utils.toWei('1');
 
-  beforeEach(async function () {
-    const [deployer] = await web3.eth.getAccounts();
-
-    this.token = await Token.new(tokenName, tokenSymbol, tokenName);
-
-    // 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,
-      votingDelay,
-      votingPeriod,
-      proposalThreshold,
-      this.timelock.address,
-      this.token.address,
-    );
-    this.receiver = await CallReceiver.new();
-
-    this.helper = new GovernorHelper(this.mock);
-
-    await web3.eth.sendTransaction({ from: owner, to: this.timelock.address, value });
-
-    await this.token.$_mint(owner, tokenSupply);
-    await this.helper.delegate({ token: this.token, to: proposer, value: proposalThreshold }, { from: owner });
-    await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner });
-    await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner });
-    await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner });
-    await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner });
-
-    // default proposal
-    this.proposal = this.helper.setProposal(
-      [
-        {
-          target: this.receiver.address,
-          value,
-          signature: 'mockFunction()',
-        },
-      ],
-      '<proposal description>',
-    );
-  });
-
-  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(votingDelay);
-    expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod);
-    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');
-  });
-
-  it('nominal workflow', async function () {
-    // Before
-    expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false);
-    expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(false);
-    expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(false);
-    expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0');
-    expect(await web3.eth.getBalance(this.timelock.address)).to.be.bignumber.equal(value);
-    expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal('0');
-
-    // Run proposal
-    const txPropose = await this.helper.propose({ from: proposer });
-    await this.helper.waitForSnapshot();
-    await this.helper.vote({ support: Enums.VoteType.For, reason: 'This is nice' }, { from: voter1 });
-    await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 });
-    await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 });
-    await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 });
-    await this.helper.waitForDeadline();
-    await this.helper.queue();
-    await this.helper.waitForEta();
-    const txExecute = await this.helper.execute();
-
-    // After
-    expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false);
-    expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true);
-    expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true);
-    expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0');
-    expect(await web3.eth.getBalance(this.timelock.address)).to.be.bignumber.equal('0');
-    expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal(value);
-
-    const proposal = await this.mock.proposals(this.proposal.id);
-    expect(proposal.id).to.be.bignumber.equal(this.proposal.id);
-    expect(proposal.proposer).to.be.equal(proposer);
-    expect(proposal.eta).to.be.bignumber.equal(await this.mock.proposalEta(this.proposal.id));
-    expect(proposal.startBlock).to.be.bignumber.equal(await this.mock.proposalSnapshot(this.proposal.id));
-    expect(proposal.endBlock).to.be.bignumber.equal(await this.mock.proposalDeadline(this.proposal.id));
-    expect(proposal.canceled).to.be.equal(false);
-    expect(proposal.executed).to.be.equal(true);
-
-    const action = await this.mock.getActions(this.proposal.id);
-    expect(action.targets).to.be.deep.equal(this.proposal.targets);
-    // expect(action.values).to.be.deep.equal(this.proposal.values);
-    expect(action.signatures).to.be.deep.equal(this.proposal.signatures);
-    expect(action.calldatas).to.be.deep.equal(this.proposal.data);
-
-    const voteReceipt1 = await this.mock.getReceipt(this.proposal.id, voter1);
-    expect(voteReceipt1.hasVoted).to.be.equal(true);
-    expect(voteReceipt1.support).to.be.bignumber.equal(Enums.VoteType.For);
-    expect(voteReceipt1.votes).to.be.bignumber.equal(web3.utils.toWei('10'));
-
-    const voteReceipt2 = await this.mock.getReceipt(this.proposal.id, voter2);
-    expect(voteReceipt2.hasVoted).to.be.equal(true);
-    expect(voteReceipt2.support).to.be.bignumber.equal(Enums.VoteType.For);
-    expect(voteReceipt2.votes).to.be.bignumber.equal(web3.utils.toWei('7'));
-
-    const voteReceipt3 = await this.mock.getReceipt(this.proposal.id, voter3);
-    expect(voteReceipt3.hasVoted).to.be.equal(true);
-    expect(voteReceipt3.support).to.be.bignumber.equal(Enums.VoteType.Against);
-    expect(voteReceipt3.votes).to.be.bignumber.equal(web3.utils.toWei('5'));
-
-    const voteReceipt4 = await this.mock.getReceipt(this.proposal.id, voter4);
-    expect(voteReceipt4.hasVoted).to.be.equal(true);
-    expect(voteReceipt4.support).to.be.bignumber.equal(Enums.VoteType.Abstain);
-    expect(voteReceipt4.votes).to.be.bignumber.equal(web3.utils.toWei('2'));
-
-    expectEvent(txPropose, 'ProposalCreated', {
-      proposalId: this.proposal.id,
-      proposer,
-      targets: this.proposal.targets,
-      // values: this.proposal.values,
-      signatures: this.proposal.signatures.map(() => ''), // this event doesn't contain the proposal detail
-      calldatas: this.proposal.fulldata,
-      startBlock: new BN(txPropose.receipt.blockNumber).add(votingDelay),
-      endBlock: new BN(txPropose.receipt.blockNumber).add(votingDelay).add(votingPeriod),
-      description: this.proposal.description,
-    });
-    expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id });
-    await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled');
-  });
-
-  it('double voting is forbidden', async function () {
-    await this.helper.propose({ from: proposer });
-    await this.helper.waitForSnapshot();
-    await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-    await expectRevert(
-      this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
-      'GovernorCompatibilityBravo: vote already cast',
-    );
-  });
-
-  it('with function selector and arguments', async function () {
-    const target = this.receiver.address;
-    this.helper.setProposal(
-      [
-        { target, data: this.receiver.contract.methods.mockFunction().encodeABI() },
-        { target, data: this.receiver.contract.methods.mockFunctionWithArgs(17, 42).encodeABI() },
-        { target, signature: 'mockFunctionNonPayable()' },
-        {
-          target,
-          signature: 'mockFunctionWithArgs(uint256,uint256)',
-          data: web3.eth.abi.encodeParameters(['uint256', 'uint256'], [18, 43]),
-        },
-      ],
-      '<proposal description>',
-    );
-
-    await this.helper.propose({ from: proposer });
-    await this.helper.waitForSnapshot();
-    await this.helper.vote({ support: Enums.VoteType.For, reason: 'This is nice' }, { from: voter1 });
-    await this.helper.waitForDeadline();
-    await this.helper.queue();
-    await this.helper.waitForEta();
-    const txExecute = await this.helper.execute();
-
-    await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled');
-    await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled');
-    await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalledWithArgs', { a: '17', b: '42' });
-    await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalledWithArgs', { a: '18', b: '43' });
-  });
-
-  describe('should revert', function () {
-    describe('on propose', function () {
-      it('if proposal does not meet proposalThreshold', async function () {
-        await expectRevert(this.helper.propose({ from: other }), 'Governor: proposer votes below proposal threshold');
+  for (const { mode, Token } of TOKENS) {
+    describe(`using ${Token._json.contractName}`, function () {
+      beforeEach(async function () {
+        const [deployer] = await web3.eth.getAccounts();
+
+        this.token = await Token.new(tokenName, tokenSymbol, tokenName);
+
+        // 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,
+          votingDelay,
+          votingPeriod,
+          proposalThreshold,
+          this.timelock.address,
+          this.token.address,
+        );
+        this.receiver = await CallReceiver.new();
+
+        this.helper = new GovernorHelper(this.mock, mode);
+
+        await web3.eth.sendTransaction({ from: owner, to: this.timelock.address, value });
+
+        await this.token.$_mint(owner, tokenSupply);
+        await this.helper.delegate({ token: this.token, to: proposer, value: proposalThreshold }, { from: owner });
+        await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner });
+        await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner });
+        await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner });
+        await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner });
+
+        // default proposal
+        this.proposal = this.helper.setProposal(
+          [
+            {
+              target: this.receiver.address,
+              value,
+              signature: 'mockFunction()',
+            },
+          ],
+          '<proposal description>',
+        );
+      });
+
+      shouldBehaveLikeEIP6372(mode);
+
+      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(votingDelay);
+        expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod);
+        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');
+      });
+
+      it('nominal workflow', async function () {
+        // Before
+        expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false);
+        expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(false);
+        expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(false);
+        expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0');
+        expect(await web3.eth.getBalance(this.timelock.address)).to.be.bignumber.equal(value);
+        expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal('0');
+
+        // Run proposal
+        const txPropose = await this.helper.propose({ from: proposer });
+        await this.helper.waitForSnapshot();
+        await this.helper.vote({ support: Enums.VoteType.For, reason: 'This is nice' }, { from: voter1 });
+        await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 });
+        await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 });
+        await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 });
+        await this.helper.waitForDeadline();
+        await this.helper.queue();
+        await this.helper.waitForEta();
+        const txExecute = await this.helper.execute();
+
+        // After
+        expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false);
+        expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true);
+        expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true);
+        expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0');
+        expect(await web3.eth.getBalance(this.timelock.address)).to.be.bignumber.equal('0');
+        expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal(value);
+
+        const proposal = await this.mock.proposals(this.proposal.id);
+        expect(proposal.id).to.be.bignumber.equal(this.proposal.id);
+        expect(proposal.proposer).to.be.equal(proposer);
+        expect(proposal.eta).to.be.bignumber.equal(await this.mock.proposalEta(this.proposal.id));
+        expect(proposal.startBlock).to.be.bignumber.equal(await this.mock.proposalSnapshot(this.proposal.id));
+        expect(proposal.endBlock).to.be.bignumber.equal(await this.mock.proposalDeadline(this.proposal.id));
+        expect(proposal.canceled).to.be.equal(false);
+        expect(proposal.executed).to.be.equal(true);
+
+        const action = await this.mock.getActions(this.proposal.id);
+        expect(action.targets).to.be.deep.equal(this.proposal.targets);
+        // expect(action.values).to.be.deep.equal(this.proposal.values);
+        expect(action.signatures).to.be.deep.equal(this.proposal.signatures);
+        expect(action.calldatas).to.be.deep.equal(this.proposal.data);
+
+        const voteReceipt1 = await this.mock.getReceipt(this.proposal.id, voter1);
+        expect(voteReceipt1.hasVoted).to.be.equal(true);
+        expect(voteReceipt1.support).to.be.bignumber.equal(Enums.VoteType.For);
+        expect(voteReceipt1.votes).to.be.bignumber.equal(web3.utils.toWei('10'));
+
+        const voteReceipt2 = await this.mock.getReceipt(this.proposal.id, voter2);
+        expect(voteReceipt2.hasVoted).to.be.equal(true);
+        expect(voteReceipt2.support).to.be.bignumber.equal(Enums.VoteType.For);
+        expect(voteReceipt2.votes).to.be.bignumber.equal(web3.utils.toWei('7'));
+
+        const voteReceipt3 = await this.mock.getReceipt(this.proposal.id, voter3);
+        expect(voteReceipt3.hasVoted).to.be.equal(true);
+        expect(voteReceipt3.support).to.be.bignumber.equal(Enums.VoteType.Against);
+        expect(voteReceipt3.votes).to.be.bignumber.equal(web3.utils.toWei('5'));
+
+        const voteReceipt4 = await this.mock.getReceipt(this.proposal.id, voter4);
+        expect(voteReceipt4.hasVoted).to.be.equal(true);
+        expect(voteReceipt4.support).to.be.bignumber.equal(Enums.VoteType.Abstain);
+        expect(voteReceipt4.votes).to.be.bignumber.equal(web3.utils.toWei('2'));
+
+        expectEvent(txPropose, 'ProposalCreated', {
+          proposalId: this.proposal.id,
+          proposer,
+          targets: this.proposal.targets,
+          // values: this.proposal.values,
+          signatures: this.proposal.signatures.map(() => ''), // this event doesn't contain the proposal detail
+          calldatas: this.proposal.fulldata,
+          voteStart: web3.utils.toBN(await clockFromReceipt[mode](txPropose.receipt)).add(votingDelay),
+          voteEnd: web3.utils
+            .toBN(await clockFromReceipt[mode](txPropose.receipt))
+            .add(votingDelay)
+            .add(votingPeriod),
+          description: this.proposal.description,
+        });
+        expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id });
+        await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled');
       });
-    });
 
-    describe('on vote', function () {
-      it('if vote type is invalide', async function () {
+      it('double voting is forbidden', async function () {
         await this.helper.propose({ from: proposer });
         await this.helper.waitForSnapshot();
+        await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
         await expectRevert(
-          this.helper.vote({ support: 5 }, { from: voter1 }),
-          'GovernorCompatibilityBravo: invalid vote type',
+          this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
+          'GovernorCompatibilityBravo: vote already cast',
         );
       });
-    });
-  });
 
-  describe('cancel', function () {
-    it('proposer can cancel', async function () {
-      await this.helper.propose({ from: proposer });
-      await this.helper.cancel('external', { from: proposer });
-    });
+      it('with function selector and arguments', async function () {
+        const target = this.receiver.address;
+        this.helper.setProposal(
+          [
+            { target, data: this.receiver.contract.methods.mockFunction().encodeABI() },
+            { target, data: this.receiver.contract.methods.mockFunctionWithArgs(17, 42).encodeABI() },
+            { target, signature: 'mockFunctionNonPayable()' },
+            {
+              target,
+              signature: 'mockFunctionWithArgs(uint256,uint256)',
+              data: web3.eth.abi.encodeParameters(['uint256', 'uint256'], [18, 43]),
+            },
+          ],
+          '<proposal description>',
+        );
 
-    it('anyone can cancel if proposer drop below threshold', async function () {
-      await this.helper.propose({ from: proposer });
-      await this.token.transfer(voter1, web3.utils.toWei('1'), { from: proposer });
-      await this.helper.cancel('external');
-    });
+        await this.helper.propose({ from: proposer });
+        await this.helper.waitForSnapshot();
+        await this.helper.vote({ support: Enums.VoteType.For, reason: 'This is nice' }, { from: voter1 });
+        await this.helper.waitForDeadline();
+        await this.helper.queue();
+        await this.helper.waitForEta();
+        const txExecute = await this.helper.execute();
+
+        await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled');
+        await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled');
+        await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalledWithArgs', {
+          a: '17',
+          b: '42',
+        });
+        await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalledWithArgs', {
+          a: '18',
+          b: '43',
+        });
+      });
 
-    it('cannot cancel is proposer is still above threshold', async function () {
-      await this.helper.propose({ from: proposer });
-      await expectRevert(this.helper.cancel('external'), 'GovernorBravo: proposer above threshold');
+      describe('should revert', function () {
+        describe('on propose', function () {
+          it('if proposal does not meet proposalThreshold', async function () {
+            await expectRevert(
+              this.helper.propose({ from: other }),
+              'Governor: proposer votes below proposal threshold',
+            );
+          });
+        });
+
+        describe('on vote', function () {
+          it('if vote type is invalide', async function () {
+            await this.helper.propose({ from: proposer });
+            await this.helper.waitForSnapshot();
+            await expectRevert(
+              this.helper.vote({ support: 5 }, { from: voter1 }),
+              'GovernorCompatibilityBravo: invalid vote type',
+            );
+          });
+        });
+      });
+
+      describe('cancel', function () {
+        it('proposer can cancel', async function () {
+          await this.helper.propose({ from: proposer });
+          await this.helper.cancel('external', { from: proposer });
+        });
+
+        it('anyone can cancel if proposer drop below threshold', async function () {
+          await this.helper.propose({ from: proposer });
+          await this.token.transfer(voter1, web3.utils.toWei('1'), { from: proposer });
+          await this.helper.cancel('external');
+        });
+
+        it('cannot cancel is proposer is still above threshold', async function () {
+          await this.helper.propose({ from: proposer });
+          await expectRevert(this.helper.cancel('external'), 'GovernorBravo: proposer above threshold');
+        });
+      });
     });
-  });
+  }
 });

+ 61 - 54
test/governance/extensions/GovernorComp.test.js

@@ -1,12 +1,15 @@
-const { BN } = require('@openzeppelin/test-helpers');
 const { expect } = require('chai');
 const Enums = require('../../helpers/enums');
 const { GovernorHelper } = require('../../helpers/governance');
 
-const Token = artifacts.require('$ERC20VotesComp');
 const Governor = artifacts.require('$GovernorCompMock');
 const CallReceiver = artifacts.require('CallReceiverMock');
 
+const TOKENS = [
+  { Token: artifacts.require('$ERC20VotesComp'), mode: 'blocknumber' },
+  { Token: artifacts.require('$ERC20VotesCompTimestampMock'), mode: 'timestamp' },
+];
+
 contract('GovernorComp', function (accounts) {
   const [owner, voter1, voter2, voter3, voter4] = accounts;
 
@@ -15,67 +18,71 @@ contract('GovernorComp', function (accounts) {
   const tokenName = 'MockToken';
   const tokenSymbol = 'MTKN';
   const tokenSupply = web3.utils.toWei('100');
-  const votingDelay = new BN(4);
-  const votingPeriod = new BN(16);
+  const votingDelay = web3.utils.toBN(4);
+  const votingPeriod = web3.utils.toBN(16);
   const value = web3.utils.toWei('1');
 
-  beforeEach(async function () {
-    this.owner = owner;
-    this.token = await Token.new(tokenName, tokenSymbol, tokenName);
-    this.mock = await Governor.new(name, this.token.address);
-    this.receiver = await CallReceiver.new();
+  for (const { mode, Token } of TOKENS) {
+    describe(`using ${Token._json.contractName}`, function () {
+      beforeEach(async function () {
+        this.owner = owner;
+        this.token = await Token.new(tokenName, tokenSymbol, tokenName);
+        this.mock = await Governor.new(name, this.token.address);
+        this.receiver = await CallReceiver.new();
 
-    this.helper = new GovernorHelper(this.mock);
+        this.helper = new GovernorHelper(this.mock, mode);
 
-    await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value });
+        await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value });
 
-    await this.token.$_mint(owner, tokenSupply);
-    await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner });
-    await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner });
-    await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner });
-    await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner });
+        await this.token.$_mint(owner, tokenSupply);
+        await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner });
+        await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner });
+        await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner });
+        await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner });
 
-    // default proposal
-    this.proposal = this.helper.setProposal(
-      [
-        {
-          target: this.receiver.address,
-          value,
-          data: this.receiver.contract.methods.mockFunction().encodeABI(),
-        },
-      ],
-      '<proposal description>',
-    );
-  });
+        // default proposal
+        this.proposal = this.helper.setProposal(
+          [
+            {
+              target: this.receiver.address,
+              value,
+              data: this.receiver.contract.methods.mockFunction().encodeABI(),
+            },
+          ],
+          '<proposal description>',
+        );
+      });
 
-  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(votingDelay);
-    expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod);
-    expect(await this.mock.quorum(0)).to.be.bignumber.equal('0');
-  });
+      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(votingDelay);
+        expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod);
+        expect(await this.mock.quorum(0)).to.be.bignumber.equal('0');
+      });
 
-  it('voting with comp token', async function () {
-    await this.helper.propose();
-    await this.helper.waitForSnapshot();
-    await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-    await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 });
-    await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 });
-    await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 });
-    await this.helper.waitForDeadline();
-    await this.helper.execute();
+      it('voting with comp token', async function () {
+        await this.helper.propose();
+        await this.helper.waitForSnapshot();
+        await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+        await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 });
+        await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 });
+        await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 });
+        await this.helper.waitForDeadline();
+        await this.helper.execute();
 
-    expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false);
-    expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true);
-    expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true);
-    expect(await this.mock.hasVoted(this.proposal.id, voter3)).to.be.equal(true);
-    expect(await this.mock.hasVoted(this.proposal.id, voter4)).to.be.equal(true);
+        expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false);
+        expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true);
+        expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true);
+        expect(await this.mock.hasVoted(this.proposal.id, voter3)).to.be.equal(true);
+        expect(await this.mock.hasVoted(this.proposal.id, voter4)).to.be.equal(true);
 
-    await this.mock.proposalVotes(this.proposal.id).then(results => {
-      expect(results.forVotes).to.be.bignumber.equal(web3.utils.toWei('17'));
-      expect(results.againstVotes).to.be.bignumber.equal(web3.utils.toWei('5'));
-      expect(results.abstainVotes).to.be.bignumber.equal(web3.utils.toWei('2'));
+        await this.mock.proposalVotes(this.proposal.id).then(results => {
+          expect(results.forVotes).to.be.bignumber.equal(web3.utils.toWei('17'));
+          expect(results.againstVotes).to.be.bignumber.equal(web3.utils.toWei('5'));
+          expect(results.abstainVotes).to.be.bignumber.equal(web3.utils.toWei('2'));
+        });
+      });
     });
-  });
+  }
 });

+ 97 - 89
test/governance/extensions/GovernorERC721.test.js

@@ -1,12 +1,16 @@
-const { BN, expectEvent } = require('@openzeppelin/test-helpers');
+const { expectEvent } = require('@openzeppelin/test-helpers');
 const { expect } = require('chai');
 const Enums = require('../../helpers/enums');
 const { GovernorHelper } = require('../../helpers/governance');
 
-const Token = artifacts.require('$ERC721Votes');
 const Governor = artifacts.require('$GovernorVoteMocks');
 const CallReceiver = artifacts.require('CallReceiverMock');
 
+const TOKENS = [
+  { Token: artifacts.require('$ERC721Votes'), mode: 'blocknumber' },
+  { Token: artifacts.require('$ERC721VotesTimestampMock'), mode: 'timestamp' },
+];
+
 contract('GovernorERC721', function (accounts) {
   const [owner, voter1, voter2, voter3, voter4] = accounts;
 
@@ -14,94 +18,98 @@ contract('GovernorERC721', function (accounts) {
   // const version = '1';
   const tokenName = 'MockNFToken';
   const tokenSymbol = 'MTKN';
-  const NFT0 = new BN(0);
-  const NFT1 = new BN(1);
-  const NFT2 = new BN(2);
-  const NFT3 = new BN(3);
-  const NFT4 = new BN(4);
-  const votingDelay = new BN(4);
-  const votingPeriod = new BN(16);
+  const NFT0 = web3.utils.toBN(0);
+  const NFT1 = web3.utils.toBN(1);
+  const NFT2 = web3.utils.toBN(2);
+  const NFT3 = web3.utils.toBN(3);
+  const NFT4 = web3.utils.toBN(4);
+  const votingDelay = web3.utils.toBN(4);
+  const votingPeriod = web3.utils.toBN(16);
   const value = web3.utils.toWei('1');
 
-  beforeEach(async function () {
-    this.owner = owner;
-    this.token = await Token.new(tokenName, tokenSymbol, tokenName, '1');
-    this.mock = await Governor.new(name, this.token.address);
-    this.receiver = await CallReceiver.new();
-
-    this.helper = new GovernorHelper(this.mock);
-
-    await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value });
-
-    await Promise.all([NFT0, NFT1, NFT2, NFT3, NFT4].map(tokenId => this.token.$_mint(owner, tokenId)));
-    await this.helper.delegate({ token: this.token, to: voter1, tokenId: NFT0 }, { from: owner });
-    await this.helper.delegate({ token: this.token, to: voter2, tokenId: NFT1 }, { from: owner });
-    await this.helper.delegate({ token: this.token, to: voter2, tokenId: NFT2 }, { from: owner });
-    await this.helper.delegate({ token: this.token, to: voter3, tokenId: NFT3 }, { from: owner });
-    await this.helper.delegate({ token: this.token, to: voter4, tokenId: NFT4 }, { from: owner });
-
-    // default proposal
-    this.proposal = this.helper.setProposal(
-      [
-        {
-          target: this.receiver.address,
-          value,
-          data: this.receiver.contract.methods.mockFunction().encodeABI(),
-        },
-      ],
-      '<proposal description>',
-    );
-  });
-
-  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(votingDelay);
-    expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod);
-    expect(await this.mock.quorum(0)).to.be.bignumber.equal('0');
-  });
-
-  it('voting with ERC721 token', async function () {
-    await this.helper.propose();
-    await this.helper.waitForSnapshot();
-
-    expectEvent(await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), 'VoteCast', {
-      voter: voter1,
-      support: Enums.VoteType.For,
-      weight: '1',
-    });
-
-    expectEvent(await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }), 'VoteCast', {
-      voter: voter2,
-      support: Enums.VoteType.For,
-      weight: '2',
-    });
-
-    expectEvent(await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }), 'VoteCast', {
-      voter: voter3,
-      support: Enums.VoteType.Against,
-      weight: '1',
-    });
-
-    expectEvent(await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }), 'VoteCast', {
-      voter: voter4,
-      support: Enums.VoteType.Abstain,
-      weight: '1',
-    });
-
-    await this.helper.waitForDeadline();
-    await this.helper.execute();
-
-    expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false);
-    expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true);
-    expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true);
-    expect(await this.mock.hasVoted(this.proposal.id, voter3)).to.be.equal(true);
-    expect(await this.mock.hasVoted(this.proposal.id, voter4)).to.be.equal(true);
-
-    await this.mock.proposalVotes(this.proposal.id).then(results => {
-      expect(results.forVotes).to.be.bignumber.equal('3');
-      expect(results.againstVotes).to.be.bignumber.equal('1');
-      expect(results.abstainVotes).to.be.bignumber.equal('1');
+  for (const { mode, Token } of TOKENS) {
+    describe(`using ${Token._json.contractName}`, function () {
+      beforeEach(async function () {
+        this.owner = owner;
+        this.token = await Token.new(tokenName, tokenSymbol, tokenName, '1');
+        this.mock = await Governor.new(name, this.token.address);
+        this.receiver = await CallReceiver.new();
+
+        this.helper = new GovernorHelper(this.mock, mode);
+
+        await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value });
+
+        await Promise.all([NFT0, NFT1, NFT2, NFT3, NFT4].map(tokenId => this.token.$_mint(owner, tokenId)));
+        await this.helper.delegate({ token: this.token, to: voter1, tokenId: NFT0 }, { from: owner });
+        await this.helper.delegate({ token: this.token, to: voter2, tokenId: NFT1 }, { from: owner });
+        await this.helper.delegate({ token: this.token, to: voter2, tokenId: NFT2 }, { from: owner });
+        await this.helper.delegate({ token: this.token, to: voter3, tokenId: NFT3 }, { from: owner });
+        await this.helper.delegate({ token: this.token, to: voter4, tokenId: NFT4 }, { from: owner });
+
+        // default proposal
+        this.proposal = this.helper.setProposal(
+          [
+            {
+              target: this.receiver.address,
+              value,
+              data: this.receiver.contract.methods.mockFunction().encodeABI(),
+            },
+          ],
+          '<proposal description>',
+        );
+      });
+
+      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(votingDelay);
+        expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod);
+        expect(await this.mock.quorum(0)).to.be.bignumber.equal('0');
+      });
+
+      it('voting with ERC721 token', async function () {
+        await this.helper.propose();
+        await this.helper.waitForSnapshot();
+
+        expectEvent(await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), 'VoteCast', {
+          voter: voter1,
+          support: Enums.VoteType.For,
+          weight: '1',
+        });
+
+        expectEvent(await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }), 'VoteCast', {
+          voter: voter2,
+          support: Enums.VoteType.For,
+          weight: '2',
+        });
+
+        expectEvent(await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }), 'VoteCast', {
+          voter: voter3,
+          support: Enums.VoteType.Against,
+          weight: '1',
+        });
+
+        expectEvent(await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }), 'VoteCast', {
+          voter: voter4,
+          support: Enums.VoteType.Abstain,
+          weight: '1',
+        });
+
+        await this.helper.waitForDeadline();
+        await this.helper.execute();
+
+        expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false);
+        expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true);
+        expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true);
+        expect(await this.mock.hasVoted(this.proposal.id, voter3)).to.be.equal(true);
+        expect(await this.mock.hasVoted(this.proposal.id, voter4)).to.be.equal(true);
+
+        await this.mock.proposalVotes(this.proposal.id).then(results => {
+          expect(results.forVotes).to.be.bignumber.equal('3');
+          expect(results.againstVotes).to.be.bignumber.equal('1');
+          expect(results.abstainVotes).to.be.bignumber.equal('1');
+        });
+      });
     });
-  });
+  }
 });

+ 164 - 147
test/governance/extensions/GovernorPreventLateQuorum.test.js

@@ -1,12 +1,17 @@
-const { BN, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
+const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
 const { expect } = require('chai');
 const Enums = require('../../helpers/enums');
 const { GovernorHelper } = require('../../helpers/governance');
+const { clockFromReceipt } = require('../../helpers/time');
 
-const Token = artifacts.require('$ERC20VotesComp');
 const Governor = artifacts.require('$GovernorPreventLateQuorumMock');
 const CallReceiver = artifacts.require('CallReceiverMock');
 
+const TOKENS = [
+  { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' },
+  { Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' },
+];
+
 contract('GovernorPreventLateQuorum', function (accounts) {
   const [owner, proposer, voter1, voter2, voter3, voter4] = accounts;
 
@@ -15,158 +20,170 @@ contract('GovernorPreventLateQuorum', function (accounts) {
   const tokenName = 'MockToken';
   const tokenSymbol = 'MTKN';
   const tokenSupply = web3.utils.toWei('100');
-  const votingDelay = new BN(4);
-  const votingPeriod = new BN(16);
-  const lateQuorumVoteExtension = new BN(8);
+  const votingDelay = web3.utils.toBN(4);
+  const votingPeriod = web3.utils.toBN(16);
+  const lateQuorumVoteExtension = web3.utils.toBN(8);
   const quorum = web3.utils.toWei('1');
   const value = web3.utils.toWei('1');
 
-  beforeEach(async function () {
-    this.owner = owner;
-    this.token = await Token.new(tokenName, tokenSymbol, tokenName);
-    this.mock = await Governor.new(
-      name,
-      votingDelay,
-      votingPeriod,
-      0,
-      this.token.address,
-      lateQuorumVoteExtension,
-      quorum,
-    );
-    this.receiver = await CallReceiver.new();
-
-    this.helper = new GovernorHelper(this.mock);
-
-    await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value });
-
-    await this.token.$_mint(owner, tokenSupply);
-    await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner });
-    await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner });
-    await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner });
-    await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner });
-
-    // default proposal
-    this.proposal = this.helper.setProposal(
-      [
-        {
-          target: this.receiver.address,
-          value,
-          data: this.receiver.contract.methods.mockFunction().encodeABI(),
-        },
-      ],
-      '<proposal description>',
-    );
-  });
-
-  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(votingDelay);
-    expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod);
-    expect(await this.mock.quorum(0)).to.be.bignumber.equal(quorum);
-    expect(await this.mock.lateQuorumVoteExtension()).to.be.bignumber.equal(lateQuorumVoteExtension);
-  });
-
-  it('nominal workflow unaffected', async function () {
-    const txPropose = await this.helper.propose({ from: proposer });
-    await this.helper.waitForSnapshot();
-    await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-    await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 });
-    await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 });
-    await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 });
-    await this.helper.waitForDeadline();
-    await this.helper.execute();
-
-    expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false);
-    expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true);
-    expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true);
-    expect(await this.mock.hasVoted(this.proposal.id, voter3)).to.be.equal(true);
-    expect(await this.mock.hasVoted(this.proposal.id, voter4)).to.be.equal(true);
-
-    await this.mock.proposalVotes(this.proposal.id).then(results => {
-      expect(results.forVotes).to.be.bignumber.equal(web3.utils.toWei('17'));
-      expect(results.againstVotes).to.be.bignumber.equal(web3.utils.toWei('5'));
-      expect(results.abstainVotes).to.be.bignumber.equal(web3.utils.toWei('2'));
-    });
-
-    const startBlock = new BN(txPropose.receipt.blockNumber).add(votingDelay);
-    const endBlock = new BN(txPropose.receipt.blockNumber).add(votingDelay).add(votingPeriod);
-    expect(await this.mock.proposalSnapshot(this.proposal.id)).to.be.bignumber.equal(startBlock);
-    expect(await this.mock.proposalDeadline(this.proposal.id)).to.be.bignumber.equal(endBlock);
-
-    expectEvent(txPropose, 'ProposalCreated', {
-      proposalId: this.proposal.id,
-      proposer,
-      targets: this.proposal.targets,
-      // values: this.proposal.values.map(value => new BN(value)),
-      signatures: this.proposal.signatures,
-      calldatas: this.proposal.data,
-      startBlock,
-      endBlock,
-      description: this.proposal.description,
-    });
-  });
-
-  it('Delay is extended to prevent last minute take-over', async function () {
-    const txPropose = await this.helper.propose({ from: proposer });
-
-    // compute original schedule
-    const startBlock = new BN(txPropose.receipt.blockNumber).add(votingDelay);
-    const endBlock = new BN(txPropose.receipt.blockNumber).add(votingDelay).add(votingPeriod);
-    expect(await this.mock.proposalSnapshot(this.proposal.id)).to.be.bignumber.equal(startBlock);
-    expect(await this.mock.proposalDeadline(this.proposal.id)).to.be.bignumber.equal(endBlock);
-
-    // wait for the last minute to vote
-    await this.helper.waitForDeadline(-1);
-    const txVote = await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 });
-
-    // cannot execute yet
-    expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active);
-
-    // compute new extended schedule
-    const extendedDeadline = new BN(txVote.receipt.blockNumber).add(lateQuorumVoteExtension);
-    expect(await this.mock.proposalSnapshot(this.proposal.id)).to.be.bignumber.equal(startBlock);
-    expect(await this.mock.proposalDeadline(this.proposal.id)).to.be.bignumber.equal(extendedDeadline);
-
-    // still possible to vote
-    await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter1 });
-
-    await this.helper.waitForDeadline();
-    expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active);
-    await this.helper.waitForDeadline(+1);
-    expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Defeated);
+  for (const { mode, Token } of TOKENS) {
+    describe(`using ${Token._json.contractName}`, function () {
+      beforeEach(async function () {
+        this.owner = owner;
+        this.token = await Token.new(tokenName, tokenSymbol, tokenName);
+        this.mock = await Governor.new(
+          name,
+          votingDelay,
+          votingPeriod,
+          0,
+          this.token.address,
+          lateQuorumVoteExtension,
+          quorum,
+        );
+        this.receiver = await CallReceiver.new();
+
+        this.helper = new GovernorHelper(this.mock, mode);
+
+        await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value });
+
+        await this.token.$_mint(owner, tokenSupply);
+        await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner });
+        await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner });
+        await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner });
+        await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner });
+
+        // default proposal
+        this.proposal = this.helper.setProposal(
+          [
+            {
+              target: this.receiver.address,
+              value,
+              data: this.receiver.contract.methods.mockFunction().encodeABI(),
+            },
+          ],
+          '<proposal description>',
+        );
+      });
 
-    // check extension event
-    expectEvent(txVote, 'ProposalExtended', { proposalId: this.proposal.id, extendedDeadline });
-  });
+      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(votingDelay);
+        expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod);
+        expect(await this.mock.quorum(0)).to.be.bignumber.equal(quorum);
+        expect(await this.mock.lateQuorumVoteExtension()).to.be.bignumber.equal(lateQuorumVoteExtension);
+      });
 
-  describe('onlyGovernance updates', function () {
-    it('setLateQuorumVoteExtension is protected', async function () {
-      await expectRevert(this.mock.setLateQuorumVoteExtension(0), 'Governor: onlyGovernance');
-    });
+      it('nominal workflow unaffected', async function () {
+        const txPropose = await this.helper.propose({ from: proposer });
+        await this.helper.waitForSnapshot();
+        await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+        await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 });
+        await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 });
+        await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 });
+        await this.helper.waitForDeadline();
+        await this.helper.execute();
+
+        expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false);
+        expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true);
+        expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true);
+        expect(await this.mock.hasVoted(this.proposal.id, voter3)).to.be.equal(true);
+        expect(await this.mock.hasVoted(this.proposal.id, voter4)).to.be.equal(true);
+
+        await this.mock.proposalVotes(this.proposal.id).then(results => {
+          expect(results.forVotes).to.be.bignumber.equal(web3.utils.toWei('17'));
+          expect(results.againstVotes).to.be.bignumber.equal(web3.utils.toWei('5'));
+          expect(results.abstainVotes).to.be.bignumber.equal(web3.utils.toWei('2'));
+        });
+
+        const voteStart = web3.utils.toBN(await clockFromReceipt[mode](txPropose.receipt)).add(votingDelay);
+        const voteEnd = web3.utils
+          .toBN(await clockFromReceipt[mode](txPropose.receipt))
+          .add(votingDelay)
+          .add(votingPeriod);
+        expect(await this.mock.proposalSnapshot(this.proposal.id)).to.be.bignumber.equal(voteStart);
+        expect(await this.mock.proposalDeadline(this.proposal.id)).to.be.bignumber.equal(voteEnd);
+
+        expectEvent(txPropose, 'ProposalCreated', {
+          proposalId: this.proposal.id,
+          proposer,
+          targets: this.proposal.targets,
+          // values: this.proposal.values.map(value => web3.utils.toBN(value)),
+          signatures: this.proposal.signatures,
+          calldatas: this.proposal.data,
+          voteStart,
+          voteEnd,
+          description: this.proposal.description,
+        });
+      });
 
-    it('can setLateQuorumVoteExtension through governance', async function () {
-      this.helper.setProposal(
-        [
-          {
-            target: this.mock.address,
-            data: this.mock.contract.methods.setLateQuorumVoteExtension('0').encodeABI(),
-          },
-        ],
-        '<proposal description>',
-      );
-
-      await this.helper.propose();
-      await this.helper.waitForSnapshot();
-      await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-      await this.helper.waitForDeadline();
-
-      expectEvent(await this.helper.execute(), 'LateQuorumVoteExtensionSet', {
-        oldVoteExtension: lateQuorumVoteExtension,
-        newVoteExtension: '0',
+      it('Delay is extended to prevent last minute take-over', async function () {
+        const txPropose = await this.helper.propose({ from: proposer });
+
+        // compute original schedule
+        const startBlock = web3.utils.toBN(await clockFromReceipt[mode](txPropose.receipt)).add(votingDelay);
+        const endBlock = web3.utils
+          .toBN(await clockFromReceipt[mode](txPropose.receipt))
+          .add(votingDelay)
+          .add(votingPeriod);
+        expect(await this.mock.proposalSnapshot(this.proposal.id)).to.be.bignumber.equal(startBlock);
+        expect(await this.mock.proposalDeadline(this.proposal.id)).to.be.bignumber.equal(endBlock);
+
+        // wait for the last minute to vote
+        await this.helper.waitForDeadline(-1);
+        const txVote = await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 });
+
+        // cannot execute yet
+        expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active);
+
+        // compute new extended schedule
+        const extendedDeadline = web3.utils
+          .toBN(await clockFromReceipt[mode](txVote.receipt))
+          .add(lateQuorumVoteExtension);
+        expect(await this.mock.proposalSnapshot(this.proposal.id)).to.be.bignumber.equal(startBlock);
+        expect(await this.mock.proposalDeadline(this.proposal.id)).to.be.bignumber.equal(extendedDeadline);
+
+        // still possible to vote
+        await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter1 });
+
+        await this.helper.waitForDeadline();
+        expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active);
+        await this.helper.waitForDeadline(+1);
+        expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Defeated);
+
+        // check extension event
+        expectEvent(txVote, 'ProposalExtended', { proposalId: this.proposal.id, extendedDeadline });
       });
 
-      expect(await this.mock.lateQuorumVoteExtension()).to.be.bignumber.equal('0');
+      describe('onlyGovernance updates', function () {
+        it('setLateQuorumVoteExtension is protected', async function () {
+          await expectRevert(this.mock.setLateQuorumVoteExtension(0), 'Governor: onlyGovernance');
+        });
+
+        it('can setLateQuorumVoteExtension through governance', async function () {
+          this.helper.setProposal(
+            [
+              {
+                target: this.mock.address,
+                data: this.mock.contract.methods.setLateQuorumVoteExtension('0').encodeABI(),
+              },
+            ],
+            '<proposal description>',
+          );
+
+          await this.helper.propose();
+          await this.helper.waitForSnapshot();
+          await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+          await this.helper.waitForDeadline();
+
+          expectEvent(await this.helper.execute(), 'LateQuorumVoteExtensionSet', {
+            oldVoteExtension: lateQuorumVoteExtension,
+            newVoteExtension: '0',
+          });
+
+          expect(await this.mock.lateQuorumVoteExtension()).to.be.bignumber.equal('0');
+        });
+      });
     });
-  });
+  }
 });

+ 285 - 266
test/governance/extensions/GovernorTimelockCompound.test.js

@@ -1,4 +1,4 @@
-const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
+const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
 const { expect } = require('chai');
 const RLP = require('rlp');
 const Enums = require('../../helpers/enums');
@@ -6,7 +6,6 @@ const { GovernorHelper } = require('../../helpers/governance');
 
 const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsInterface.behavior');
 
-const Token = artifacts.require('$ERC20Votes');
 const Timelock = artifacts.require('CompTimelock');
 const Governor = artifacts.require('$GovernorTimelockCompoundMock');
 const CallReceiver = artifacts.require('CallReceiverMock');
@@ -20,6 +19,11 @@ function makeContractAddress(creator, nonce) {
   );
 }
 
+const TOKENS = [
+  { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' },
+  { Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' },
+];
+
 contract('GovernorTimelockCompound', function (accounts) {
   const [owner, voter1, voter2, voter3, voter4, other] = accounts;
 
@@ -28,306 +32,321 @@ contract('GovernorTimelockCompound', function (accounts) {
   const tokenName = 'MockToken';
   const tokenSymbol = 'MTKN';
   const tokenSupply = web3.utils.toWei('100');
-  const votingDelay = new BN(4);
-  const votingPeriod = new BN(16);
+  const votingDelay = web3.utils.toBN(4);
+  const votingPeriod = web3.utils.toBN(16);
   const value = web3.utils.toWei('1');
 
-  beforeEach(async function () {
-    const [deployer] = await web3.eth.getAccounts();
-
-    this.token = await Token.new(tokenName, tokenSymbol, tokenName);
-
-    // 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, votingDelay, votingPeriod, 0, this.timelock.address, this.token.address, 0);
-    this.receiver = await CallReceiver.new();
-
-    this.helper = new GovernorHelper(this.mock);
-
-    await web3.eth.sendTransaction({ from: owner, to: this.timelock.address, value });
-
-    await this.token.$_mint(owner, tokenSupply);
-    await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner });
-    await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner });
-    await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner });
-    await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner });
-
-    // default proposal
-    this.proposal = this.helper.setProposal(
-      [
-        {
-          target: this.receiver.address,
-          value,
-          data: this.receiver.contract.methods.mockFunction().encodeABI(),
-        },
-      ],
-      '<proposal description>',
-    );
-  });
-
-  shouldSupportInterfaces(['ERC165', 'Governor', 'GovernorWithParams', 'GovernorTimelock']);
-
-  it("doesn't accept ether transfers", async function () {
-    await expectRevert.unspecified(web3.eth.sendTransaction({ from: owner, to: this.mock.address, value: 1 }));
-  });
-
-  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(votingDelay);
-    expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod);
-    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);
-  });
-
-  it('nominal', async function () {
-    await this.helper.propose();
-    await this.helper.waitForSnapshot();
-    await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-    await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 });
-    await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 });
-    await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 });
-    await this.helper.waitForDeadline();
-    const txQueue = await this.helper.queue();
-    const eta = await this.mock.proposalEta(this.proposal.id);
-    await this.helper.waitForEta();
-    const txExecute = await this.helper.execute();
-
-    expectEvent(txQueue, 'ProposalQueued', { proposalId: this.proposal.id });
-    await expectEvent.inTransaction(txQueue.tx, this.timelock, 'QueueTransaction', { eta });
-
-    expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id });
-    await expectEvent.inTransaction(txExecute.tx, this.timelock, 'ExecuteTransaction', { eta });
-    await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled');
-  });
-
-  describe('should revert', function () {
-    describe('on queue', function () {
-      it('if already queued', async function () {
-        await this.helper.propose();
-        await this.helper.waitForSnapshot();
-        await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-        await this.helper.waitForDeadline();
-        await this.helper.queue();
-        await expectRevert(this.helper.queue(), 'Governor: proposal not successful');
-      });
+  for (const { mode, Token } of TOKENS) {
+    describe(`using ${Token._json.contractName}`, function () {
+      beforeEach(async function () {
+        const [deployer] = await web3.eth.getAccounts();
+
+        this.token = await Token.new(tokenName, tokenSymbol, tokenName);
+
+        // 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,
+          votingDelay,
+          votingPeriod,
+          0,
+          this.timelock.address,
+          this.token.address,
+          0,
+        );
+        this.receiver = await CallReceiver.new();
 
-      it('if proposal contains duplicate calls', async function () {
-        const action = {
-          target: this.token.address,
-          data: this.token.contract.methods.approve(this.receiver.address, constants.MAX_UINT256).encodeABI(),
-        };
-        this.helper.setProposal([action, action], '<proposal description>');
+        this.helper = new GovernorHelper(this.mock, mode);
 
-        await this.helper.propose();
-        await this.helper.waitForSnapshot();
-        await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-        await this.helper.waitForDeadline();
-        await expectRevert(this.helper.queue(), 'GovernorTimelockCompound: identical proposal action already queued');
-        await expectRevert(this.helper.execute(), 'GovernorTimelockCompound: proposal not yet queued');
-      });
-    });
-
-    describe('on execute', function () {
-      it('if not queued', async function () {
-        await this.helper.propose();
-        await this.helper.waitForSnapshot();
-        await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-        await this.helper.waitForDeadline(+1);
+        await web3.eth.sendTransaction({ from: owner, to: this.timelock.address, value });
 
-        expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded);
+        await this.token.$_mint(owner, tokenSupply);
+        await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner });
+        await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner });
+        await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner });
+        await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner });
 
-        await expectRevert(this.helper.execute(), 'GovernorTimelockCompound: proposal not yet queued');
+        // default proposal
+        this.proposal = this.helper.setProposal(
+          [
+            {
+              target: this.receiver.address,
+              value,
+              data: this.receiver.contract.methods.mockFunction().encodeABI(),
+            },
+          ],
+          '<proposal description>',
+        );
       });
 
-      it('if too early', async function () {
-        await this.helper.propose();
-        await this.helper.waitForSnapshot();
-        await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-        await this.helper.waitForDeadline();
-        await this.helper.queue();
-
-        expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Queued);
+      shouldSupportInterfaces(['ERC165', 'Governor', 'GovernorWithParams', 'GovernorTimelock']);
 
-        await expectRevert(
-          this.helper.execute(),
-          "Timelock::executeTransaction: Transaction hasn't surpassed time lock",
-        );
+      it("doesn't accept ether transfers", async function () {
+        await expectRevert.unspecified(web3.eth.sendTransaction({ from: owner, to: this.mock.address, value: 1 }));
       });
 
-      it('if too late', async function () {
-        await this.helper.propose();
-        await this.helper.waitForSnapshot();
-        await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-        await this.helper.waitForDeadline();
-        await this.helper.queue();
-        await this.helper.waitForEta(+30 * 86400);
-
-        expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Expired);
+      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(votingDelay);
+        expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod);
+        expect(await this.mock.quorum(0)).to.be.bignumber.equal('0');
 
-        await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
+        expect(await this.mock.timelock()).to.be.equal(this.timelock.address);
+        expect(await this.timelock.admin()).to.be.equal(this.mock.address);
       });
 
-      it('if already executed', async function () {
+      it('nominal', async function () {
         await this.helper.propose();
         await this.helper.waitForSnapshot();
         await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+        await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 });
+        await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 });
+        await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 });
         await this.helper.waitForDeadline();
-        await this.helper.queue();
+        const txQueue = await this.helper.queue();
+        const eta = await this.mock.proposalEta(this.proposal.id);
         await this.helper.waitForEta();
-        await this.helper.execute();
-        await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
-      });
-    });
-  });
-
-  describe('cancel', function () {
-    it('cancel before queue prevents scheduling', async function () {
-      await this.helper.propose();
-      await this.helper.waitForSnapshot();
-      await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-      await this.helper.waitForDeadline();
-
-      expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: this.proposal.id });
-
-      expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
-      await expectRevert(this.helper.queue(), 'Governor: proposal not successful');
-    });
+        const txExecute = await this.helper.execute();
 
-    it('cancel after queue prevents executing', async function () {
-      await this.helper.propose();
-      await this.helper.waitForSnapshot();
-      await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-      await this.helper.waitForDeadline();
-      await this.helper.queue();
+        expectEvent(txQueue, 'ProposalQueued', { proposalId: this.proposal.id });
+        await expectEvent.inTransaction(txQueue.tx, this.timelock, 'QueueTransaction', { eta });
 
-      expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: this.proposal.id });
+        expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id });
+        await expectEvent.inTransaction(txExecute.tx, this.timelock, 'ExecuteTransaction', { eta });
+        await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled');
+      });
 
-      expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
-      await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
-    });
-  });
+      describe('should revert', function () {
+        describe('on queue', function () {
+          it('if already queued', async function () {
+            await this.helper.propose();
+            await this.helper.waitForSnapshot();
+            await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+            await this.helper.waitForDeadline();
+            await this.helper.queue();
+            await expectRevert(this.helper.queue(), 'Governor: proposal not successful');
+          });
+
+          it('if proposal contains duplicate calls', async function () {
+            const action = {
+              target: this.token.address,
+              data: this.token.contract.methods.approve(this.receiver.address, constants.MAX_UINT256).encodeABI(),
+            };
+            this.helper.setProposal([action, action], '<proposal description>');
+
+            await this.helper.propose();
+            await this.helper.waitForSnapshot();
+            await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+            await this.helper.waitForDeadline();
+            await expectRevert(
+              this.helper.queue(),
+              'GovernorTimelockCompound: identical proposal action already queued',
+            );
+            await expectRevert(this.helper.execute(), 'GovernorTimelockCompound: proposal not yet queued');
+          });
+        });
 
-  describe('onlyGovernance', function () {
-    describe('relay', function () {
-      beforeEach(async function () {
-        await this.token.$_mint(this.mock.address, 1);
+        describe('on execute', function () {
+          it('if not queued', async function () {
+            await this.helper.propose();
+            await this.helper.waitForSnapshot();
+            await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+            await this.helper.waitForDeadline(+1);
+
+            expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded);
+
+            await expectRevert(this.helper.execute(), 'GovernorTimelockCompound: proposal not yet queued');
+          });
+
+          it('if too early', async function () {
+            await this.helper.propose();
+            await this.helper.waitForSnapshot();
+            await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+            await this.helper.waitForDeadline();
+            await this.helper.queue();
+
+            expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Queued);
+
+            await expectRevert(
+              this.helper.execute(),
+              "Timelock::executeTransaction: Transaction hasn't surpassed time lock",
+            );
+          });
+
+          it('if too late', async function () {
+            await this.helper.propose();
+            await this.helper.waitForSnapshot();
+            await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+            await this.helper.waitForDeadline();
+            await this.helper.queue();
+            await this.helper.waitForEta(+30 * 86400);
+
+            expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Expired);
+
+            await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
+          });
+
+          it('if already executed', async function () {
+            await this.helper.propose();
+            await this.helper.waitForSnapshot();
+            await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+            await this.helper.waitForDeadline();
+            await this.helper.queue();
+            await this.helper.waitForEta();
+            await this.helper.execute();
+            await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
+          });
+        });
       });
 
-      it('is protected', async function () {
-        await expectRevert(
-          this.mock.relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI()),
-          'Governor: onlyGovernance',
-        );
-      });
+      describe('cancel', function () {
+        it('cancel before queue prevents scheduling', async function () {
+          await this.helper.propose();
+          await this.helper.waitForSnapshot();
+          await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+          await this.helper.waitForDeadline();
 
-      it('can be executed through governance', async function () {
-        this.helper.setProposal(
-          [
-            {
-              target: this.mock.address,
-              data: this.mock.contract.methods
-                .relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI())
-                .encodeABI(),
-            },
-          ],
-          '<proposal description>',
-        );
+          expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: this.proposal.id });
 
-        expect(await this.token.balanceOf(this.mock.address), 1);
-        expect(await this.token.balanceOf(other), 0);
+          expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
+          await expectRevert(this.helper.queue(), 'Governor: proposal not successful');
+        });
 
-        await this.helper.propose();
-        await this.helper.waitForSnapshot();
-        await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-        await this.helper.waitForDeadline();
-        await this.helper.queue();
-        await this.helper.waitForEta();
-        const txExecute = await this.helper.execute();
+        it('cancel after queue prevents executing', async function () {
+          await this.helper.propose();
+          await this.helper.waitForSnapshot();
+          await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+          await this.helper.waitForDeadline();
+          await this.helper.queue();
 
-        expect(await this.token.balanceOf(this.mock.address), 0);
-        expect(await this.token.balanceOf(other), 1);
+          expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: this.proposal.id });
 
-        await expectEvent.inTransaction(txExecute.tx, this.token, 'Transfer', {
-          from: this.mock.address,
-          to: other,
-          value: '1',
+          expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
+          await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
         });
       });
-    });
-
-    describe('updateTimelock', function () {
-      beforeEach(async function () {
-        this.newTimelock = await Timelock.new(this.mock.address, 7 * 86400);
-      });
-
-      it('is protected', async function () {
-        await expectRevert(this.mock.updateTimelock(this.newTimelock.address), 'Governor: onlyGovernance');
-      });
-
-      it('can be executed through governance to', async function () {
-        this.helper.setProposal(
-          [
-            {
-              target: this.timelock.address,
-              data: this.timelock.contract.methods.setPendingAdmin(owner).encodeABI(),
-            },
-            {
-              target: this.mock.address,
-              data: this.mock.contract.methods.updateTimelock(this.newTimelock.address).encodeABI(),
-            },
-          ],
-          '<proposal description>',
-        );
-
-        await this.helper.propose();
-        await this.helper.waitForSnapshot();
-        await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-        await this.helper.waitForDeadline();
-        await this.helper.queue();
-        await this.helper.waitForEta();
-        const txExecute = await this.helper.execute();
 
-        expectEvent(txExecute, 'TimelockChange', {
-          oldTimelock: this.timelock.address,
-          newTimelock: this.newTimelock.address,
+      describe('onlyGovernance', function () {
+        describe('relay', function () {
+          beforeEach(async function () {
+            await this.token.$_mint(this.mock.address, 1);
+          });
+
+          it('is protected', async function () {
+            await expectRevert(
+              this.mock.relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI()),
+              'Governor: onlyGovernance',
+            );
+          });
+
+          it('can be executed through governance', async function () {
+            this.helper.setProposal(
+              [
+                {
+                  target: this.mock.address,
+                  data: this.mock.contract.methods
+                    .relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI())
+                    .encodeABI(),
+                },
+              ],
+              '<proposal description>',
+            );
+
+            expect(await this.token.balanceOf(this.mock.address), 1);
+            expect(await this.token.balanceOf(other), 0);
+
+            await this.helper.propose();
+            await this.helper.waitForSnapshot();
+            await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+            await this.helper.waitForDeadline();
+            await this.helper.queue();
+            await this.helper.waitForEta();
+            const txExecute = await this.helper.execute();
+
+            expect(await this.token.balanceOf(this.mock.address), 0);
+            expect(await this.token.balanceOf(other), 1);
+
+            await expectEvent.inTransaction(txExecute.tx, this.token, 'Transfer', {
+              from: this.mock.address,
+              to: other,
+              value: '1',
+            });
+          });
         });
 
-        expect(await this.mock.timelock()).to.be.bignumber.equal(this.newTimelock.address);
-      });
-    });
+        describe('updateTimelock', function () {
+          beforeEach(async function () {
+            this.newTimelock = await Timelock.new(this.mock.address, 7 * 86400);
+          });
+
+          it('is protected', async function () {
+            await expectRevert(this.mock.updateTimelock(this.newTimelock.address), 'Governor: onlyGovernance');
+          });
+
+          it('can be executed through governance to', async function () {
+            this.helper.setProposal(
+              [
+                {
+                  target: this.timelock.address,
+                  data: this.timelock.contract.methods.setPendingAdmin(owner).encodeABI(),
+                },
+                {
+                  target: this.mock.address,
+                  data: this.mock.contract.methods.updateTimelock(this.newTimelock.address).encodeABI(),
+                },
+              ],
+              '<proposal description>',
+            );
+
+            await this.helper.propose();
+            await this.helper.waitForSnapshot();
+            await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+            await this.helper.waitForDeadline();
+            await this.helper.queue();
+            await this.helper.waitForEta();
+            const txExecute = await this.helper.execute();
+
+            expectEvent(txExecute, 'TimelockChange', {
+              oldTimelock: this.timelock.address,
+              newTimelock: this.newTimelock.address,
+            });
+
+            expect(await this.mock.timelock()).to.be.bignumber.equal(this.newTimelock.address);
+          });
+        });
 
-    it('can transfer timelock to new governor', async function () {
-      const newGovernor = await Governor.new(name, 8, 32, 0, this.timelock.address, this.token.address, 0);
-      this.helper.setProposal(
-        [
-          {
-            target: this.timelock.address,
-            data: this.timelock.contract.methods.setPendingAdmin(newGovernor.address).encodeABI(),
-          },
-        ],
-        '<proposal description>',
-      );
-
-      await this.helper.propose();
-      await this.helper.waitForSnapshot();
-      await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-      await this.helper.waitForDeadline();
-      await this.helper.queue();
-      await this.helper.waitForEta();
-      const txExecute = await this.helper.execute();
-
-      await expectEvent.inTransaction(txExecute.tx, this.timelock, 'NewPendingAdmin', {
-        newPendingAdmin: newGovernor.address,
+        it('can transfer timelock to new governor', async function () {
+          const newGovernor = await Governor.new(name, 8, 32, 0, this.timelock.address, this.token.address, 0);
+          this.helper.setProposal(
+            [
+              {
+                target: this.timelock.address,
+                data: this.timelock.contract.methods.setPendingAdmin(newGovernor.address).encodeABI(),
+              },
+            ],
+            '<proposal description>',
+          );
+
+          await this.helper.propose();
+          await this.helper.waitForSnapshot();
+          await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+          await this.helper.waitForDeadline();
+          await this.helper.queue();
+          await this.helper.waitForEta();
+          const txExecute = await this.helper.execute();
+
+          await expectEvent.inTransaction(txExecute.tx, this.timelock, 'NewPendingAdmin', {
+            newPendingAdmin: newGovernor.address,
+          });
+
+          await newGovernor.__acceptAdmin();
+          expect(await this.timelock.admin()).to.be.bignumber.equal(newGovernor.address);
+        });
       });
-
-      await newGovernor.__acceptAdmin();
-      expect(await this.timelock.admin()).to.be.bignumber.equal(newGovernor.address);
     });
-  });
+  }
 });

+ 386 - 335
test/governance/extensions/GovernorTimelockControl.test.js

@@ -1,15 +1,19 @@
-const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers');
+const { constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers');
 const { expect } = require('chai');
 const Enums = require('../../helpers/enums');
 const { GovernorHelper } = require('../../helpers/governance');
 
 const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsInterface.behavior');
 
-const Token = artifacts.require('$ERC20Votes');
 const Timelock = artifacts.require('TimelockController');
 const Governor = artifacts.require('$GovernorTimelockControlMock');
 const CallReceiver = artifacts.require('CallReceiverMock');
 
+const TOKENS = [
+  { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' },
+  { Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' },
+];
+
 contract('GovernorTimelockControl', function (accounts) {
   const [owner, voter1, voter2, voter3, voter4, other] = accounts;
 
@@ -23,372 +27,419 @@ contract('GovernorTimelockControl', function (accounts) {
   const tokenName = 'MockToken';
   const tokenSymbol = 'MTKN';
   const tokenSupply = web3.utils.toWei('100');
-  const votingDelay = new BN(4);
-  const votingPeriod = new BN(16);
+  const votingDelay = web3.utils.toBN(4);
+  const votingPeriod = web3.utils.toBN(16);
   const value = web3.utils.toWei('1');
 
-  beforeEach(async function () {
-    const [deployer] = await web3.eth.getAccounts();
-
-    this.token = await Token.new(tokenName, tokenSymbol, tokenName);
-    this.timelock = await Timelock.new(3600, [], [], deployer);
-    this.mock = await Governor.new(name, votingDelay, votingPeriod, 0, this.timelock.address, this.token.address, 0);
-    this.receiver = await CallReceiver.new();
-
-    this.helper = new GovernorHelper(this.mock);
-
-    this.TIMELOCK_ADMIN_ROLE = await this.timelock.TIMELOCK_ADMIN_ROLE();
-    this.PROPOSER_ROLE = await this.timelock.PROPOSER_ROLE();
-    this.EXECUTOR_ROLE = await this.timelock.EXECUTOR_ROLE();
-    this.CANCELLER_ROLE = await this.timelock.CANCELLER_ROLE();
-
-    await web3.eth.sendTransaction({ from: owner, to: this.timelock.address, value });
-
-    // normal setup: governor is proposer, everyone is executor, timelock is its own admin
-    await this.timelock.grantRole(PROPOSER_ROLE, this.mock.address);
-    await this.timelock.grantRole(PROPOSER_ROLE, owner);
-    await this.timelock.grantRole(CANCELLER_ROLE, this.mock.address);
-    await this.timelock.grantRole(CANCELLER_ROLE, owner);
-    await this.timelock.grantRole(EXECUTOR_ROLE, constants.ZERO_ADDRESS);
-    await this.timelock.revokeRole(TIMELOCK_ADMIN_ROLE, deployer);
-
-    await this.token.$_mint(owner, tokenSupply);
-    await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner });
-    await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner });
-    await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner });
-    await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner });
-
-    // default proposal
-    this.proposal = this.helper.setProposal(
-      [
-        {
-          target: this.receiver.address,
-          value,
-          data: this.receiver.contract.methods.mockFunction().encodeABI(),
-        },
-      ],
-      '<proposal description>',
-    );
-    this.proposal.timelockid = await this.timelock.hashOperationBatch(
-      ...this.proposal.shortProposal.slice(0, 3),
-      '0x0',
-      this.proposal.shortProposal[3],
-    );
-  });
-
-  shouldSupportInterfaces(['ERC165', 'Governor', 'GovernorWithParams', 'GovernorTimelock']);
-
-  it("doesn't accept ether transfers", async function () {
-    await expectRevert.unspecified(web3.eth.sendTransaction({ from: owner, to: this.mock.address, value: 1 }));
-  });
-
-  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(votingDelay);
-    expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod);
-    expect(await this.mock.quorum(0)).to.be.bignumber.equal('0');
-
-    expect(await this.mock.timelock()).to.be.equal(this.timelock.address);
-  });
-
-  it('nominal', async function () {
-    await this.helper.propose();
-    await this.helper.waitForSnapshot();
-    await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-    await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 });
-    await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 });
-    await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 });
-    await this.helper.waitForDeadline();
-    const txQueue = await this.helper.queue();
-    await this.helper.waitForEta();
-    const txExecute = await this.helper.execute();
-
-    expectEvent(txQueue, 'ProposalQueued', { proposalId: this.proposal.id });
-    await expectEvent.inTransaction(txQueue.tx, this.timelock, 'CallScheduled', { id: this.proposal.timelockid });
-    await expectEvent.inTransaction(txQueue.tx, this.timelock, 'CallSalt', {
-      id: this.proposal.timelockid,
-    });
-
-    expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id });
-    await expectEvent.inTransaction(txExecute.tx, this.timelock, 'CallExecuted', { id: this.proposal.timelockid });
-    await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled');
-  });
-
-  describe('should revert', function () {
-    describe('on queue', function () {
-      it('if already queued', async function () {
-        await this.helper.propose();
-        await this.helper.waitForSnapshot();
-        await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-        await this.helper.waitForDeadline();
-        await this.helper.queue();
-        await expectRevert(this.helper.queue(), 'Governor: proposal not successful');
-      });
-    });
-
-    describe('on execute', function () {
-      it('if not queued', async function () {
-        await this.helper.propose();
-        await this.helper.waitForSnapshot();
-        await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-        await this.helper.waitForDeadline(+1);
-
-        expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded);
-
-        await expectRevert(this.helper.execute(), 'TimelockController: operation is not ready');
-      });
-
-      it('if too early', async function () {
-        await this.helper.propose();
-        await this.helper.waitForSnapshot();
-        await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-        await this.helper.waitForDeadline();
-        await this.helper.queue();
-
-        expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Queued);
-
-        await expectRevert(this.helper.execute(), 'TimelockController: operation is not ready');
-      });
-
-      it('if already executed', async function () {
-        await this.helper.propose();
-        await this.helper.waitForSnapshot();
-        await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-        await this.helper.waitForDeadline();
-        await this.helper.queue();
-        await this.helper.waitForEta();
-        await this.helper.execute();
-        await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
-      });
-
-      it('if already executed by another proposer', async function () {
-        await this.helper.propose();
-        await this.helper.waitForSnapshot();
-        await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-        await this.helper.waitForDeadline();
-        await this.helper.queue();
-        await this.helper.waitForEta();
-
-        await this.timelock.executeBatch(
-          ...this.proposal.shortProposal.slice(0, 3),
-          '0x0',
-          this.proposal.shortProposal[3],
-        );
-
-        await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
-      });
-    });
-  });
-
-  describe('cancel', function () {
-    it('cancel before queue prevents scheduling', async function () {
-      await this.helper.propose();
-      await this.helper.waitForSnapshot();
-      await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-      await this.helper.waitForDeadline();
-
-      expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: this.proposal.id });
-
-      expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
-      await expectRevert(this.helper.queue(), 'Governor: proposal not successful');
-    });
-
-    it('cancel after queue prevents executing', async function () {
-      await this.helper.propose();
-      await this.helper.waitForSnapshot();
-      await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-      await this.helper.waitForDeadline();
-      await this.helper.queue();
-
-      expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: this.proposal.id });
-
-      expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
-      await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
-    });
-
-    it('cancel on timelock is reflected on governor', async function () {
-      await this.helper.propose();
-      await this.helper.waitForSnapshot();
-      await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-      await this.helper.waitForDeadline();
-      await this.helper.queue();
-
-      expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Queued);
-
-      expectEvent(await this.timelock.cancel(this.proposal.timelockid, { from: owner }), 'Cancelled', {
-        id: this.proposal.timelockid,
-      });
-
-      expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
-    });
-  });
-
-  describe('onlyGovernance', function () {
-    describe('relay', function () {
+  for (const { mode, Token } of TOKENS) {
+    describe(`using ${Token._json.contractName}`, function () {
       beforeEach(async function () {
-        await this.token.$_mint(this.mock.address, 1);
-      });
-
-      it('is protected', async function () {
-        await expectRevert(
-          this.mock.relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI()),
-          'Governor: onlyGovernance',
+        const [deployer] = await web3.eth.getAccounts();
+
+        this.token = await Token.new(tokenName, tokenSymbol, tokenName);
+        this.timelock = await Timelock.new(3600, [], [], deployer);
+        this.mock = await Governor.new(
+          name,
+          votingDelay,
+          votingPeriod,
+          0,
+          this.timelock.address,
+          this.token.address,
+          0,
         );
-      });
+        this.receiver = await CallReceiver.new();
 
-      it('can be executed through governance', async function () {
-        this.helper.setProposal(
-          [
-            {
-              target: this.mock.address,
-              data: this.mock.contract.methods
-                .relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI())
-                .encodeABI(),
-            },
-          ],
-          '<proposal description>',
-        );
+        this.helper = new GovernorHelper(this.mock, mode);
 
-        expect(await this.token.balanceOf(this.mock.address), 1);
-        expect(await this.token.balanceOf(other), 0);
+        this.TIMELOCK_ADMIN_ROLE = await this.timelock.TIMELOCK_ADMIN_ROLE();
+        this.PROPOSER_ROLE = await this.timelock.PROPOSER_ROLE();
+        this.EXECUTOR_ROLE = await this.timelock.EXECUTOR_ROLE();
+        this.CANCELLER_ROLE = await this.timelock.CANCELLER_ROLE();
 
-        await this.helper.propose();
-        await this.helper.waitForSnapshot();
-        await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-        await this.helper.waitForDeadline();
-        await this.helper.queue();
-        await this.helper.waitForEta();
-        const txExecute = await this.helper.execute();
-
-        expect(await this.token.balanceOf(this.mock.address), 0);
-        expect(await this.token.balanceOf(other), 1);
+        await web3.eth.sendTransaction({ from: owner, to: this.timelock.address, value });
 
-        await expectEvent.inTransaction(txExecute.tx, this.token, 'Transfer', {
-          from: this.mock.address,
-          to: other,
-          value: '1',
-        });
-      });
+        // normal setup: governor is proposer, everyone is executor, timelock is its own admin
+        await this.timelock.grantRole(PROPOSER_ROLE, this.mock.address);
+        await this.timelock.grantRole(PROPOSER_ROLE, owner);
+        await this.timelock.grantRole(CANCELLER_ROLE, this.mock.address);
+        await this.timelock.grantRole(CANCELLER_ROLE, owner);
+        await this.timelock.grantRole(EXECUTOR_ROLE, constants.ZERO_ADDRESS);
+        await this.timelock.revokeRole(TIMELOCK_ADMIN_ROLE, deployer);
 
-      it('is payable and can transfer eth to EOA', async function () {
-        const t2g = web3.utils.toBN(128); // timelock to governor
-        const g2o = web3.utils.toBN(100); // governor to eoa (other)
+        await this.token.$_mint(owner, tokenSupply);
+        await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner });
+        await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner });
+        await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner });
+        await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner });
 
-        this.helper.setProposal(
+        // default proposal
+        this.proposal = this.helper.setProposal(
           [
             {
-              target: this.mock.address,
-              value: t2g,
-              data: this.mock.contract.methods.relay(other, g2o, '0x').encodeABI(),
+              target: this.receiver.address,
+              value,
+              data: this.receiver.contract.methods.mockFunction().encodeABI(),
             },
           ],
           '<proposal description>',
         );
-
-        expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(web3.utils.toBN(0));
-        const timelockBalance = await web3.eth.getBalance(this.timelock.address).then(web3.utils.toBN);
-        const otherBalance = await web3.eth.getBalance(other).then(web3.utils.toBN);
-
-        await this.helper.propose();
-        await this.helper.waitForSnapshot();
-        await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-        await this.helper.waitForDeadline();
-        await this.helper.queue();
-        await this.helper.waitForEta();
-        await this.helper.execute();
-
-        expect(await web3.eth.getBalance(this.timelock.address)).to.be.bignumber.equal(timelockBalance.sub(t2g));
-        expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(t2g.sub(g2o));
-        expect(await web3.eth.getBalance(other)).to.be.bignumber.equal(otherBalance.add(g2o));
+        this.proposal.timelockid = await this.timelock.hashOperationBatch(
+          ...this.proposal.shortProposal.slice(0, 3),
+          '0x0',
+          this.proposal.shortProposal[3],
+        );
       });
 
-      it('protected against other proposers', async function () {
-        await this.timelock.schedule(
-          this.mock.address,
-          web3.utils.toWei('0'),
-          this.mock.contract.methods.relay(constants.ZERO_ADDRESS, 0, '0x').encodeABI(),
-          constants.ZERO_BYTES32,
-          constants.ZERO_BYTES32,
-          3600,
-          { from: owner },
-        );
+      shouldSupportInterfaces(['ERC165', 'Governor', 'GovernorWithParams', 'GovernorTimelock']);
 
-        await time.increase(3600);
-
-        await expectRevert(
-          this.timelock.execute(
-            this.mock.address,
-            web3.utils.toWei('0'),
-            this.mock.contract.methods.relay(constants.ZERO_ADDRESS, 0, '0x').encodeABI(),
-            constants.ZERO_BYTES32,
-            constants.ZERO_BYTES32,
-            { from: owner },
-          ),
-          'TimelockController: underlying transaction reverted',
-        );
+      it("doesn't accept ether transfers", async function () {
+        await expectRevert.unspecified(web3.eth.sendTransaction({ from: owner, to: this.mock.address, value: 1 }));
       });
-    });
 
-    describe('updateTimelock', function () {
-      beforeEach(async function () {
-        this.newTimelock = await Timelock.new(3600, [this.mock.address], [this.mock.address], constants.ZERO_ADDRESS);
-      });
+      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(votingDelay);
+        expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod);
+        expect(await this.mock.quorum(0)).to.be.bignumber.equal('0');
 
-      it('is protected', async function () {
-        await expectRevert(this.mock.updateTimelock(this.newTimelock.address), 'Governor: onlyGovernance');
+        expect(await this.mock.timelock()).to.be.equal(this.timelock.address);
       });
 
-      it('can be executed through governance to', async function () {
-        this.helper.setProposal(
-          [
-            {
-              target: this.mock.address,
-              data: this.mock.contract.methods.updateTimelock(this.newTimelock.address).encodeABI(),
-            },
-          ],
-          '<proposal description>',
-        );
-
+      it('nominal', async function () {
         await this.helper.propose();
         await this.helper.waitForSnapshot();
         await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+        await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 });
+        await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 });
+        await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 });
         await this.helper.waitForDeadline();
-        await this.helper.queue();
+        const txQueue = await this.helper.queue();
         await this.helper.waitForEta();
         const txExecute = await this.helper.execute();
 
-        expectEvent(txExecute, 'TimelockChange', {
-          oldTimelock: this.timelock.address,
-          newTimelock: this.newTimelock.address,
+        expectEvent(txQueue, 'ProposalQueued', { proposalId: this.proposal.id });
+        await expectEvent.inTransaction(txQueue.tx, this.timelock, 'CallScheduled', { id: this.proposal.timelockid });
+        await expectEvent.inTransaction(txQueue.tx, this.timelock, 'CallSalt', {
+          id: this.proposal.timelockid,
         });
 
-        expect(await this.mock.timelock()).to.be.bignumber.equal(this.newTimelock.address);
+        expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id });
+        await expectEvent.inTransaction(txExecute.tx, this.timelock, 'CallExecuted', { id: this.proposal.timelockid });
+        await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled');
+      });
+
+      describe('should revert', function () {
+        describe('on queue', function () {
+          it('if already queued', async function () {
+            await this.helper.propose();
+            await this.helper.waitForSnapshot();
+            await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+            await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 });
+            await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 });
+            await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 });
+            await this.helper.waitForDeadline();
+            const txQueue = await this.helper.queue();
+            await this.helper.waitForEta();
+            const txExecute = await this.helper.execute();
+
+            expectEvent(txQueue, 'ProposalQueued', { proposalId: this.proposal.id });
+            await expectEvent.inTransaction(txQueue.tx, this.timelock, 'CallScheduled', {
+              id: this.proposal.timelockid,
+            });
+
+            expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id });
+            await expectEvent.inTransaction(txExecute.tx, this.timelock, 'CallExecuted', {
+              id: this.proposal.timelockid,
+            });
+            await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled');
+          });
+
+          describe('should revert', function () {
+            describe('on queue', function () {
+              it('if already queued', async function () {
+                await this.helper.propose();
+                await this.helper.waitForSnapshot();
+                await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+                await this.helper.waitForDeadline();
+                await this.helper.queue();
+                await expectRevert(this.helper.queue(), 'Governor: proposal not successful');
+              });
+            });
+
+            describe('on execute', function () {
+              it('if not queued', async function () {
+                await this.helper.propose();
+                await this.helper.waitForSnapshot();
+                await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+                await this.helper.waitForDeadline(+1);
+
+                expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded);
+
+                await expectRevert(this.helper.execute(), 'TimelockController: operation is not ready');
+              });
+
+              it('if too early', async function () {
+                await this.helper.propose();
+                await this.helper.waitForSnapshot();
+                await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+                await this.helper.waitForDeadline();
+                await this.helper.queue();
+
+                expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Queued);
+
+                await expectRevert(this.helper.execute(), 'TimelockController: operation is not ready');
+              });
+
+              it('if already executed', async function () {
+                await this.helper.propose();
+                await this.helper.waitForSnapshot();
+                await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+                await this.helper.waitForDeadline();
+                await this.helper.queue();
+                await this.helper.waitForEta();
+                await this.helper.execute();
+                await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
+              });
+
+              it('if already executed by another proposer', async function () {
+                await this.helper.propose();
+                await this.helper.waitForSnapshot();
+                await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+                await this.helper.waitForDeadline();
+                await this.helper.queue();
+                await this.helper.waitForEta();
+
+                await this.timelock.executeBatch(
+                  ...this.proposal.shortProposal.slice(0, 3),
+                  '0x0',
+                  this.proposal.shortProposal[3],
+                );
+
+                await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
+              });
+            });
+          });
+
+          describe('cancel', function () {
+            it('cancel before queue prevents scheduling', async function () {
+              await this.helper.propose();
+              await this.helper.waitForSnapshot();
+              await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+              await this.helper.waitForDeadline();
+
+              expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: this.proposal.id });
+
+              expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
+              await expectRevert(this.helper.queue(), 'Governor: proposal not successful');
+            });
+
+            it('cancel after queue prevents executing', async function () {
+              await this.helper.propose();
+              await this.helper.waitForSnapshot();
+              await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+              await this.helper.waitForDeadline();
+              await this.helper.queue();
+
+              expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: this.proposal.id });
+
+              expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
+              await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
+            });
+
+            it('cancel on timelock is reflected on governor', async function () {
+              await this.helper.propose();
+              await this.helper.waitForSnapshot();
+              await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+              await this.helper.waitForDeadline();
+              await this.helper.queue();
+
+              expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Queued);
+
+              expectEvent(await this.timelock.cancel(this.proposal.timelockid, { from: owner }), 'Cancelled', {
+                id: this.proposal.timelockid,
+              });
+
+              expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled);
+            });
+          });
+
+          describe('onlyGovernance', function () {
+            describe('relay', function () {
+              beforeEach(async function () {
+                await this.token.$_mint(this.mock.address, 1);
+              });
+
+              it('is protected', async function () {
+                await expectRevert(
+                  this.mock.relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI()),
+                  'Governor: onlyGovernance',
+                );
+              });
+
+              it('can be executed through governance', async function () {
+                this.helper.setProposal(
+                  [
+                    {
+                      target: this.mock.address,
+                      data: this.mock.contract.methods
+                        .relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI())
+                        .encodeABI(),
+                    },
+                  ],
+                  '<proposal description>',
+                );
+
+                expect(await this.token.balanceOf(this.mock.address), 1);
+                expect(await this.token.balanceOf(other), 0);
+
+                await this.helper.propose();
+                await this.helper.waitForSnapshot();
+                await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+                await this.helper.waitForDeadline();
+                await this.helper.queue();
+                await this.helper.waitForEta();
+                const txExecute = await this.helper.execute();
+
+                expect(await this.token.balanceOf(this.mock.address), 0);
+                expect(await this.token.balanceOf(other), 1);
+
+                await expectEvent.inTransaction(txExecute.tx, this.token, 'Transfer', {
+                  from: this.mock.address,
+                  to: other,
+                  value: '1',
+                });
+              });
+
+              it('is payable and can transfer eth to EOA', async function () {
+                const t2g = web3.utils.toBN(128); // timelock to governor
+                const g2o = web3.utils.toBN(100); // governor to eoa (other)
+
+                this.helper.setProposal(
+                  [
+                    {
+                      target: this.mock.address,
+                      value: t2g,
+                      data: this.mock.contract.methods.relay(other, g2o, '0x').encodeABI(),
+                    },
+                  ],
+                  '<proposal description>',
+                );
+
+                expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(web3.utils.toBN(0));
+                const timelockBalance = await web3.eth.getBalance(this.timelock.address).then(web3.utils.toBN);
+                const otherBalance = await web3.eth.getBalance(other).then(web3.utils.toBN);
+
+                await this.helper.propose();
+                await this.helper.waitForSnapshot();
+                await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+                await this.helper.waitForDeadline();
+                await this.helper.queue();
+                await this.helper.waitForEta();
+                await this.helper.execute();
+
+                expect(await web3.eth.getBalance(this.timelock.address)).to.be.bignumber.equal(
+                  timelockBalance.sub(t2g),
+                );
+                expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(t2g.sub(g2o));
+                expect(await web3.eth.getBalance(other)).to.be.bignumber.equal(otherBalance.add(g2o));
+              });
+
+              it('protected against other proposers', async function () {
+                await this.timelock.schedule(
+                  this.mock.address,
+                  web3.utils.toWei('0'),
+                  this.mock.contract.methods.relay(constants.ZERO_ADDRESS, 0, '0x').encodeABI(),
+                  constants.ZERO_BYTES32,
+                  constants.ZERO_BYTES32,
+                  3600,
+                  { from: owner },
+                );
+
+                await time.increase(3600);
+
+                await expectRevert(
+                  this.timelock.execute(
+                    this.mock.address,
+                    web3.utils.toWei('0'),
+                    this.mock.contract.methods.relay(constants.ZERO_ADDRESS, 0, '0x').encodeABI(),
+                    constants.ZERO_BYTES32,
+                    constants.ZERO_BYTES32,
+                    { from: owner },
+                  ),
+                  'TimelockController: underlying transaction reverted',
+                );
+              });
+            });
+
+            describe('updateTimelock', function () {
+              beforeEach(async function () {
+                this.newTimelock = await Timelock.new(
+                  3600,
+                  [this.mock.address],
+                  [this.mock.address],
+                  constants.ZERO_ADDRESS,
+                );
+              });
+
+              it('is protected', async function () {
+                await expectRevert(this.mock.updateTimelock(this.newTimelock.address), 'Governor: onlyGovernance');
+              });
+
+              it('can be executed through governance to', async function () {
+                this.helper.setProposal(
+                  [
+                    {
+                      target: this.mock.address,
+                      data: this.mock.contract.methods.updateTimelock(this.newTimelock.address).encodeABI(),
+                    },
+                  ],
+                  '<proposal description>',
+                );
+
+                await this.helper.propose();
+                await this.helper.waitForSnapshot();
+                await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+                await this.helper.waitForDeadline();
+                await this.helper.queue();
+                await this.helper.waitForEta();
+                const txExecute = await this.helper.execute();
+
+                expectEvent(txExecute, 'TimelockChange', {
+                  oldTimelock: this.timelock.address,
+                  newTimelock: this.newTimelock.address,
+                });
+
+                expect(await this.mock.timelock()).to.be.bignumber.equal(this.newTimelock.address);
+              });
+            });
+          });
+
+          it('clear queue of pending governor calls', async function () {
+            this.helper.setProposal(
+              [
+                {
+                  target: this.mock.address,
+                  data: this.mock.contract.methods.nonGovernanceFunction().encodeABI(),
+                },
+              ],
+              '<proposal description>',
+            );
+
+            await this.helper.propose();
+            await this.helper.waitForSnapshot();
+            await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+            await this.helper.waitForDeadline();
+            await this.helper.queue();
+            await this.helper.waitForEta();
+            await this.helper.execute();
+
+            // This path clears _governanceCall as part of the afterExecute call,
+            // but we have not way to check that the cleanup actually happened other
+            // then coverage reports.
+          });
+        });
       });
     });
-  });
-
-  it('clear queue of pending governor calls', async function () {
-    this.helper.setProposal(
-      [
-        {
-          target: this.mock.address,
-          data: this.mock.contract.methods.nonGovernanceFunction().encodeABI(),
-        },
-      ],
-      '<proposal description>',
-    );
-
-    await this.helper.propose();
-    await this.helper.waitForSnapshot();
-    await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-    await this.helper.waitForDeadline();
-    await this.helper.queue();
-    await this.helper.waitForEta();
-    await this.helper.execute();
-
-    // This path clears _governanceCall as part of the afterExecute call,
-    // but we have not way to check that the cleanup actually happened other
-    // then coverage reports.
-  });
+  }
 });

+ 131 - 119
test/governance/extensions/GovernorVotesQuorumFraction.test.js

@@ -1,12 +1,17 @@
-const { BN, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers');
+const { expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers');
 const { expect } = require('chai');
 const Enums = require('../../helpers/enums');
 const { GovernorHelper } = require('../../helpers/governance');
+const { clock } = require('../../helpers/time');
 
-const Token = artifacts.require('$ERC20Votes');
 const Governor = artifacts.require('$GovernorMock');
 const CallReceiver = artifacts.require('CallReceiverMock');
 
+const TOKENS = [
+  { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' },
+  { Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' },
+];
+
 contract('GovernorVotesQuorumFraction', function (accounts) {
   const [owner, voter1, voter2, voter3, voter4] = accounts;
 
@@ -14,129 +19,136 @@ contract('GovernorVotesQuorumFraction', function (accounts) {
   // 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
-  const votingDelay = new BN(4);
-  const votingPeriod = new BN(16);
+  const tokenSupply = web3.utils.toBN(web3.utils.toWei('100'));
+  const ratio = web3.utils.toBN(8); // percents
+  const newRatio = web3.utils.toBN(6); // percents
+  const votingDelay = web3.utils.toBN(4);
+  const votingPeriod = web3.utils.toBN(16);
   const value = web3.utils.toWei('1');
 
-  beforeEach(async function () {
-    this.owner = owner;
-    this.token = await Token.new(tokenName, tokenSymbol, tokenName);
-    this.mock = await Governor.new(name, votingDelay, votingPeriod, 0, this.token.address, ratio);
-    this.receiver = await CallReceiver.new();
-
-    this.helper = new GovernorHelper(this.mock);
-
-    await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value });
-
-    await this.token.$_mint(owner, tokenSupply);
-    await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner });
-    await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner });
-    await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner });
-    await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner });
-
-    // default proposal
-    this.proposal = this.helper.setProposal(
-      [
-        {
-          target: this.receiver.address,
-          value,
-          data: this.receiver.contract.methods.mockFunction().encodeABI(),
-        },
-      ],
-      '<proposal description>',
-    );
-  });
-
-  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(votingDelay);
-    expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod);
-    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),
-    );
-  });
-
-  it('quroum reached', async function () {
-    await this.helper.propose();
-    await this.helper.waitForSnapshot();
-    await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-    await this.helper.waitForDeadline();
-    await this.helper.execute();
-  });
-
-  it('quroum not reached', async function () {
-    await this.helper.propose();
-    await this.helper.waitForSnapshot();
-    await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 });
-    await this.helper.waitForDeadline();
-    await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
-  });
-
-  describe('onlyGovernance updates', function () {
-    it('updateQuorumNumerator is protected', async function () {
-      await expectRevert(this.mock.updateQuorumNumerator(newRatio), 'Governor: onlyGovernance');
-    });
-
-    it('can updateQuorumNumerator through governance', async function () {
-      this.helper.setProposal(
-        [
-          {
-            target: this.mock.address,
-            data: this.mock.contract.methods.updateQuorumNumerator(newRatio).encodeABI(),
-          },
-        ],
-        '<proposal description>',
-      );
-
-      await this.helper.propose();
-      await this.helper.waitForSnapshot();
-      await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-      await this.helper.waitForDeadline();
-
-      expectEvent(await this.helper.execute(), 'QuorumNumeratorUpdated', {
-        oldQuorumNumerator: ratio,
-        newQuorumNumerator: newRatio,
+  for (const { mode, Token } of TOKENS) {
+    describe(`using ${Token._json.contractName}`, function () {
+      beforeEach(async function () {
+        this.owner = owner;
+        this.token = await Token.new(tokenName, tokenSymbol, tokenName);
+        this.mock = await Governor.new(name, votingDelay, votingPeriod, 0, this.token.address, ratio);
+        this.receiver = await CallReceiver.new();
+
+        this.helper = new GovernorHelper(this.mock, mode);
+
+        await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value });
+
+        await this.token.$_mint(owner, tokenSupply);
+        await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner });
+        await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner });
+        await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner });
+        await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner });
+
+        // default proposal
+        this.proposal = this.helper.setProposal(
+          [
+            {
+              target: this.receiver.address,
+              value,
+              data: this.receiver.contract.methods.mockFunction().encodeABI(),
+            },
+          ],
+          '<proposal description>',
+        );
       });
 
-      expect(await this.mock.quorumNumerator()).to.be.bignumber.equal(newRatio);
-      expect(await this.mock.quorumDenominator()).to.be.bignumber.equal('100');
-
-      // it takes one block for the new quorum to take effect
-      expect(await time.latestBlock().then(blockNumber => this.mock.quorum(blockNumber.subn(1)))).to.be.bignumber.equal(
-        tokenSupply.mul(ratio).divn(100),
-      );
+      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(votingDelay);
+        expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod);
+        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 clock[mode]().then(timepoint => this.mock.quorum(timepoint - 1))).to.be.bignumber.equal(
+          tokenSupply.mul(ratio).divn(100),
+        );
+      });
 
-      await time.advanceBlock();
+      it('quroum reached', async function () {
+        await this.helper.propose();
+        await this.helper.waitForSnapshot();
+        await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+        await this.helper.waitForDeadline();
+        await this.helper.execute();
+      });
 
-      expect(await time.latestBlock().then(blockNumber => this.mock.quorum(blockNumber.subn(1)))).to.be.bignumber.equal(
-        tokenSupply.mul(newRatio).divn(100),
-      );
-    });
+      it('quroum not reached', async function () {
+        await this.helper.propose();
+        await this.helper.waitForSnapshot();
+        await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 });
+        await this.helper.waitForDeadline();
+        await expectRevert(this.helper.execute(), 'Governor: proposal not successful');
+      });
 
-    it('cannot updateQuorumNumerator over the maximum', async function () {
-      this.helper.setProposal(
-        [
-          {
-            target: this.mock.address,
-            data: this.mock.contract.methods.updateQuorumNumerator('101').encodeABI(),
-          },
-        ],
-        '<proposal description>',
-      );
-
-      await this.helper.propose();
-      await this.helper.waitForSnapshot();
-      await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
-      await this.helper.waitForDeadline();
-
-      await expectRevert(this.helper.execute(), 'GovernorVotesQuorumFraction: quorumNumerator over quorumDenominator');
+      describe('onlyGovernance updates', function () {
+        it('updateQuorumNumerator is protected', async function () {
+          await expectRevert(this.mock.updateQuorumNumerator(newRatio), 'Governor: onlyGovernance');
+        });
+
+        it('can updateQuorumNumerator through governance', async function () {
+          this.helper.setProposal(
+            [
+              {
+                target: this.mock.address,
+                data: this.mock.contract.methods.updateQuorumNumerator(newRatio).encodeABI(),
+              },
+            ],
+            '<proposal description>',
+          );
+
+          await this.helper.propose();
+          await this.helper.waitForSnapshot();
+          await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+          await this.helper.waitForDeadline();
+
+          expectEvent(await this.helper.execute(), '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');
+
+          // it takes one block for the new quorum to take effect
+          expect(await clock[mode]().then(blockNumber => this.mock.quorum(blockNumber - 1))).to.be.bignumber.equal(
+            tokenSupply.mul(ratio).divn(100),
+          );
+
+          await time.advanceBlock();
+
+          expect(await clock[mode]().then(blockNumber => this.mock.quorum(blockNumber - 1))).to.be.bignumber.equal(
+            tokenSupply.mul(newRatio).divn(100),
+          );
+        });
+
+        it('cannot updateQuorumNumerator over the maximum', async function () {
+          this.helper.setProposal(
+            [
+              {
+                target: this.mock.address,
+                data: this.mock.contract.methods.updateQuorumNumerator('101').encodeABI(),
+              },
+            ],
+            '<proposal description>',
+          );
+
+          await this.helper.propose();
+          await this.helper.waitForSnapshot();
+          await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+          await this.helper.waitForDeadline();
+
+          await expectRevert(
+            this.helper.execute(),
+            'GovernorVotesQuorumFraction: quorumNumerator over quorumDenominator',
+          );
+        });
+      });
     });
-  });
+  }
 });

+ 144 - 136
test/governance/extensions/GovernorWithParams.test.js

@@ -1,4 +1,4 @@
-const { BN, expectEvent } = require('@openzeppelin/test-helpers');
+const { expectEvent } = require('@openzeppelin/test-helpers');
 const { expect } = require('chai');
 const ethSigUtil = require('eth-sig-util');
 const Wallet = require('ethereumjs-wallet').default;
@@ -7,17 +7,21 @@ const Enums = require('../../helpers/enums');
 const { getDomain, domainType } = require('../../helpers/eip712');
 const { GovernorHelper } = require('../../helpers/governance');
 
-const Token = artifacts.require('$ERC20VotesComp');
 const Governor = artifacts.require('$GovernorWithParamsMock');
 const CallReceiver = artifacts.require('CallReceiverMock');
 
 const rawParams = {
-  uintParam: new BN('42'),
+  uintParam: web3.utils.toBN('42'),
   strParam: 'These are my params',
 };
 
 const encodedParams = web3.eth.abi.encodeParameters(['uint256', 'string'], Object.values(rawParams));
 
+const TOKENS = [
+  { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' },
+  { Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' },
+];
+
 contract('GovernorWithParams', function (accounts) {
   const [owner, proposer, voter1, voter2, voter3, voter4] = accounts;
 
@@ -25,141 +29,145 @@ contract('GovernorWithParams', function (accounts) {
   const tokenName = 'MockToken';
   const tokenSymbol = 'MTKN';
   const tokenSupply = web3.utils.toWei('100');
-  const votingDelay = new BN(4);
-  const votingPeriod = new BN(16);
+  const votingDelay = web3.utils.toBN(4);
+  const votingPeriod = web3.utils.toBN(16);
   const value = web3.utils.toWei('1');
 
-  beforeEach(async function () {
-    this.chainId = await web3.eth.getChainId();
-    this.token = await Token.new(tokenName, tokenSymbol, tokenName);
-    this.mock = await Governor.new(name, this.token.address);
-    this.receiver = await CallReceiver.new();
-
-    this.helper = new GovernorHelper(this.mock);
-
-    await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value });
-
-    await this.token.$_mint(owner, tokenSupply);
-    await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner });
-    await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner });
-    await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner });
-    await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner });
-
-    // default proposal
-    this.proposal = this.helper.setProposal(
-      [
-        {
-          target: this.receiver.address,
-          value,
-          data: this.receiver.contract.methods.mockFunction().encodeABI(),
-        },
-      ],
-      '<proposal description>',
-    );
-  });
-
-  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(votingDelay);
-    expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod);
-  });
-
-  it('nominal is unaffected', async function () {
-    await this.helper.propose({ from: proposer });
-    await this.helper.waitForSnapshot();
-    await this.helper.vote({ support: Enums.VoteType.For, reason: 'This is nice' }, { from: voter1 });
-    await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 });
-    await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 });
-    await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 });
-    await this.helper.waitForDeadline();
-    await this.helper.execute();
-
-    expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false);
-    expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true);
-    expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true);
-    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(value);
-  });
-
-  it('Voting with params is properly supported', async function () {
-    await this.helper.propose({ from: proposer });
-    await this.helper.waitForSnapshot();
-
-    const weight = new BN(web3.utils.toWei('7')).sub(rawParams.uintParam);
-
-    const tx = await this.helper.vote(
-      {
-        support: Enums.VoteType.For,
-        reason: 'no particular reason',
-        params: encodedParams,
-      },
-      { from: voter2 },
-    );
-
-    expectEvent(tx, 'CountParams', { ...rawParams });
-    expectEvent(tx, 'VoteCastWithParams', {
-      voter: voter2,
-      proposalId: this.proposal.id,
-      support: Enums.VoteType.For,
-      weight,
-      reason: 'no particular reason',
-      params: encodedParams,
-    });
-
-    const votes = await this.mock.proposalVotes(this.proposal.id);
-    expect(votes.forVotes).to.be.bignumber.equal(weight);
-  });
-
-  it('Voting with params by signature is properly supported', async function () {
-    const voterBySig = Wallet.generate();
-    const voterBySigAddress = web3.utils.toChecksumAddress(voterBySig.getAddressString());
-
-    const signature = (contract, message) =>
-      getDomain(contract)
-        .then(domain => ({
-          primaryType: 'ExtendedBallot',
-          types: {
-            EIP712Domain: domainType(domain),
-            ExtendedBallot: [
-              { name: 'proposalId', type: 'uint256' },
-              { name: 'support', type: 'uint8' },
-              { name: 'reason', type: 'string' },
-              { name: 'params', type: 'bytes' },
-            ],
+  for (const { mode, Token } of TOKENS) {
+    describe(`using ${Token._json.contractName}`, function () {
+      beforeEach(async function () {
+        this.chainId = await web3.eth.getChainId();
+        this.token = await Token.new(tokenName, tokenSymbol, tokenName);
+        this.mock = await Governor.new(name, this.token.address);
+        this.receiver = await CallReceiver.new();
+
+        this.helper = new GovernorHelper(this.mock, mode);
+
+        await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value });
+
+        await this.token.$_mint(owner, tokenSupply);
+        await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner });
+        await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner });
+        await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner });
+        await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner });
+
+        // default proposal
+        this.proposal = this.helper.setProposal(
+          [
+            {
+              target: this.receiver.address,
+              value,
+              data: this.receiver.contract.methods.mockFunction().encodeABI(),
+            },
+          ],
+          '<proposal description>',
+        );
+      });
+
+      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(votingDelay);
+        expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod);
+      });
+
+      it('nominal is unaffected', async function () {
+        await this.helper.propose({ from: proposer });
+        await this.helper.waitForSnapshot();
+        await this.helper.vote({ support: Enums.VoteType.For, reason: 'This is nice' }, { from: voter1 });
+        await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 });
+        await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 });
+        await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 });
+        await this.helper.waitForDeadline();
+        await this.helper.execute();
+
+        expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false);
+        expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true);
+        expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true);
+        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(value);
+      });
+
+      it('Voting with params is properly supported', async function () {
+        await this.helper.propose({ from: proposer });
+        await this.helper.waitForSnapshot();
+
+        const weight = web3.utils.toBN(web3.utils.toWei('7')).sub(rawParams.uintParam);
+
+        const tx = await this.helper.vote(
+          {
+            support: Enums.VoteType.For,
+            reason: 'no particular reason',
+            params: encodedParams,
           },
-          domain,
-          message,
-        }))
-        .then(data => ethSigUtil.signTypedMessage(voterBySig.getPrivateKey(), { data }))
-        .then(fromRpcSig);
-
-    await this.token.delegate(voterBySigAddress, { from: voter2 });
-
-    // Run proposal
-    await this.helper.propose();
-    await this.helper.waitForSnapshot();
-
-    const weight = new BN(web3.utils.toWei('7')).sub(rawParams.uintParam);
-
-    const tx = await this.helper.vote({
-      support: Enums.VoteType.For,
-      reason: 'no particular reason',
-      params: encodedParams,
-      signature,
+          { from: voter2 },
+        );
+
+        expectEvent(tx, 'CountParams', { ...rawParams });
+        expectEvent(tx, 'VoteCastWithParams', {
+          voter: voter2,
+          proposalId: this.proposal.id,
+          support: Enums.VoteType.For,
+          weight,
+          reason: 'no particular reason',
+          params: encodedParams,
+        });
+
+        const votes = await this.mock.proposalVotes(this.proposal.id);
+        expect(votes.forVotes).to.be.bignumber.equal(weight);
+      });
+
+      it('Voting with params by signature is properly supported', async function () {
+        const voterBySig = Wallet.generate();
+        const voterBySigAddress = web3.utils.toChecksumAddress(voterBySig.getAddressString());
+
+        const signature = (contract, message) =>
+          getDomain(contract)
+            .then(domain => ({
+              primaryType: 'ExtendedBallot',
+              types: {
+                EIP712Domain: domainType(domain),
+                ExtendedBallot: [
+                  { name: 'proposalId', type: 'uint256' },
+                  { name: 'support', type: 'uint8' },
+                  { name: 'reason', type: 'string' },
+                  { name: 'params', type: 'bytes' },
+                ],
+              },
+              domain,
+              message,
+            }))
+            .then(data => ethSigUtil.signTypedMessage(voterBySig.getPrivateKey(), { data }))
+            .then(fromRpcSig);
+
+        await this.token.delegate(voterBySigAddress, { from: voter2 });
+
+        // Run proposal
+        await this.helper.propose();
+        await this.helper.waitForSnapshot();
+
+        const weight = web3.utils.toBN(web3.utils.toWei('7')).sub(rawParams.uintParam);
+
+        const tx = await this.helper.vote({
+          support: Enums.VoteType.For,
+          reason: 'no particular reason',
+          params: encodedParams,
+          signature,
+        });
+
+        expectEvent(tx, 'CountParams', { ...rawParams });
+        expectEvent(tx, 'VoteCastWithParams', {
+          voter: voterBySigAddress,
+          proposalId: this.proposal.id,
+          support: Enums.VoteType.For,
+          weight,
+          reason: 'no particular reason',
+          params: encodedParams,
+        });
+
+        const votes = await this.mock.proposalVotes(this.proposal.id);
+        expect(votes.forVotes).to.be.bignumber.equal(weight);
+      });
     });
-
-    expectEvent(tx, 'CountParams', { ...rawParams });
-    expectEvent(tx, 'VoteCastWithParams', {
-      voter: voterBySigAddress,
-      proposalId: this.proposal.id,
-      support: Enums.VoteType.For,
-      weight,
-      reason: 'no particular reason',
-      params: encodedParams,
-    });
-
-    const votes = await this.mock.proposalVotes(this.proposal.id);
-    expect(votes.forVotes).to.be.bignumber.equal(weight);
-  });
+  }
 });

+ 23 - 0
test/governance/utils/EIP6372.behavior.js

@@ -0,0 +1,23 @@
+const { clock } = require('../../helpers/time');
+
+function shouldBehaveLikeEIP6372(mode = 'blocknumber') {
+  describe('should implement EIP6372', function () {
+    beforeEach(async function () {
+      this.mock = this.mock ?? this.token ?? this.votes;
+    });
+
+    it('clock is correct', async function () {
+      expect(await this.mock.clock()).to.be.bignumber.equal(await clock[mode]().then(web3.utils.toBN));
+    });
+
+    it('CLOCK_MODE is correct', async function () {
+      const params = new URLSearchParams(await this.mock.CLOCK_MODE());
+      expect(params.get('mode')).to.be.equal(mode);
+      expect(params.get('from')).to.be.equal(mode == 'blocknumber' ? 'default' : null);
+    });
+  });
+}
+
+module.exports = {
+  shouldBehaveLikeEIP6372,
+};

+ 56 - 35
test/governance/utils/Votes.behavior.js

@@ -6,7 +6,10 @@ const { fromRpcSig } = require('ethereumjs-util');
 const ethSigUtil = require('eth-sig-util');
 const Wallet = require('ethereumjs-wallet').default;
 
+const { shouldBehaveLikeEIP6372 } = require('./EIP6372.behavior');
+
 const { getDomain, domainType, domainSeparator } = require('../../helpers/eip712');
+const { clockFromReceipt } = require('../../helpers/time');
 
 const Delegation = [
   { name: 'delegatee', type: 'address' },
@@ -14,7 +17,9 @@ const Delegation = [
   { name: 'expiry', type: 'uint256' },
 ];
 
-function shouldBehaveLikeVotes() {
+function shouldBehaveLikeVotes(mode = 'blocknumber') {
+  shouldBehaveLikeEIP6372(mode);
+
   describe('run votes workflow', function () {
     it('initial nonce is 0', async function () {
       expect(await this.votes.nonces(this.account1)).to.be.bignumber.equal('0');
@@ -57,6 +62,8 @@ function shouldBehaveLikeVotes() {
         expect(await this.votes.delegates(delegatorAddress)).to.be.equal(ZERO_ADDRESS);
 
         const { receipt } = await this.votes.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s);
+        const timepoint = await clockFromReceipt[mode](receipt);
+
         expectEvent(receipt, 'DelegateChanged', {
           delegator: delegatorAddress,
           fromDelegate: ZERO_ADDRESS,
@@ -71,9 +78,9 @@ function shouldBehaveLikeVotes() {
         expect(await this.votes.delegates(delegatorAddress)).to.be.equal(delegatorAddress);
 
         expect(await this.votes.getVotes(delegatorAddress)).to.be.bignumber.equal('1');
-        expect(await this.votes.getPastVotes(delegatorAddress, receipt.blockNumber - 1)).to.be.bignumber.equal('0');
+        expect(await this.votes.getPastVotes(delegatorAddress, timepoint - 1)).to.be.bignumber.equal('0');
         await time.advanceBlock();
-        expect(await this.votes.getPastVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal('1');
+        expect(await this.votes.getPastVotes(delegatorAddress, timepoint)).to.be.bignumber.equal('1');
       });
 
       it('rejects reused signature', async function () {
@@ -157,6 +164,8 @@ function shouldBehaveLikeVotes() {
           expect(await this.votes.delegates(this.account1)).to.be.equal(ZERO_ADDRESS);
 
           const { receipt } = await this.votes.delegate(this.account1, { from: this.account1 });
+          const timepoint = await clockFromReceipt[mode](receipt);
+
           expectEvent(receipt, 'DelegateChanged', {
             delegator: this.account1,
             fromDelegate: ZERO_ADDRESS,
@@ -171,9 +180,9 @@ function shouldBehaveLikeVotes() {
           expect(await this.votes.delegates(this.account1)).to.be.equal(this.account1);
 
           expect(await this.votes.getVotes(this.account1)).to.be.bignumber.equal('1');
-          expect(await this.votes.getPastVotes(this.account1, receipt.blockNumber - 1)).to.be.bignumber.equal('0');
+          expect(await this.votes.getPastVotes(this.account1, timepoint - 1)).to.be.bignumber.equal('0');
           await time.advanceBlock();
-          expect(await this.votes.getPastVotes(this.account1, receipt.blockNumber)).to.be.bignumber.equal('1');
+          expect(await this.votes.getPastVotes(this.account1, timepoint)).to.be.bignumber.equal('1');
         });
 
         it('delegation without tokens', async function () {
@@ -202,6 +211,8 @@ function shouldBehaveLikeVotes() {
         expect(await this.votes.delegates(this.account1)).to.be.equal(this.account1);
 
         const { receipt } = await this.votes.delegate(this.account1Delegatee, { from: this.account1 });
+        const timepoint = await clockFromReceipt[mode](receipt);
+
         expectEvent(receipt, 'DelegateChanged', {
           delegator: this.account1,
           fromDelegate: this.account1,
@@ -217,16 +228,16 @@ function shouldBehaveLikeVotes() {
           previousBalance: '0',
           newBalance: '1',
         });
-        const prevBlock = receipt.blockNumber - 1;
+
         expect(await this.votes.delegates(this.account1)).to.be.equal(this.account1Delegatee);
 
         expect(await this.votes.getVotes(this.account1)).to.be.bignumber.equal('0');
         expect(await this.votes.getVotes(this.account1Delegatee)).to.be.bignumber.equal('1');
-        expect(await this.votes.getPastVotes(this.account1, receipt.blockNumber - 1)).to.be.bignumber.equal('1');
-        expect(await this.votes.getPastVotes(this.account1Delegatee, prevBlock)).to.be.bignumber.equal('0');
+        expect(await this.votes.getPastVotes(this.account1, timepoint - 1)).to.be.bignumber.equal('1');
+        expect(await this.votes.getPastVotes(this.account1Delegatee, timepoint - 1)).to.be.bignumber.equal('0');
         await time.advanceBlock();
-        expect(await this.votes.getPastVotes(this.account1, receipt.blockNumber)).to.be.bignumber.equal('0');
-        expect(await this.votes.getPastVotes(this.account1Delegatee, receipt.blockNumber)).to.be.bignumber.equal('1');
+        expect(await this.votes.getPastVotes(this.account1, timepoint)).to.be.bignumber.equal('0');
+        expect(await this.votes.getPastVotes(this.account1Delegatee, timepoint)).to.be.bignumber.equal('1');
       });
     });
 
@@ -236,7 +247,7 @@ function shouldBehaveLikeVotes() {
       });
 
       it('reverts if block number >= current block', async function () {
-        await expectRevert(this.votes.getPastTotalSupply(5e10), 'block not yet mined');
+        await expectRevert(this.votes.getPastTotalSupply(5e10), 'future lookup');
       });
 
       it('returns 0 if there are no checkpoints', async function () {
@@ -244,22 +255,24 @@ function shouldBehaveLikeVotes() {
       });
 
       it('returns the latest block if >= last checkpoint block', async function () {
-        const t1 = await this.votes.$_mint(this.account1, this.NFT0);
+        const { receipt } = await this.votes.$_mint(this.account1, this.NFT0);
+        const timepoint = await clockFromReceipt[mode](receipt);
         await time.advanceBlock();
         await time.advanceBlock();
 
-        expect(await this.votes.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0');
-        expect(await this.votes.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1');
+        expect(await this.votes.getPastTotalSupply(timepoint - 1)).to.be.bignumber.equal('0');
+        expect(await this.votes.getPastTotalSupply(timepoint + 1)).to.be.bignumber.equal('1');
       });
 
       it('returns zero if < first checkpoint block', async function () {
         await time.advanceBlock();
-        const t2 = await this.votes.$_mint(this.account1, this.NFT1);
+        const { receipt } = await this.votes.$_mint(this.account1, this.NFT1);
+        const timepoint = await clockFromReceipt[mode](receipt);
         await time.advanceBlock();
         await time.advanceBlock();
 
-        expect(await this.votes.getPastTotalSupply(t2.receipt.blockNumber - 1)).to.be.bignumber.equal('0');
-        expect(await this.votes.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('1');
+        expect(await this.votes.getPastTotalSupply(timepoint - 1)).to.be.bignumber.equal('0');
+        expect(await this.votes.getPastTotalSupply(timepoint + 1)).to.be.bignumber.equal('1');
       });
 
       it('generally returns the voting balance at the appropriate checkpoint', async function () {
@@ -279,17 +292,23 @@ function shouldBehaveLikeVotes() {
         await time.advanceBlock();
         await time.advanceBlock();
 
-        expect(await this.votes.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0');
-        expect(await this.votes.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('1');
-        expect(await this.votes.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1');
-        expect(await this.votes.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal('0');
-        expect(await this.votes.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('0');
-        expect(await this.votes.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal('1');
-        expect(await this.votes.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('1');
-        expect(await this.votes.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal('0');
-        expect(await this.votes.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('0');
-        expect(await this.votes.getPastTotalSupply(t5.receipt.blockNumber)).to.be.bignumber.equal('1');
-        expect(await this.votes.getPastTotalSupply(t5.receipt.blockNumber + 1)).to.be.bignumber.equal('1');
+        t1.timepoint = await clockFromReceipt[mode](t1.receipt);
+        t2.timepoint = await clockFromReceipt[mode](t2.receipt);
+        t3.timepoint = await clockFromReceipt[mode](t3.receipt);
+        t4.timepoint = await clockFromReceipt[mode](t4.receipt);
+        t5.timepoint = await clockFromReceipt[mode](t5.receipt);
+
+        expect(await this.votes.getPastTotalSupply(t1.timepoint - 1)).to.be.bignumber.equal('0');
+        expect(await this.votes.getPastTotalSupply(t1.timepoint)).to.be.bignumber.equal('1');
+        expect(await this.votes.getPastTotalSupply(t1.timepoint + 1)).to.be.bignumber.equal('1');
+        expect(await this.votes.getPastTotalSupply(t2.timepoint)).to.be.bignumber.equal('0');
+        expect(await this.votes.getPastTotalSupply(t2.timepoint + 1)).to.be.bignumber.equal('0');
+        expect(await this.votes.getPastTotalSupply(t3.timepoint)).to.be.bignumber.equal('1');
+        expect(await this.votes.getPastTotalSupply(t3.timepoint + 1)).to.be.bignumber.equal('1');
+        expect(await this.votes.getPastTotalSupply(t4.timepoint)).to.be.bignumber.equal('0');
+        expect(await this.votes.getPastTotalSupply(t4.timepoint + 1)).to.be.bignumber.equal('0');
+        expect(await this.votes.getPastTotalSupply(t5.timepoint)).to.be.bignumber.equal('1');
+        expect(await this.votes.getPastTotalSupply(t5.timepoint + 1)).to.be.bignumber.equal('1');
       });
     });
 
@@ -305,7 +324,7 @@ function shouldBehaveLikeVotes() {
 
       describe('getPastVotes', function () {
         it('reverts if block number >= current block', async function () {
-          await expectRevert(this.votes.getPastVotes(this.account2, 5e10), 'block not yet mined');
+          await expectRevert(this.votes.getPastVotes(this.account2, 5e10), 'future lookup');
         });
 
         it('returns 0 if there are no checkpoints', async function () {
@@ -313,22 +332,24 @@ function shouldBehaveLikeVotes() {
         });
 
         it('returns the latest block if >= last checkpoint block', async function () {
-          const t1 = await this.votes.delegate(this.account2, { from: this.account1 });
+          const { receipt } = await this.votes.delegate(this.account2, { from: this.account1 });
+          const timepoint = await clockFromReceipt[mode](receipt);
           await time.advanceBlock();
           await time.advanceBlock();
+
           const latest = await this.votes.getVotes(this.account2);
-          const nextBlock = t1.receipt.blockNumber + 1;
-          expect(await this.votes.getPastVotes(this.account2, t1.receipt.blockNumber)).to.be.bignumber.equal(latest);
-          expect(await this.votes.getPastVotes(this.account2, nextBlock)).to.be.bignumber.equal(latest);
+          expect(await this.votes.getPastVotes(this.account2, timepoint)).to.be.bignumber.equal(latest);
+          expect(await this.votes.getPastVotes(this.account2, timepoint + 1)).to.be.bignumber.equal(latest);
         });
 
         it('returns zero if < first checkpoint block', async function () {
           await time.advanceBlock();
-          const t1 = await this.votes.delegate(this.account2, { from: this.account1 });
+          const { receipt } = await this.votes.delegate(this.account2, { from: this.account1 });
+          const timepoint = await clockFromReceipt[mode](receipt);
           await time.advanceBlock();
           await time.advanceBlock();
 
-          expect(await this.votes.getPastVotes(this.account2, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0');
+          expect(await this.votes.getPastVotes(this.account2, timepoint - 1)).to.be.bignumber.equal('0');
         });
       });
     });

+ 54 - 41
test/governance/utils/Votes.test.js

@@ -3,56 +3,69 @@ const { expectRevert, BN } = require('@openzeppelin/test-helpers');
 const { expect } = require('chai');
 
 const { getChainId } = require('../../helpers/chainid');
+const { clockFromReceipt } = require('../../helpers/time');
 
 const { shouldBehaveLikeVotes } = require('./Votes.behavior');
 
-const Votes = artifacts.require('$VotesMock');
+const MODES = {
+  blocknumber: artifacts.require('$VotesMock'),
+  timestamp: artifacts.require('$VotesTimestampMock'),
+};
 
 contract('Votes', function (accounts) {
   const [account1, account2, account3] = accounts;
-  beforeEach(async function () {
-    this.name = 'My Vote';
-    this.votes = await Votes.new(this.name, '1');
-  });
-
-  it('starts with zero votes', async function () {
-    expect(await this.votes.getTotalSupply()).to.be.bignumber.equal('0');
-  });
-
-  describe('performs voting operations', function () {
-    beforeEach(async function () {
-      this.tx1 = await this.votes.$_mint(account1, 1);
-      this.tx2 = await this.votes.$_mint(account2, 1);
-      this.tx3 = await this.votes.$_mint(account3, 1);
-    });
 
-    it('reverts if block number >= current block', async function () {
-      await expectRevert(this.votes.getPastTotalSupply(this.tx3.receipt.blockNumber + 1), 'Votes: block not yet mined');
-    });
+  for (const [mode, artifact] of Object.entries(MODES)) {
+    describe(`vote with ${mode}`, function () {
+      beforeEach(async function () {
+        this.name = 'My Vote';
+        this.votes = await artifact.new(this.name, '1');
+      });
 
-    it('delegates', async function () {
-      await this.votes.delegate(account3, account2);
+      it('starts with zero votes', async function () {
+        expect(await this.votes.getTotalSupply()).to.be.bignumber.equal('0');
+      });
 
-      expect(await this.votes.delegates(account3)).to.be.equal(account2);
-    });
+      describe('performs voting operations', function () {
+        beforeEach(async function () {
+          this.tx1 = await this.votes.$_mint(account1, 1);
+          this.tx2 = await this.votes.$_mint(account2, 1);
+          this.tx3 = await this.votes.$_mint(account3, 1);
+          this.tx1.timepoint = await clockFromReceipt[mode](this.tx1.receipt);
+          this.tx2.timepoint = await clockFromReceipt[mode](this.tx2.receipt);
+          this.tx3.timepoint = await clockFromReceipt[mode](this.tx3.receipt);
+        });
 
-    it('returns total amount of votes', async function () {
-      expect(await this.votes.getTotalSupply()).to.be.bignumber.equal('3');
-    });
-  });
-
-  describe('performs voting workflow', function () {
-    beforeEach(async function () {
-      this.chainId = await getChainId();
-      this.account1 = account1;
-      this.account2 = account2;
-      this.account1Delegatee = account2;
-      this.NFT0 = new BN('10000000000000000000000000');
-      this.NFT1 = new BN('10');
-      this.NFT2 = new BN('20');
-      this.NFT3 = new BN('30');
-    });
+        it('reverts if block number >= current block', async function () {
+          await expectRevert(this.votes.getPastTotalSupply(this.tx3.timepoint + 1), 'Votes: future lookup');
+        });
 
-    shouldBehaveLikeVotes();
-  });
+        it('delegates', async function () {
+          await this.votes.delegate(account3, account2);
+
+          expect(await this.votes.delegates(account3)).to.be.equal(account2);
+        });
+
+        it('returns total amount of votes', async function () {
+          expect(await this.votes.getTotalSupply()).to.be.bignumber.equal('3');
+        });
+      });
+
+      describe('performs voting workflow', function () {
+        beforeEach(async function () {
+          this.chainId = await getChainId();
+          this.account1 = account1;
+          this.account2 = account2;
+          this.account1Delegatee = account2;
+          this.NFT0 = new BN('10000000000000000000000000');
+          this.NFT1 = new BN('10');
+          this.NFT2 = new BN('20');
+          this.NFT3 = new BN('30');
+        });
+
+        // includes EIP6372 behavior check
+        shouldBehaveLikeVotes(mode);
+      });
+    });
+  }
 });

+ 6 - 9
test/helpers/governance.js

@@ -1,4 +1,4 @@
-const { time } = require('@openzeppelin/test-helpers');
+const { forward } = require('../helpers/time');
 
 function zip(...args) {
   return Array(Math.max(...args.map(array => array.length)))
@@ -15,8 +15,9 @@ function concatOpts(args, opts = null) {
 }
 
 class GovernorHelper {
-  constructor(governor) {
+  constructor(governor, mode = 'blocknumber') {
     this.governor = governor;
+    this.mode = mode;
   }
 
   delegate(delegation = {}, opts = null) {
@@ -116,21 +117,17 @@ class GovernorHelper {
 
   waitForSnapshot(offset = 0) {
     const proposal = this.currentProposal;
-    return this.governor
-      .proposalSnapshot(proposal.id)
-      .then(blockNumber => time.advanceBlockTo(blockNumber.addn(offset)));
+    return this.governor.proposalSnapshot(proposal.id).then(timepoint => forward[this.mode](timepoint.addn(offset)));
   }
 
   waitForDeadline(offset = 0) {
     const proposal = this.currentProposal;
-    return this.governor
-      .proposalDeadline(proposal.id)
-      .then(blockNumber => time.advanceBlockTo(blockNumber.addn(offset)));
+    return this.governor.proposalDeadline(proposal.id).then(timepoint => forward[this.mode](timepoint.addn(offset)));
   }
 
   waitForEta(offset = 0) {
     const proposal = this.currentProposal;
-    return this.governor.proposalEta(proposal.id).then(timestamp => time.increaseTo(timestamp.addn(offset)));
+    return this.governor.proposalEta(proposal.id).then(timestamp => forward.timestamp(timestamp.addn(offset)));
   }
 
   /**

+ 16 - 0
test/helpers/time.js

@@ -0,0 +1,16 @@
+const { time } = require('@openzeppelin/test-helpers');
+
+module.exports = {
+  clock: {
+    blocknumber: () => web3.eth.getBlock('latest').then(block => block.number),
+    timestamp: () => web3.eth.getBlock('latest').then(block => block.timestamp),
+  },
+  clockFromReceipt: {
+    blocknumber: receipt => Promise.resolve(receipt.blockNumber),
+    timestamp: receipt => web3.eth.getBlock(receipt.blockNumber).then(block => block.timestamp),
+  },
+  forward: {
+    blocknumber: time.advanceBlockTo,
+    timestamp: time.increaseTo,
+  },
+};

+ 508 - 490
test/token/ERC20/extensions/ERC20Votes.test.js

@@ -8,11 +8,11 @@ const { fromRpcSig } = require('ethereumjs-util');
 const ethSigUtil = require('eth-sig-util');
 const Wallet = require('ethereumjs-wallet').default;
 
-const ERC20Votes = artifacts.require('$ERC20Votes');
-
 const { batchInBlock } = require('../../../helpers/txpool');
 const { getDomain, domainType, domainSeparator } = require('../../../helpers/eip712');
-const { getChainId } = require('../../../helpers/chainid');
+const { clock, clockFromReceipt } = require('../../../helpers/time');
+
+const { shouldBehaveLikeEIP6372 } = require('../../../governance/utils/EIP6372.behavior');
 
 const Delegation = [
   { name: 'delegatee', type: 'address' },
@@ -20,541 +20,559 @@ const Delegation = [
   { name: 'expiry', type: 'uint256' },
 ];
 
+const MODES = {
+  blocknumber: artifacts.require('$ERC20Votes'),
+  timestamp: artifacts.require('$ERC20VotesTimestampMock'),
+};
+
 contract('ERC20Votes', function (accounts) {
   const [holder, recipient, holderDelegatee, other1, other2] = accounts;
 
   const name = 'My Token';
   const symbol = 'MTKN';
-  const version = '1';
   const supply = new BN('10000000000000000000000000');
 
-  beforeEach(async function () {
-    this.chainId = await getChainId();
-    this.token = await ERC20Votes.new(name, symbol, name);
-  });
-
-  it('initial nonce is 0', async function () {
-    expect(await this.token.nonces(holder)).to.be.bignumber.equal('0');
-  });
-
-  it('domain separator', async function () {
-    expect(await this.token.DOMAIN_SEPARATOR()).to.equal(await getDomain(this.token).then(domainSeparator));
-  });
-
-  it('minting restriction', async function () {
-    const amount = new BN('2').pow(new BN('224'));
-    await expectRevert(this.token.$_mint(holder, amount), 'ERC20Votes: total supply risks overflowing votes');
-  });
-
-  it('recent checkpoints', async function () {
-    await this.token.delegate(holder, { from: holder });
-    for (let i = 0; i < 6; i++) {
-      await this.token.$_mint(holder, 1);
-    }
-    const block = await web3.eth.getBlockNumber();
-    expect(await this.token.numCheckpoints(holder)).to.be.bignumber.equal('6');
-    // recent
-    expect(await this.token.getPastVotes(holder, block - 1)).to.be.bignumber.equal('5');
-    // non-recent
-    expect(await this.token.getPastVotes(holder, block - 6)).to.be.bignumber.equal('0');
-  });
-
-  describe('set delegation', function () {
-    describe('call', function () {
-      it('delegation with balance', async function () {
-        await this.token.$_mint(holder, supply);
-        expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS);
-
-        const { receipt } = await this.token.delegate(holder, { from: holder });
-        expectEvent(receipt, 'DelegateChanged', {
-          delegator: holder,
-          fromDelegate: ZERO_ADDRESS,
-          toDelegate: holder,
-        });
-        expectEvent(receipt, 'DelegateVotesChanged', {
-          delegate: holder,
-          previousBalance: '0',
-          newBalance: supply,
-        });
-
-        expect(await this.token.delegates(holder)).to.be.equal(holder);
-
-        expect(await this.token.getVotes(holder)).to.be.bignumber.equal(supply);
-        expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('0');
-        await time.advanceBlock();
-        expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal(supply);
-      });
-
-      it('delegation without balance', async function () {
-        expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS);
-
-        const { receipt } = await this.token.delegate(holder, { from: holder });
-        expectEvent(receipt, 'DelegateChanged', {
-          delegator: holder,
-          fromDelegate: ZERO_ADDRESS,
-          toDelegate: holder,
-        });
-        expectEvent.notEmitted(receipt, 'DelegateVotesChanged');
-
-        expect(await this.token.delegates(holder)).to.be.equal(holder);
-      });
-    });
-
-    describe('with signature', function () {
-      const delegator = Wallet.generate();
-      const delegatorAddress = web3.utils.toChecksumAddress(delegator.getAddressString());
-      const nonce = 0;
-
-      const buildData = (contract, message) =>
-        getDomain(contract).then(domain => ({
-          primaryType: 'Delegation',
-          types: { EIP712Domain: domainType(domain), Delegation },
-          domain,
-          message,
-        }));
-
+  for (const [mode, artifact] of Object.entries(MODES)) {
+    describe(`vote with ${mode}`, function () {
       beforeEach(async function () {
-        await this.token.$_mint(delegatorAddress, supply);
+        this.token = await artifact.new(name, symbol, name);
       });
 
-      it('accept signed delegation', async function () {
-        const { v, r, s } = await buildData(this.token, {
-          delegatee: delegatorAddress,
-          nonce,
-          expiry: MAX_UINT256,
-        })
-          .then(data => ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))
-          .then(fromRpcSig);
-
-        expect(await this.token.delegates(delegatorAddress)).to.be.equal(ZERO_ADDRESS);
-
-        const { receipt } = await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s);
-        expectEvent(receipt, 'DelegateChanged', {
-          delegator: delegatorAddress,
-          fromDelegate: ZERO_ADDRESS,
-          toDelegate: delegatorAddress,
-        });
-        expectEvent(receipt, 'DelegateVotesChanged', {
-          delegate: delegatorAddress,
-          previousBalance: '0',
-          newBalance: supply,
-        });
-
-        expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress);
+      shouldBehaveLikeEIP6372(mode);
 
-        expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal(supply);
-        expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber - 1)).to.be.bignumber.equal('0');
-        await time.advanceBlock();
-        expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal(supply);
+      it('initial nonce is 0', async function () {
+        expect(await this.token.nonces(holder)).to.be.bignumber.equal('0');
       });
 
-      it('rejects reused signature', async function () {
-        const { v, r, s } = await buildData(this.token, {
-          delegatee: delegatorAddress,
-          nonce,
-          expiry: MAX_UINT256,
-        })
-          .then(data => ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))
-          .then(fromRpcSig);
-
-        await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s);
-
-        await expectRevert(
-          this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s),
-          'ERC20Votes: invalid nonce',
-        );
+      it('domain separator', async function () {
+        expect(await this.token.DOMAIN_SEPARATOR()).to.equal(await getDomain(this.token).then(domainSeparator));
       });
 
-      it('rejects bad delegatee', async function () {
-        const { v, r, s } = await buildData(this.token, {
-          delegatee: delegatorAddress,
-          nonce,
-          expiry: MAX_UINT256,
-        })
-          .then(data => ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))
-          .then(fromRpcSig);
-
-        const receipt = await this.token.delegateBySig(holderDelegatee, nonce, MAX_UINT256, v, r, s);
-        const { args } = receipt.logs.find(({ event }) => event == 'DelegateChanged');
-        expect(args.delegator).to.not.be.equal(delegatorAddress);
-        expect(args.fromDelegate).to.be.equal(ZERO_ADDRESS);
-        expect(args.toDelegate).to.be.equal(holderDelegatee);
+      it('minting restriction', async function () {
+        const amount = new BN('2').pow(new BN('224'));
+        await expectRevert(this.token.$_mint(holder, amount), 'ERC20Votes: total supply risks overflowing votes');
       });
 
-      it('rejects bad nonce', async function () {
-        const { v, r, s } = await buildData(this.token, {
-          delegatee: delegatorAddress,
-          nonce,
-          expiry: MAX_UINT256,
-        })
-          .then(data => ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))
-          .then(fromRpcSig);
-
-        await expectRevert(
-          this.token.delegateBySig(delegatorAddress, nonce + 1, MAX_UINT256, v, r, s),
-          'ERC20Votes: invalid nonce',
-        );
+      it('recent checkpoints', async function () {
+        await this.token.delegate(holder, { from: holder });
+        for (let i = 0; i < 6; i++) {
+          await this.token.$_mint(holder, 1);
+        }
+        const block = await clock[mode]();
+        expect(await this.token.numCheckpoints(holder)).to.be.bignumber.equal('6');
+        // recent
+        expect(await this.token.getPastVotes(holder, block - 1)).to.be.bignumber.equal('5');
+        // non-recent
+        expect(await this.token.getPastVotes(holder, block - 6)).to.be.bignumber.equal('0');
       });
 
-      it('rejects expired permit', async function () {
-        const expiry = (await time.latest()) - time.duration.weeks(1);
-
-        const { v, r, s } = await buildData(this.token, {
-          delegatee: delegatorAddress,
-          nonce,
-          expiry,
-        })
-          .then(data => ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))
-          .then(fromRpcSig);
-
-        await expectRevert(
-          this.token.delegateBySig(delegatorAddress, nonce, expiry, v, r, s),
-          'ERC20Votes: signature expired',
-        );
-      });
-    });
-  });
-
-  describe('change delegation', function () {
-    beforeEach(async function () {
-      await this.token.$_mint(holder, supply);
-      await this.token.delegate(holder, { from: holder });
-    });
-
-    it('call', async function () {
-      expect(await this.token.delegates(holder)).to.be.equal(holder);
-
-      const { receipt } = await this.token.delegate(holderDelegatee, { from: holder });
-      expectEvent(receipt, 'DelegateChanged', {
-        delegator: holder,
-        fromDelegate: holder,
-        toDelegate: holderDelegatee,
-      });
-      expectEvent(receipt, 'DelegateVotesChanged', {
-        delegate: holder,
-        previousBalance: supply,
-        newBalance: '0',
-      });
-      expectEvent(receipt, 'DelegateVotesChanged', {
-        delegate: holderDelegatee,
-        previousBalance: '0',
-        newBalance: supply,
-      });
-
-      expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee);
-
-      expect(await this.token.getVotes(holder)).to.be.bignumber.equal('0');
-      expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal(supply);
-      expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal(supply);
-      expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber - 1)).to.be.bignumber.equal('0');
-      await time.advanceBlock();
-      expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('0');
-      expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal(supply);
-    });
-  });
-
-  describe('transfers', function () {
-    beforeEach(async function () {
-      await this.token.$_mint(holder, supply);
-    });
-
-    it('no delegation', async function () {
-      const { receipt } = await this.token.transfer(recipient, 1, { from: holder });
-      expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' });
-      expectEvent.notEmitted(receipt, 'DelegateVotesChanged');
-
-      this.holderVotes = '0';
-      this.recipientVotes = '0';
-    });
-
-    it('sender delegation', async function () {
-      await this.token.delegate(holder, { from: holder });
+      describe('set delegation', function () {
+        describe('call', function () {
+          it('delegation with balance', async function () {
+            await this.token.$_mint(holder, supply);
+            expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS);
+
+            const { receipt } = await this.token.delegate(holder, { from: holder });
+            const timepoint = await clockFromReceipt[mode](receipt);
+
+            expectEvent(receipt, 'DelegateChanged', {
+              delegator: holder,
+              fromDelegate: ZERO_ADDRESS,
+              toDelegate: holder,
+            });
+            expectEvent(receipt, 'DelegateVotesChanged', {
+              delegate: holder,
+              previousBalance: '0',
+              newBalance: supply,
+            });
+
+            expect(await this.token.delegates(holder)).to.be.equal(holder);
+
+            expect(await this.token.getVotes(holder)).to.be.bignumber.equal(supply);
+            expect(await this.token.getPastVotes(holder, timepoint - 1)).to.be.bignumber.equal('0');
+            await time.advanceBlock();
+            expect(await this.token.getPastVotes(holder, timepoint)).to.be.bignumber.equal(supply);
+          });
+
+          it('delegation without balance', async function () {
+            expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS);
+
+            const { receipt } = await this.token.delegate(holder, { from: holder });
+            expectEvent(receipt, 'DelegateChanged', {
+              delegator: holder,
+              fromDelegate: ZERO_ADDRESS,
+              toDelegate: holder,
+            });
+            expectEvent.notEmitted(receipt, 'DelegateVotesChanged');
+
+            expect(await this.token.delegates(holder)).to.be.equal(holder);
+          });
+        });
 
-      const { receipt } = await this.token.transfer(recipient, 1, { from: holder });
-      expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' });
-      expectEvent(receipt, 'DelegateVotesChanged', {
-        delegate: holder,
-        previousBalance: supply,
-        newBalance: supply.subn(1),
+        describe('with signature', function () {
+          const delegator = Wallet.generate();
+          const delegatorAddress = web3.utils.toChecksumAddress(delegator.getAddressString());
+          const nonce = 0;
+
+          const buildData = (contract, message) =>
+            getDomain(contract).then(domain => ({
+              primaryType: 'Delegation',
+              types: { EIP712Domain: domainType(domain), Delegation },
+              domain,
+              message,
+            }));
+
+          beforeEach(async function () {
+            await this.token.$_mint(delegatorAddress, supply);
+          });
+
+          it('accept signed delegation', async function () {
+            const { v, r, s } = await buildData(this.token, {
+              delegatee: delegatorAddress,
+              nonce,
+              expiry: MAX_UINT256,
+            }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })));
+
+            expect(await this.token.delegates(delegatorAddress)).to.be.equal(ZERO_ADDRESS);
+
+            const { receipt } = await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s);
+            const timepoint = await clockFromReceipt[mode](receipt);
+
+            expectEvent(receipt, 'DelegateChanged', {
+              delegator: delegatorAddress,
+              fromDelegate: ZERO_ADDRESS,
+              toDelegate: delegatorAddress,
+            });
+            expectEvent(receipt, 'DelegateVotesChanged', {
+              delegate: delegatorAddress,
+              previousBalance: '0',
+              newBalance: supply,
+            });
+
+            expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress);
+
+            expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal(supply);
+            expect(await this.token.getPastVotes(delegatorAddress, timepoint - 1)).to.be.bignumber.equal('0');
+            await time.advanceBlock();
+            expect(await this.token.getPastVotes(delegatorAddress, timepoint)).to.be.bignumber.equal(supply);
+          });
+
+          it('rejects reused signature', async function () {
+            const { v, r, s } = await buildData(this.token, {
+              delegatee: delegatorAddress,
+              nonce,
+              expiry: MAX_UINT256,
+            }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })));
+
+            await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s);
+
+            await expectRevert(
+              this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s),
+              'ERC20Votes: invalid nonce',
+            );
+          });
+
+          it('rejects bad delegatee', async function () {
+            const { v, r, s } = await buildData(this.token, {
+              delegatee: delegatorAddress,
+              nonce,
+              expiry: MAX_UINT256,
+            }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })));
+
+            const receipt = await this.token.delegateBySig(holderDelegatee, nonce, MAX_UINT256, v, r, s);
+            const { args } = receipt.logs.find(({ event }) => event == 'DelegateChanged');
+            expect(args.delegator).to.not.be.equal(delegatorAddress);
+            expect(args.fromDelegate).to.be.equal(ZERO_ADDRESS);
+            expect(args.toDelegate).to.be.equal(holderDelegatee);
+          });
+
+          it('rejects bad nonce', async function () {
+            const { v, r, s } = await buildData(this.token, {
+              delegatee: delegatorAddress,
+              nonce,
+              expiry: MAX_UINT256,
+            }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })));
+
+            await expectRevert(
+              this.token.delegateBySig(delegatorAddress, nonce + 1, MAX_UINT256, v, r, s),
+              'ERC20Votes: invalid nonce',
+            );
+          });
+
+          it('rejects expired permit', async function () {
+            const expiry = (await time.latest()) - time.duration.weeks(1);
+            const { v, r, s } = await buildData(this.token, {
+              delegatee: delegatorAddress,
+              nonce,
+              expiry,
+            }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })));
+
+            await expectRevert(
+              this.token.delegateBySig(delegatorAddress, nonce, expiry, v, r, s),
+              'ERC20Votes: signature expired',
+            );
+          });
+        });
       });
 
-      const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer');
-      expect(
-        receipt.logs
-          .filter(({ event }) => event == 'DelegateVotesChanged')
-          .every(({ logIndex }) => transferLogIndex < logIndex),
-      ).to.be.equal(true);
-
-      this.holderVotes = supply.subn(1);
-      this.recipientVotes = '0';
-    });
-
-    it('receiver delegation', async function () {
-      await this.token.delegate(recipient, { from: recipient });
-
-      const { receipt } = await this.token.transfer(recipient, 1, { from: holder });
-      expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' });
-      expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' });
-
-      const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer');
-      expect(
-        receipt.logs
-          .filter(({ event }) => event == 'DelegateVotesChanged')
-          .every(({ logIndex }) => transferLogIndex < logIndex),
-      ).to.be.equal(true);
-
-      this.holderVotes = '0';
-      this.recipientVotes = '1';
-    });
-
-    it('full delegation', async function () {
-      await this.token.delegate(holder, { from: holder });
-      await this.token.delegate(recipient, { from: recipient });
+      describe('change delegation', function () {
+        beforeEach(async function () {
+          await this.token.$_mint(holder, supply);
+          await this.token.delegate(holder, { from: holder });
+        });
 
-      const { receipt } = await this.token.transfer(recipient, 1, { from: holder });
-      expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' });
-      expectEvent(receipt, 'DelegateVotesChanged', {
-        delegate: holder,
-        previousBalance: supply,
-        newBalance: supply.subn(1),
+        it('call', async function () {
+          expect(await this.token.delegates(holder)).to.be.equal(holder);
+
+          const { receipt } = await this.token.delegate(holderDelegatee, { from: holder });
+          const timepoint = await clockFromReceipt[mode](receipt);
+
+          expectEvent(receipt, 'DelegateChanged', {
+            delegator: holder,
+            fromDelegate: holder,
+            toDelegate: holderDelegatee,
+          });
+          expectEvent(receipt, 'DelegateVotesChanged', {
+            delegate: holder,
+            previousBalance: supply,
+            newBalance: '0',
+          });
+          expectEvent(receipt, 'DelegateVotesChanged', {
+            delegate: holderDelegatee,
+            previousBalance: '0',
+            newBalance: supply,
+          });
+
+          expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee);
+
+          expect(await this.token.getVotes(holder)).to.be.bignumber.equal('0');
+          expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal(supply);
+          expect(await this.token.getPastVotes(holder, timepoint - 1)).to.be.bignumber.equal(supply);
+          expect(await this.token.getPastVotes(holderDelegatee, timepoint - 1)).to.be.bignumber.equal('0');
+          await time.advanceBlock();
+          expect(await this.token.getPastVotes(holder, timepoint)).to.be.bignumber.equal('0');
+          expect(await this.token.getPastVotes(holderDelegatee, timepoint)).to.be.bignumber.equal(supply);
+        });
       });
-      expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' });
-
-      const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer');
-      expect(
-        receipt.logs
-          .filter(({ event }) => event == 'DelegateVotesChanged')
-          .every(({ logIndex }) => transferLogIndex < logIndex),
-      ).to.be.equal(true);
 
-      this.holderVotes = supply.subn(1);
-      this.recipientVotes = '1';
-    });
-
-    afterEach(async function () {
-      expect(await this.token.getVotes(holder)).to.be.bignumber.equal(this.holderVotes);
-      expect(await this.token.getVotes(recipient)).to.be.bignumber.equal(this.recipientVotes);
-
-      // need to advance 2 blocks to see the effect of a transfer on "getPastVotes"
-      const blockNumber = await time.latestBlock();
-      await time.advanceBlock();
-      expect(await this.token.getPastVotes(holder, blockNumber)).to.be.bignumber.equal(this.holderVotes);
-      expect(await this.token.getPastVotes(recipient, blockNumber)).to.be.bignumber.equal(this.recipientVotes);
-    });
-  });
-
-  // The following tests are a adaptation of https://github.com/compound-finance/compound-protocol/blob/master/tests/Governance/CompTest.js.
-  describe('Compound test suite', function () {
-    beforeEach(async function () {
-      await this.token.$_mint(holder, supply);
-    });
+      describe('transfers', function () {
+        beforeEach(async function () {
+          await this.token.$_mint(holder, supply);
+        });
 
-    describe('balanceOf', function () {
-      it('grants to initial account', async function () {
-        expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('10000000000000000000000000');
-      });
-    });
+        it('no delegation', async function () {
+          const { receipt } = await this.token.transfer(recipient, 1, { from: holder });
+          expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' });
+          expectEvent.notEmitted(receipt, 'DelegateVotesChanged');
 
-    describe('numCheckpoints', function () {
-      it('returns the number of checkpoints for a delegate', async function () {
-        await this.token.transfer(recipient, '100', { from: holder }); //give an account a few tokens for readability
-        expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0');
+          this.holderVotes = '0';
+          this.recipientVotes = '0';
+        });
 
-        const t1 = await this.token.delegate(other1, { from: recipient });
-        expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1');
+        it('sender delegation', async function () {
+          await this.token.delegate(holder, { from: holder });
+
+          const { receipt } = await this.token.transfer(recipient, 1, { from: holder });
+          expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' });
+          expectEvent(receipt, 'DelegateVotesChanged', {
+            delegate: holder,
+            previousBalance: supply,
+            newBalance: supply.subn(1),
+          });
+
+          const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer');
+          expect(
+            receipt.logs
+              .filter(({ event }) => event == 'DelegateVotesChanged')
+              .every(({ logIndex }) => transferLogIndex < logIndex),
+          ).to.be.equal(true);
+
+          this.holderVotes = supply.subn(1);
+          this.recipientVotes = '0';
+        });
 
-        const t2 = await this.token.transfer(other2, 10, { from: recipient });
-        expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2');
+        it('receiver delegation', async function () {
+          await this.token.delegate(recipient, { from: recipient });
 
-        const t3 = await this.token.transfer(other2, 10, { from: recipient });
-        expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3');
+          const { receipt } = await this.token.transfer(recipient, 1, { from: holder });
+          expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' });
+          expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' });
 
-        const t4 = await this.token.transfer(recipient, 20, { from: holder });
-        expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4');
+          const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer');
+          expect(
+            receipt.logs
+              .filter(({ event }) => event == 'DelegateVotesChanged')
+              .every(({ logIndex }) => transferLogIndex < logIndex),
+          ).to.be.equal(true);
 
-        expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([t1.receipt.blockNumber.toString(), '100']);
-        expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([t2.receipt.blockNumber.toString(), '90']);
-        expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([t3.receipt.blockNumber.toString(), '80']);
-        expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([t4.receipt.blockNumber.toString(), '100']);
+          this.holderVotes = '0';
+          this.recipientVotes = '1';
+        });
 
-        await time.advanceBlock();
-        expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('100');
-        expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('90');
-        expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('80');
-        expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('100');
-      });
+        it('full delegation', async function () {
+          await this.token.delegate(holder, { from: holder });
+          await this.token.delegate(recipient, { from: recipient });
+
+          const { receipt } = await this.token.transfer(recipient, 1, { from: holder });
+          expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' });
+          expectEvent(receipt, 'DelegateVotesChanged', {
+            delegate: holder,
+            previousBalance: supply,
+            newBalance: supply.subn(1),
+          });
+          expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' });
+
+          const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer');
+          expect(
+            receipt.logs
+              .filter(({ event }) => event == 'DelegateVotesChanged')
+              .every(({ logIndex }) => transferLogIndex < logIndex),
+          ).to.be.equal(true);
+
+          this.holderVotes = supply.subn(1);
+          this.recipientVotes = '1';
+        });
 
-      it('does not add more than one checkpoint in a block', async function () {
-        await this.token.transfer(recipient, '100', { from: holder });
-        expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0');
-
-        const [t1, t2, t3] = await batchInBlock([
-          () => this.token.delegate(other1, { from: recipient, gas: 100000 }),
-          () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }),
-          () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }),
-        ]);
-        expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1');
-        expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([t1.receipt.blockNumber.toString(), '80']);
-        // expectReve(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check
-        // expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check
-
-        const t4 = await this.token.transfer(recipient, 20, { from: holder });
-        expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2');
-        expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([t4.receipt.blockNumber.toString(), '100']);
-      });
-    });
+        afterEach(async function () {
+          expect(await this.token.getVotes(holder)).to.be.bignumber.equal(this.holderVotes);
+          expect(await this.token.getVotes(recipient)).to.be.bignumber.equal(this.recipientVotes);
 
-    describe('getPastVotes', function () {
-      it('reverts if block number >= current block', async function () {
-        await expectRevert(this.token.getPastVotes(other1, 5e10), 'ERC20Votes: block not yet mined');
-      });
-
-      it('returns 0 if there are no checkpoints', async function () {
-        expect(await this.token.getPastVotes(other1, 0)).to.be.bignumber.equal('0');
+          // need to advance 2 blocks to see the effect of a transfer on "getPastVotes"
+          const timepoint = await clock[mode]();
+          await time.advanceBlock();
+          expect(await this.token.getPastVotes(holder, timepoint)).to.be.bignumber.equal(this.holderVotes);
+          expect(await this.token.getPastVotes(recipient, timepoint)).to.be.bignumber.equal(this.recipientVotes);
+        });
       });
 
-      it('returns the latest block if >= last checkpoint block', async function () {
-        const t1 = await this.token.delegate(other1, { from: holder });
-        await time.advanceBlock();
-        await time.advanceBlock();
-
-        expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal(
-          '10000000000000000000000000',
-        );
-        expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal(
-          '10000000000000000000000000',
-        );
-      });
+      // The following tests are a adaptation of https://github.com/compound-finance/compound-protocol/blob/master/tests/Governance/CompTest.js.
+      describe('Compound test suite', function () {
+        beforeEach(async function () {
+          await this.token.$_mint(holder, supply);
+        });
 
-      it('returns zero if < first checkpoint block', async function () {
-        await time.advanceBlock();
-        const t1 = await this.token.delegate(other1, { from: holder });
-        await time.advanceBlock();
-        await time.advanceBlock();
+        describe('balanceOf', function () {
+          it('grants to initial account', async function () {
+            expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('10000000000000000000000000');
+          });
+        });
 
-        expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0');
-        expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal(
-          '10000000000000000000000000',
-        );
-      });
+        describe('numCheckpoints', function () {
+          it('returns the number of checkpoints for a delegate', async function () {
+            await this.token.transfer(recipient, '100', { from: holder }); //give an account a few tokens for readability
+            expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0');
+
+            const t1 = await this.token.delegate(other1, { from: recipient });
+            t1.timepoint = await clockFromReceipt[mode](t1.receipt);
+            expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1');
+
+            const t2 = await this.token.transfer(other2, 10, { from: recipient });
+            t2.timepoint = await clockFromReceipt[mode](t2.receipt);
+            expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2');
+
+            const t3 = await this.token.transfer(other2, 10, { from: recipient });
+            t3.timepoint = await clockFromReceipt[mode](t3.receipt);
+            expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3');
+
+            const t4 = await this.token.transfer(recipient, 20, { from: holder });
+            t4.timepoint = await clockFromReceipt[mode](t4.receipt);
+            expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4');
+
+            expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([t1.timepoint.toString(), '100']);
+            expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([t2.timepoint.toString(), '90']);
+            expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([t3.timepoint.toString(), '80']);
+            expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([t4.timepoint.toString(), '100']);
+
+            await time.advanceBlock();
+            expect(await this.token.getPastVotes(other1, t1.timepoint)).to.be.bignumber.equal('100');
+            expect(await this.token.getPastVotes(other1, t2.timepoint)).to.be.bignumber.equal('90');
+            expect(await this.token.getPastVotes(other1, t3.timepoint)).to.be.bignumber.equal('80');
+            expect(await this.token.getPastVotes(other1, t4.timepoint)).to.be.bignumber.equal('100');
+          });
+
+          it('does not add more than one checkpoint in a block', async function () {
+            await this.token.transfer(recipient, '100', { from: holder });
+            expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0');
+
+            const [t1, t2, t3] = await batchInBlock([
+              () => this.token.delegate(other1, { from: recipient, gas: 100000 }),
+              () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }),
+              () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }),
+            ]);
+            t1.timepoint = await clockFromReceipt[mode](t1.receipt);
+            t2.timepoint = await clockFromReceipt[mode](t2.receipt);
+            t3.timepoint = await clockFromReceipt[mode](t3.receipt);
+
+            expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1');
+            expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([t1.timepoint.toString(), '80']);
+
+            const t4 = await this.token.transfer(recipient, 20, { from: holder });
+            t4.timepoint = await clockFromReceipt[mode](t4.receipt);
+
+            expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2');
+            expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([t4.timepoint.toString(), '100']);
+          });
+        });
 
-      it('generally returns the voting balance at the appropriate checkpoint', async function () {
-        const t1 = await this.token.delegate(other1, { from: holder });
-        await time.advanceBlock();
-        await time.advanceBlock();
-        const t2 = await this.token.transfer(other2, 10, { from: holder });
-        await time.advanceBlock();
-        await time.advanceBlock();
-        const t3 = await this.token.transfer(other2, 10, { from: holder });
-        await time.advanceBlock();
-        await time.advanceBlock();
-        const t4 = await this.token.transfer(holder, 20, { from: other2 });
-        await time.advanceBlock();
-        await time.advanceBlock();
-
-        expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0');
-        expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal(
-          '10000000000000000000000000',
-        );
-        expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal(
-          '10000000000000000000000000',
-        );
-        expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal(
-          '9999999999999999999999990',
-        );
-        expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber + 1)).to.be.bignumber.equal(
-          '9999999999999999999999990',
-        );
-        expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal(
-          '9999999999999999999999980',
-        );
-        expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal(
-          '9999999999999999999999980',
-        );
-        expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal(
-          '10000000000000000000000000',
-        );
-        expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal(
-          '10000000000000000000000000',
-        );
+        describe('getPastVotes', function () {
+          it('reverts if block number >= current block', async function () {
+            await expectRevert(this.token.getPastVotes(other1, 5e10), 'ERC20Votes: future lookup');
+          });
+
+          it('returns 0 if there are no checkpoints', async function () {
+            expect(await this.token.getPastVotes(other1, 0)).to.be.bignumber.equal('0');
+          });
+
+          it('returns the latest block if >= last checkpoint block', async function () {
+            const { receipt } = await this.token.delegate(other1, { from: holder });
+            const timepoint = await clockFromReceipt[mode](receipt);
+            await time.advanceBlock();
+            await time.advanceBlock();
+
+            expect(await this.token.getPastVotes(other1, timepoint)).to.be.bignumber.equal(
+              '10000000000000000000000000',
+            );
+            expect(await this.token.getPastVotes(other1, timepoint + 1)).to.be.bignumber.equal(
+              '10000000000000000000000000',
+            );
+          });
+
+          it('returns zero if < first checkpoint block', async function () {
+            await time.advanceBlock();
+            const { receipt } = await this.token.delegate(other1, { from: holder });
+            const timepoint = await clockFromReceipt[mode](receipt);
+            await time.advanceBlock();
+            await time.advanceBlock();
+
+            expect(await this.token.getPastVotes(other1, timepoint - 1)).to.be.bignumber.equal('0');
+            expect(await this.token.getPastVotes(other1, timepoint + 1)).to.be.bignumber.equal(
+              '10000000000000000000000000',
+            );
+          });
+
+          it('generally returns the voting balance at the appropriate checkpoint', async function () {
+            const t1 = await this.token.delegate(other1, { from: holder });
+            await time.advanceBlock();
+            await time.advanceBlock();
+            const t2 = await this.token.transfer(other2, 10, { from: holder });
+            await time.advanceBlock();
+            await time.advanceBlock();
+            const t3 = await this.token.transfer(other2, 10, { from: holder });
+            await time.advanceBlock();
+            await time.advanceBlock();
+            const t4 = await this.token.transfer(holder, 20, { from: other2 });
+            await time.advanceBlock();
+            await time.advanceBlock();
+
+            t1.timepoint = await clockFromReceipt[mode](t1.receipt);
+            t2.timepoint = await clockFromReceipt[mode](t2.receipt);
+            t3.timepoint = await clockFromReceipt[mode](t3.receipt);
+            t4.timepoint = await clockFromReceipt[mode](t4.receipt);
+
+            expect(await this.token.getPastVotes(other1, t1.timepoint - 1)).to.be.bignumber.equal('0');
+            expect(await this.token.getPastVotes(other1, t1.timepoint)).to.be.bignumber.equal(
+              '10000000000000000000000000',
+            );
+            expect(await this.token.getPastVotes(other1, t1.timepoint + 1)).to.be.bignumber.equal(
+              '10000000000000000000000000',
+            );
+            expect(await this.token.getPastVotes(other1, t2.timepoint)).to.be.bignumber.equal(
+              '9999999999999999999999990',
+            );
+            expect(await this.token.getPastVotes(other1, t2.timepoint + 1)).to.be.bignumber.equal(
+              '9999999999999999999999990',
+            );
+            expect(await this.token.getPastVotes(other1, t3.timepoint)).to.be.bignumber.equal(
+              '9999999999999999999999980',
+            );
+            expect(await this.token.getPastVotes(other1, t3.timepoint + 1)).to.be.bignumber.equal(
+              '9999999999999999999999980',
+            );
+            expect(await this.token.getPastVotes(other1, t4.timepoint)).to.be.bignumber.equal(
+              '10000000000000000000000000',
+            );
+            expect(await this.token.getPastVotes(other1, t4.timepoint + 1)).to.be.bignumber.equal(
+              '10000000000000000000000000',
+            );
+          });
+        });
       });
-    });
-  });
-
-  describe('getPastTotalSupply', function () {
-    beforeEach(async function () {
-      await this.token.delegate(holder, { from: holder });
-    });
 
-    it('reverts if block number >= current block', async function () {
-      await expectRevert(this.token.getPastTotalSupply(5e10), 'ERC20Votes: block not yet mined');
-    });
+      describe('getPastTotalSupply', function () {
+        beforeEach(async function () {
+          await this.token.delegate(holder, { from: holder });
+        });
 
-    it('returns 0 if there are no checkpoints', async function () {
-      expect(await this.token.getPastTotalSupply(0)).to.be.bignumber.equal('0');
-    });
+        it('reverts if block number >= current block', async function () {
+          await expectRevert(this.token.getPastTotalSupply(5e10), 'ERC20Votes: future lookup');
+        });
 
-    it('returns the latest block if >= last checkpoint block', async function () {
-      t1 = await this.token.$_mint(holder, supply);
+        it('returns 0 if there are no checkpoints', async function () {
+          expect(await this.token.getPastTotalSupply(0)).to.be.bignumber.equal('0');
+        });
 
-      await time.advanceBlock();
-      await time.advanceBlock();
+        it('returns the latest block if >= last checkpoint block', async function () {
+          const { receipt } = await this.token.$_mint(holder, supply);
+          const timepoint = await clockFromReceipt[mode](receipt);
 
-      expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal(supply);
-      expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal(supply);
-    });
+          await time.advanceBlock();
+          await time.advanceBlock();
 
-    it('returns zero if < first checkpoint block', async function () {
-      await time.advanceBlock();
-      const t1 = await this.token.$_mint(holder, supply);
-      await time.advanceBlock();
-      await time.advanceBlock();
+          expect(await this.token.getPastTotalSupply(timepoint)).to.be.bignumber.equal(supply);
+          expect(await this.token.getPastTotalSupply(timepoint + 1)).to.be.bignumber.equal(supply);
+        });
 
-      expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0');
-      expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal(
-        '10000000000000000000000000',
-      );
-    });
+        it('returns zero if < first checkpoint block', async function () {
+          await time.advanceBlock();
+          const { receipt } = await this.token.$_mint(holder, supply);
+          const timepoint = await clockFromReceipt[mode](receipt);
+          await time.advanceBlock();
+          await time.advanceBlock();
+
+          expect(await this.token.getPastTotalSupply(timepoint - 1)).to.be.bignumber.equal('0');
+          expect(await this.token.getPastTotalSupply(timepoint + 1)).to.be.bignumber.equal(
+            '10000000000000000000000000',
+          );
+        });
 
-    it('generally returns the voting balance at the appropriate checkpoint', async function () {
-      const t1 = await this.token.$_mint(holder, supply);
-      await time.advanceBlock();
-      await time.advanceBlock();
-      const t2 = await this.token.$_burn(holder, 10);
-      await time.advanceBlock();
-      await time.advanceBlock();
-      const t3 = await this.token.$_burn(holder, 10);
-      await time.advanceBlock();
-      await time.advanceBlock();
-      const t4 = await this.token.$_mint(holder, 20);
-      await time.advanceBlock();
-      await time.advanceBlock();
-
-      expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0');
-      expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal(
-        '10000000000000000000000000',
-      );
-      expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal(
-        '10000000000000000000000000',
-      );
-      expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal(
-        '9999999999999999999999990',
-      );
-      expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal(
-        '9999999999999999999999990',
-      );
-      expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal(
-        '9999999999999999999999980',
-      );
-      expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal(
-        '9999999999999999999999980',
-      );
-      expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal(
-        '10000000000000000000000000',
-      );
-      expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal(
-        '10000000000000000000000000',
-      );
+        it('generally returns the voting balance at the appropriate checkpoint', async function () {
+          const t1 = await this.token.$_mint(holder, supply);
+          await time.advanceBlock();
+          await time.advanceBlock();
+          const t2 = await this.token.$_burn(holder, 10);
+          await time.advanceBlock();
+          await time.advanceBlock();
+          const t3 = await this.token.$_burn(holder, 10);
+          await time.advanceBlock();
+          await time.advanceBlock();
+          const t4 = await this.token.$_mint(holder, 20);
+          await time.advanceBlock();
+          await time.advanceBlock();
+
+          t1.timepoint = await clockFromReceipt[mode](t1.receipt);
+          t2.timepoint = await clockFromReceipt[mode](t2.receipt);
+          t3.timepoint = await clockFromReceipt[mode](t3.receipt);
+          t4.timepoint = await clockFromReceipt[mode](t4.receipt);
+
+          expect(await this.token.getPastTotalSupply(t1.timepoint - 1)).to.be.bignumber.equal('0');
+          expect(await this.token.getPastTotalSupply(t1.timepoint)).to.be.bignumber.equal('10000000000000000000000000');
+          expect(await this.token.getPastTotalSupply(t1.timepoint + 1)).to.be.bignumber.equal(
+            '10000000000000000000000000',
+          );
+          expect(await this.token.getPastTotalSupply(t2.timepoint)).to.be.bignumber.equal('9999999999999999999999990');
+          expect(await this.token.getPastTotalSupply(t2.timepoint + 1)).to.be.bignumber.equal(
+            '9999999999999999999999990',
+          );
+          expect(await this.token.getPastTotalSupply(t3.timepoint)).to.be.bignumber.equal('9999999999999999999999980');
+          expect(await this.token.getPastTotalSupply(t3.timepoint + 1)).to.be.bignumber.equal(
+            '9999999999999999999999980',
+          );
+          expect(await this.token.getPastTotalSupply(t4.timepoint)).to.be.bignumber.equal('10000000000000000000000000');
+          expect(await this.token.getPastTotalSupply(t4.timepoint + 1)).to.be.bignumber.equal(
+            '10000000000000000000000000',
+          );
+        });
+      });
     });
-  });
+  }
 });

+ 474 - 457
test/token/ERC20/extensions/ERC20VotesComp.test.js

@@ -8,11 +8,11 @@ const { fromRpcSig } = require('ethereumjs-util');
 const ethSigUtil = require('eth-sig-util');
 const Wallet = require('ethereumjs-wallet').default;
 
-const ERC20VotesComp = artifacts.require('$ERC20VotesComp');
-
 const { batchInBlock } = require('../../../helpers/txpool');
 const { getDomain, domainType, domainSeparator } = require('../../../helpers/eip712');
-const { getChainId } = require('../../../helpers/chainid');
+const { clock, clockFromReceipt } = require('../../../helpers/time');
+
+const { shouldBehaveLikeEIP6372 } = require('../../../governance/utils/EIP6372.behavior');
 
 const Delegation = [
   { name: 'delegatee', type: 'address' },
@@ -20,507 +20,524 @@ const Delegation = [
   { name: 'expiry', type: 'uint256' },
 ];
 
+const MODES = {
+  blocknumber: artifacts.require('$ERC20VotesComp'),
+  // no timestamp mode for ERC20VotesComp yet
+};
+
 contract('ERC20VotesComp', function (accounts) {
   const [holder, recipient, holderDelegatee, other1, other2] = accounts;
 
   const name = 'My Token';
   const symbol = 'MTKN';
-  const version = '1';
   const supply = new BN('10000000000000000000000000');
 
-  beforeEach(async function () {
-    this.chainId = await getChainId();
-    this.token = await ERC20VotesComp.new(name, symbol, name);
-  });
-
-  it('initial nonce is 0', async function () {
-    expect(await this.token.nonces(holder)).to.be.bignumber.equal('0');
-  });
-
-  it('domain separator', async function () {
-    expect(await this.token.DOMAIN_SEPARATOR()).to.equal(await getDomain(this.token).then(domainSeparator));
-  });
-
-  it('minting restriction', async function () {
-    const amount = new BN('2').pow(new BN('96'));
-    await expectRevert(this.token.$_mint(holder, amount), 'ERC20Votes: total supply risks overflowing votes');
-  });
-
-  describe('set delegation', function () {
-    describe('call', function () {
-      it('delegation with balance', async function () {
-        await this.token.$_mint(holder, supply);
-        expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS);
-
-        const { receipt } = await this.token.delegate(holder, { from: holder });
-        expectEvent(receipt, 'DelegateChanged', {
-          delegator: holder,
-          fromDelegate: ZERO_ADDRESS,
-          toDelegate: holder,
-        });
-        expectEvent(receipt, 'DelegateVotesChanged', {
-          delegate: holder,
-          previousBalance: '0',
-          newBalance: supply,
-        });
-
-        expect(await this.token.delegates(holder)).to.be.equal(holder);
-
-        expect(await this.token.getCurrentVotes(holder)).to.be.bignumber.equal(supply);
-        expect(await this.token.getPriorVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('0');
-        await time.advanceBlock();
-        expect(await this.token.getPriorVotes(holder, receipt.blockNumber)).to.be.bignumber.equal(supply);
-      });
-
-      it('delegation without balance', async function () {
-        expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS);
-
-        const { receipt } = await this.token.delegate(holder, { from: holder });
-        expectEvent(receipt, 'DelegateChanged', {
-          delegator: holder,
-          fromDelegate: ZERO_ADDRESS,
-          toDelegate: holder,
-        });
-        expectEvent.notEmitted(receipt, 'DelegateVotesChanged');
-
-        expect(await this.token.delegates(holder)).to.be.equal(holder);
-      });
-    });
-
-    describe('with signature', function () {
-      const delegator = Wallet.generate();
-      const delegatorAddress = web3.utils.toChecksumAddress(delegator.getAddressString());
-      const nonce = 0;
-
-      const buildData = (contract, message) =>
-        getDomain(contract).then(domain => ({
-          primaryType: 'Delegation',
-          types: { EIP712Domain: domainType(domain), Delegation },
-          domain,
-          message,
-        }));
-
+  for (const [mode, artifact] of Object.entries(MODES)) {
+    describe(`vote with ${mode}`, function () {
       beforeEach(async function () {
-        await this.token.$_mint(delegatorAddress, supply);
+        this.token = await artifact.new(name, symbol, name);
       });
 
-      it('accept signed delegation', async function () {
-        const { v, r, s } = await buildData(this.token, {
-          delegatee: delegatorAddress,
-          nonce,
-          expiry: MAX_UINT256,
-        })
-          .then(data => ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))
-          .then(fromRpcSig);
-
-        expect(await this.token.delegates(delegatorAddress)).to.be.equal(ZERO_ADDRESS);
-
-        const { receipt } = await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s);
-        expectEvent(receipt, 'DelegateChanged', {
-          delegator: delegatorAddress,
-          fromDelegate: ZERO_ADDRESS,
-          toDelegate: delegatorAddress,
-        });
-        expectEvent(receipt, 'DelegateVotesChanged', {
-          delegate: delegatorAddress,
-          previousBalance: '0',
-          newBalance: supply,
-        });
-
-        expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress);
+      shouldBehaveLikeEIP6372(mode);
 
-        expect(await this.token.getCurrentVotes(delegatorAddress)).to.be.bignumber.equal(supply);
-        expect(await this.token.getPriorVotes(delegatorAddress, receipt.blockNumber - 1)).to.be.bignumber.equal('0');
-        await time.advanceBlock();
-        expect(await this.token.getPriorVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal(supply);
+      it('initial nonce is 0', async function () {
+        expect(await this.token.nonces(holder)).to.be.bignumber.equal('0');
       });
 
-      it('rejects reused signature', async function () {
-        const { v, r, s } = await buildData(this.token, {
-          delegatee: delegatorAddress,
-          nonce,
-          expiry: MAX_UINT256,
-        })
-          .then(data => ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))
-          .then(fromRpcSig);
-
-        await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s);
-
-        await expectRevert(
-          this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s),
-          'ERC20Votes: invalid nonce',
-        );
+      it('domain separator', async function () {
+        expect(await this.token.DOMAIN_SEPARATOR()).to.equal(await getDomain(this.token).then(domainSeparator));
       });
 
-      it('rejects bad delegatee', async function () {
-        const { v, r, s } = await buildData(this.token, {
-          delegatee: delegatorAddress,
-          nonce,
-          expiry: MAX_UINT256,
-        })
-          .then(data => ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))
-          .then(fromRpcSig);
-
-        const receipt = await this.token.delegateBySig(holderDelegatee, nonce, MAX_UINT256, v, r, s);
-        const { args } = receipt.logs.find(({ event }) => event == 'DelegateChanged');
-        expect(args.delegator).to.not.be.equal(delegatorAddress);
-        expect(args.fromDelegate).to.be.equal(ZERO_ADDRESS);
-        expect(args.toDelegate).to.be.equal(holderDelegatee);
+      it('minting restriction', async function () {
+        const amount = new BN('2').pow(new BN('96'));
+        await expectRevert(this.token.$_mint(holder, amount), 'ERC20Votes: total supply risks overflowing votes');
       });
 
-      it('rejects bad nonce', async function () {
-        const { v, r, s } = await buildData(this.token, {
-          delegatee: delegatorAddress,
-          nonce,
-          expiry: MAX_UINT256,
-        })
-          .then(data => ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))
-          .then(fromRpcSig);
-
-        await expectRevert(
-          this.token.delegateBySig(delegatorAddress, nonce + 1, MAX_UINT256, v, r, s),
-          'ERC20Votes: invalid nonce',
-        );
-      });
+      describe('set delegation', function () {
+        describe('call', function () {
+          it('delegation with balance', async function () {
+            await this.token.$_mint(holder, supply);
+            expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS);
+
+            const { receipt } = await this.token.delegate(holder, { from: holder });
+            const timepoint = await clockFromReceipt[mode](receipt);
+
+            expectEvent(receipt, 'DelegateChanged', {
+              delegator: holder,
+              fromDelegate: ZERO_ADDRESS,
+              toDelegate: holder,
+            });
+            expectEvent(receipt, 'DelegateVotesChanged', {
+              delegate: holder,
+              previousBalance: '0',
+              newBalance: supply,
+            });
+
+            expect(await this.token.delegates(holder)).to.be.equal(holder);
+
+            expect(await this.token.getCurrentVotes(holder)).to.be.bignumber.equal(supply);
+            expect(await this.token.getPriorVotes(holder, timepoint - 1)).to.be.bignumber.equal('0');
+            await time.advanceBlock();
+            expect(await this.token.getPriorVotes(holder, timepoint)).to.be.bignumber.equal(supply);
+          });
+
+          it('delegation without balance', async function () {
+            expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS);
+
+            const { receipt } = await this.token.delegate(holder, { from: holder });
+            expectEvent(receipt, 'DelegateChanged', {
+              delegator: holder,
+              fromDelegate: ZERO_ADDRESS,
+              toDelegate: holder,
+            });
+            expectEvent.notEmitted(receipt, 'DelegateVotesChanged');
+
+            expect(await this.token.delegates(holder)).to.be.equal(holder);
+          });
+        });
 
-      it('rejects expired permit', async function () {
-        const expiry = (await time.latest()) - time.duration.weeks(1);
-
-        const { v, r, s } = await buildData(this.token, {
-          delegatee: delegatorAddress,
-          nonce,
-          expiry,
-        })
-          .then(data => ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))
-          .then(fromRpcSig);
-
-        await expectRevert(
-          this.token.delegateBySig(delegatorAddress, nonce, expiry, v, r, s),
-          'ERC20Votes: signature expired',
-        );
+        describe('with signature', function () {
+          const delegator = Wallet.generate();
+          const delegatorAddress = web3.utils.toChecksumAddress(delegator.getAddressString());
+          const nonce = 0;
+
+          const buildData = (contract, message) =>
+            getDomain(contract).then(domain => ({
+              primaryType: 'Delegation',
+              types: { EIP712Domain: domainType(domain), Delegation },
+              domain,
+              message,
+            }));
+
+          beforeEach(async function () {
+            await this.token.$_mint(delegatorAddress, supply);
+          });
+
+          it('accept signed delegation', async function () {
+            const { v, r, s } = await buildData(this.token, {
+              delegatee: delegatorAddress,
+              nonce,
+              expiry: MAX_UINT256,
+            }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })));
+
+            expect(await this.token.delegates(delegatorAddress)).to.be.equal(ZERO_ADDRESS);
+
+            const { receipt } = await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s);
+            const timepoint = await clockFromReceipt[mode](receipt);
+
+            expectEvent(receipt, 'DelegateChanged', {
+              delegator: delegatorAddress,
+              fromDelegate: ZERO_ADDRESS,
+              toDelegate: delegatorAddress,
+            });
+            expectEvent(receipt, 'DelegateVotesChanged', {
+              delegate: delegatorAddress,
+              previousBalance: '0',
+              newBalance: supply,
+            });
+
+            expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress);
+
+            expect(await this.token.getCurrentVotes(delegatorAddress)).to.be.bignumber.equal(supply);
+            expect(await this.token.getPriorVotes(delegatorAddress, timepoint - 1)).to.be.bignumber.equal('0');
+            await time.advanceBlock();
+            expect(await this.token.getPriorVotes(delegatorAddress, timepoint)).to.be.bignumber.equal(supply);
+          });
+
+          it('rejects reused signature', async function () {
+            const { v, r, s } = await buildData(this.token, {
+              delegatee: delegatorAddress,
+              nonce,
+              expiry: MAX_UINT256,
+            }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })));
+
+            await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s);
+
+            await expectRevert(
+              this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s),
+              'ERC20Votes: invalid nonce',
+            );
+          });
+
+          it('rejects bad delegatee', async function () {
+            const { v, r, s } = await buildData(this.token, {
+              delegatee: delegatorAddress,
+              nonce,
+              expiry: MAX_UINT256,
+            }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })));
+
+            const receipt = await this.token.delegateBySig(holderDelegatee, nonce, MAX_UINT256, v, r, s);
+            const { args } = receipt.logs.find(({ event }) => event == 'DelegateChanged');
+            expect(args.delegator).to.not.be.equal(delegatorAddress);
+            expect(args.fromDelegate).to.be.equal(ZERO_ADDRESS);
+            expect(args.toDelegate).to.be.equal(holderDelegatee);
+          });
+
+          it('rejects bad nonce', async function () {
+            const { v, r, s } = await buildData(this.token, {
+              delegatee: delegatorAddress,
+              nonce,
+              expiry: MAX_UINT256,
+            }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })));
+
+            await expectRevert(
+              this.token.delegateBySig(delegatorAddress, nonce + 1, MAX_UINT256, v, r, s),
+              'ERC20Votes: invalid nonce',
+            );
+          });
+
+          it('rejects expired permit', async function () {
+            const expiry = (await time.latest()) - time.duration.weeks(1);
+            const { v, r, s } = await buildData(this.token, {
+              delegatee: delegatorAddress,
+              nonce,
+              expiry,
+            }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })));
+
+            await expectRevert(
+              this.token.delegateBySig(delegatorAddress, nonce, expiry, v, r, s),
+              'ERC20Votes: signature expired',
+            );
+          });
+        });
       });
-    });
-  });
-
-  describe('change delegation', function () {
-    beforeEach(async function () {
-      await this.token.$_mint(holder, supply);
-      await this.token.delegate(holder, { from: holder });
-    });
 
-    it('call', async function () {
-      expect(await this.token.delegates(holder)).to.be.equal(holder);
+      describe('change delegation', function () {
+        beforeEach(async function () {
+          await this.token.$_mint(holder, supply);
+          await this.token.delegate(holder, { from: holder });
+        });
 
-      const { receipt } = await this.token.delegate(holderDelegatee, { from: holder });
-      expectEvent(receipt, 'DelegateChanged', {
-        delegator: holder,
-        fromDelegate: holder,
-        toDelegate: holderDelegatee,
-      });
-      expectEvent(receipt, 'DelegateVotesChanged', {
-        delegate: holder,
-        previousBalance: supply,
-        newBalance: '0',
-      });
-      expectEvent(receipt, 'DelegateVotesChanged', {
-        delegate: holderDelegatee,
-        previousBalance: '0',
-        newBalance: supply,
+        it('call', async function () {
+          expect(await this.token.delegates(holder)).to.be.equal(holder);
+
+          const { receipt } = await this.token.delegate(holderDelegatee, { from: holder });
+          const timepoint = await clockFromReceipt[mode](receipt);
+
+          expectEvent(receipt, 'DelegateChanged', {
+            delegator: holder,
+            fromDelegate: holder,
+            toDelegate: holderDelegatee,
+          });
+          expectEvent(receipt, 'DelegateVotesChanged', {
+            delegate: holder,
+            previousBalance: supply,
+            newBalance: '0',
+          });
+          expectEvent(receipt, 'DelegateVotesChanged', {
+            delegate: holderDelegatee,
+            previousBalance: '0',
+            newBalance: supply,
+          });
+
+          expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee);
+
+          expect(await this.token.getCurrentVotes(holder)).to.be.bignumber.equal('0');
+          expect(await this.token.getCurrentVotes(holderDelegatee)).to.be.bignumber.equal(supply);
+          expect(await this.token.getPriorVotes(holder, timepoint - 1)).to.be.bignumber.equal(supply);
+          expect(await this.token.getPriorVotes(holderDelegatee, timepoint - 1)).to.be.bignumber.equal('0');
+          await time.advanceBlock();
+          expect(await this.token.getPriorVotes(holder, timepoint)).to.be.bignumber.equal('0');
+          expect(await this.token.getPriorVotes(holderDelegatee, timepoint)).to.be.bignumber.equal(supply);
+        });
       });
 
-      expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee);
-
-      expect(await this.token.getCurrentVotes(holder)).to.be.bignumber.equal('0');
-      expect(await this.token.getCurrentVotes(holderDelegatee)).to.be.bignumber.equal(supply);
-      expect(await this.token.getPriorVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal(supply);
-      expect(await this.token.getPriorVotes(holderDelegatee, receipt.blockNumber - 1)).to.be.bignumber.equal('0');
-      await time.advanceBlock();
-      expect(await this.token.getPriorVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('0');
-      expect(await this.token.getPriorVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal(supply);
-    });
-  });
-
-  describe('transfers', function () {
-    beforeEach(async function () {
-      await this.token.$_mint(holder, supply);
-    });
-
-    it('no delegation', async function () {
-      const { receipt } = await this.token.transfer(recipient, 1, { from: holder });
-      expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' });
-      expectEvent.notEmitted(receipt, 'DelegateVotesChanged');
-
-      this.holderVotes = '0';
-      this.recipientVotes = '0';
-    });
-
-    it('sender delegation', async function () {
-      await this.token.delegate(holder, { from: holder });
-
-      const { receipt } = await this.token.transfer(recipient, 1, { from: holder });
-      expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' });
-      expectEvent(receipt, 'DelegateVotesChanged', {
-        delegate: holder,
-        previousBalance: supply,
-        newBalance: supply.subn(1),
-      });
+      describe('transfers', function () {
+        beforeEach(async function () {
+          await this.token.$_mint(holder, supply);
+        });
 
-      this.holderVotes = supply.subn(1);
-      this.recipientVotes = '0';
-    });
+        it('no delegation', async function () {
+          const { receipt } = await this.token.transfer(recipient, 1, { from: holder });
+          expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' });
+          expectEvent.notEmitted(receipt, 'DelegateVotesChanged');
 
-    it('receiver delegation', async function () {
-      await this.token.delegate(recipient, { from: recipient });
+          this.holderVotes = '0';
+          this.recipientVotes = '0';
+        });
 
-      const { receipt } = await this.token.transfer(recipient, 1, { from: holder });
-      expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' });
-      expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' });
+        it('sender delegation', async function () {
+          await this.token.delegate(holder, { from: holder });
 
-      this.holderVotes = '0';
-      this.recipientVotes = '1';
-    });
+          const { receipt } = await this.token.transfer(recipient, 1, { from: holder });
+          expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' });
+          expectEvent(receipt, 'DelegateVotesChanged', {
+            delegate: holder,
+            previousBalance: supply,
+            newBalance: supply.subn(1),
+          });
 
-    it('full delegation', async function () {
-      await this.token.delegate(holder, { from: holder });
-      await this.token.delegate(recipient, { from: recipient });
+          this.holderVotes = supply.subn(1);
+          this.recipientVotes = '0';
+        });
 
-      const { receipt } = await this.token.transfer(recipient, 1, { from: holder });
-      expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' });
-      expectEvent(receipt, 'DelegateVotesChanged', {
-        delegate: holder,
-        previousBalance: supply,
-        newBalance: supply.subn(1),
-      });
-      expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' });
+        it('receiver delegation', async function () {
+          await this.token.delegate(recipient, { from: recipient });
 
-      this.holderVotes = supply.subn(1);
-      this.recipientVotes = '1';
-    });
+          const { receipt } = await this.token.transfer(recipient, 1, { from: holder });
+          expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' });
+          expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' });
 
-    afterEach(async function () {
-      expect(await this.token.getCurrentVotes(holder)).to.be.bignumber.equal(this.holderVotes);
-      expect(await this.token.getCurrentVotes(recipient)).to.be.bignumber.equal(this.recipientVotes);
+          this.holderVotes = '0';
+          this.recipientVotes = '1';
+        });
 
-      // need to advance 2 blocks to see the effect of a transfer on "getPriorVotes"
-      const blockNumber = await time.latestBlock();
-      await time.advanceBlock();
-      expect(await this.token.getPriorVotes(holder, blockNumber)).to.be.bignumber.equal(this.holderVotes);
-      expect(await this.token.getPriorVotes(recipient, blockNumber)).to.be.bignumber.equal(this.recipientVotes);
-    });
-  });
+        it('full delegation', async function () {
+          await this.token.delegate(holder, { from: holder });
+          await this.token.delegate(recipient, { from: recipient });
+
+          const { receipt } = await this.token.transfer(recipient, 1, { from: holder });
+          expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' });
+          expectEvent(receipt, 'DelegateVotesChanged', {
+            delegate: holder,
+            previousBalance: supply,
+            newBalance: supply.subn(1),
+          });
+          expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' });
+
+          this.holderVotes = supply.subn(1);
+          this.recipientVotes = '1';
+        });
 
-  // The following tests are a adaptation of https://github.com/compound-finance/compound-protocol/blob/master/tests/Governance/CompTest.js.
-  describe('Compound test suite', function () {
-    beforeEach(async function () {
-      await this.token.$_mint(holder, supply);
-    });
+        afterEach(async function () {
+          expect(await this.token.getCurrentVotes(holder)).to.be.bignumber.equal(this.holderVotes);
+          expect(await this.token.getCurrentVotes(recipient)).to.be.bignumber.equal(this.recipientVotes);
 
-    describe('balanceOf', function () {
-      it('grants to initial account', async function () {
-        expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('10000000000000000000000000');
+          // need to advance 2 blocks to see the effect of a transfer on "getPriorVotes"
+          const timepoint = await clock[mode]();
+          await time.advanceBlock();
+          expect(await this.token.getPriorVotes(holder, timepoint)).to.be.bignumber.equal(this.holderVotes);
+          expect(await this.token.getPriorVotes(recipient, timepoint)).to.be.bignumber.equal(this.recipientVotes);
+        });
       });
-    });
-
-    describe('numCheckpoints', function () {
-      it('returns the number of checkpoints for a delegate', async function () {
-        await this.token.transfer(recipient, '100', { from: holder }); //give an account a few tokens for readability
-        expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0');
-
-        const t1 = await this.token.delegate(other1, { from: recipient });
-        expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1');
 
-        const t2 = await this.token.transfer(other2, 10, { from: recipient });
-        expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2');
-
-        const t3 = await this.token.transfer(other2, 10, { from: recipient });
-        expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3');
+      // The following tests are a adaptation of https://github.com/compound-finance/compound-protocol/blob/master/tests/Governance/CompTest.js.
+      describe('Compound test suite', function () {
+        beforeEach(async function () {
+          await this.token.$_mint(holder, supply);
+        });
 
-        const t4 = await this.token.transfer(recipient, 20, { from: holder });
-        expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4');
+        describe('balanceOf', function () {
+          it('grants to initial account', async function () {
+            expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('10000000000000000000000000');
+          });
+        });
 
-        expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([t1.receipt.blockNumber.toString(), '100']);
-        expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([t2.receipt.blockNumber.toString(), '90']);
-        expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([t3.receipt.blockNumber.toString(), '80']);
-        expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([t4.receipt.blockNumber.toString(), '100']);
+        describe('numCheckpoints', function () {
+          it('returns the number of checkpoints for a delegate', async function () {
+            await this.token.transfer(recipient, '100', { from: holder }); //give an account a few tokens for readability
+            expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0');
+
+            const t1 = await this.token.delegate(other1, { from: recipient });
+            t1.timepoint = await clockFromReceipt[mode](t1.receipt);
+            expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1');
+
+            const t2 = await this.token.transfer(other2, 10, { from: recipient });
+            t2.timepoint = await clockFromReceipt[mode](t2.receipt);
+            expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2');
+
+            const t3 = await this.token.transfer(other2, 10, { from: recipient });
+            t3.timepoint = await clockFromReceipt[mode](t3.receipt);
+            expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3');
+
+            const t4 = await this.token.transfer(recipient, 20, { from: holder });
+            t4.timepoint = await clockFromReceipt[mode](t4.receipt);
+            expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4');
+
+            expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([t1.timepoint.toString(), '100']);
+            expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([t2.timepoint.toString(), '90']);
+            expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([t3.timepoint.toString(), '80']);
+            expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([t4.timepoint.toString(), '100']);
+
+            await time.advanceBlock();
+            expect(await this.token.getPriorVotes(other1, t1.timepoint)).to.be.bignumber.equal('100');
+            expect(await this.token.getPriorVotes(other1, t2.timepoint)).to.be.bignumber.equal('90');
+            expect(await this.token.getPriorVotes(other1, t3.timepoint)).to.be.bignumber.equal('80');
+            expect(await this.token.getPriorVotes(other1, t4.timepoint)).to.be.bignumber.equal('100');
+          });
+
+          it('does not add more than one checkpoint in a block', async function () {
+            await this.token.transfer(recipient, '100', { from: holder });
+            expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0');
+
+            const [t1, t2, t3] = await batchInBlock([
+              () => this.token.delegate(other1, { from: recipient, gas: 100000 }),
+              () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }),
+              () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }),
+            ]);
+            t1.timepoint = await clockFromReceipt[mode](t1.receipt);
+            t2.timepoint = await clockFromReceipt[mode](t2.receipt);
+            t3.timepoint = await clockFromReceipt[mode](t3.receipt);
+
+            expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1');
+            expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([t1.timepoint.toString(), '80']);
+
+            const t4 = await this.token.transfer(recipient, 20, { from: holder });
+            t4.timepoint = await clockFromReceipt[mode](t4.receipt);
+
+            expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2');
+            expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([t4.timepoint.toString(), '100']);
+          });
+        });
 
-        await time.advanceBlock();
-        expect(await this.token.getPriorVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('100');
-        expect(await this.token.getPriorVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('90');
-        expect(await this.token.getPriorVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('80');
-        expect(await this.token.getPriorVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('100');
+        describe('getPriorVotes', function () {
+          it('reverts if block number >= current block', async function () {
+            await expectRevert(this.token.getPriorVotes(other1, 5e10), 'ERC20Votes: future lookup');
+          });
+
+          it('returns 0 if there are no checkpoints', async function () {
+            expect(await this.token.getPriorVotes(other1, 0)).to.be.bignumber.equal('0');
+          });
+
+          it('returns the latest block if >= last checkpoint block', async function () {
+            const { receipt } = await this.token.delegate(other1, { from: holder });
+            const timepoint = await clockFromReceipt[mode](receipt);
+            await time.advanceBlock();
+            await time.advanceBlock();
+
+            expect(await this.token.getPriorVotes(other1, timepoint)).to.be.bignumber.equal(
+              '10000000000000000000000000',
+            );
+            expect(await this.token.getPriorVotes(other1, timepoint + 1)).to.be.bignumber.equal(
+              '10000000000000000000000000',
+            );
+          });
+
+          it('returns zero if < first checkpoint block', async function () {
+            await time.advanceBlock();
+            const { receipt } = await this.token.delegate(other1, { from: holder });
+            const timepoint = await clockFromReceipt[mode](receipt);
+            await time.advanceBlock();
+            await time.advanceBlock();
+
+            expect(await this.token.getPriorVotes(other1, timepoint - 1)).to.be.bignumber.equal('0');
+            expect(await this.token.getPriorVotes(other1, timepoint + 1)).to.be.bignumber.equal(
+              '10000000000000000000000000',
+            );
+          });
+
+          it('generally returns the voting balance at the appropriate checkpoint', async function () {
+            const t1 = await this.token.delegate(other1, { from: holder });
+            await time.advanceBlock();
+            await time.advanceBlock();
+            const t2 = await this.token.transfer(other2, 10, { from: holder });
+            await time.advanceBlock();
+            await time.advanceBlock();
+            const t3 = await this.token.transfer(other2, 10, { from: holder });
+            await time.advanceBlock();
+            await time.advanceBlock();
+            const t4 = await this.token.transfer(holder, 20, { from: other2 });
+            await time.advanceBlock();
+            await time.advanceBlock();
+
+            t1.timepoint = await clockFromReceipt[mode](t1.receipt);
+            t2.timepoint = await clockFromReceipt[mode](t2.receipt);
+            t3.timepoint = await clockFromReceipt[mode](t3.receipt);
+            t4.timepoint = await clockFromReceipt[mode](t4.receipt);
+
+            expect(await this.token.getPriorVotes(other1, t1.timepoint - 1)).to.be.bignumber.equal('0');
+            expect(await this.token.getPriorVotes(other1, t1.timepoint)).to.be.bignumber.equal(
+              '10000000000000000000000000',
+            );
+            expect(await this.token.getPriorVotes(other1, t1.timepoint + 1)).to.be.bignumber.equal(
+              '10000000000000000000000000',
+            );
+            expect(await this.token.getPriorVotes(other1, t2.timepoint)).to.be.bignumber.equal(
+              '9999999999999999999999990',
+            );
+            expect(await this.token.getPriorVotes(other1, t2.timepoint + 1)).to.be.bignumber.equal(
+              '9999999999999999999999990',
+            );
+            expect(await this.token.getPriorVotes(other1, t3.timepoint)).to.be.bignumber.equal(
+              '9999999999999999999999980',
+            );
+            expect(await this.token.getPriorVotes(other1, t3.timepoint + 1)).to.be.bignumber.equal(
+              '9999999999999999999999980',
+            );
+            expect(await this.token.getPriorVotes(other1, t4.timepoint)).to.be.bignumber.equal(
+              '10000000000000000000000000',
+            );
+            expect(await this.token.getPriorVotes(other1, t4.timepoint + 1)).to.be.bignumber.equal(
+              '10000000000000000000000000',
+            );
+          });
+        });
       });
 
-      it('does not add more than one checkpoint in a block', async function () {
-        await this.token.transfer(recipient, '100', { from: holder });
-        expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0');
-
-        const [t1, t2, t3] = await batchInBlock([
-          () => this.token.delegate(other1, { from: recipient, gas: 100000 }),
-          () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }),
-          () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }),
-        ]);
-        expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1');
-        expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([t1.receipt.blockNumber.toString(), '80']);
-        // expectReve(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check
-        // expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check
-
-        const t4 = await this.token.transfer(recipient, 20, { from: holder });
-        expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2');
-        expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([t4.receipt.blockNumber.toString(), '100']);
-      });
-    });
+      describe('getPastTotalSupply', function () {
+        beforeEach(async function () {
+          await this.token.delegate(holder, { from: holder });
+        });
 
-    describe('getPriorVotes', function () {
-      it('reverts if block number >= current block', async function () {
-        await expectRevert(this.token.getPriorVotes(other1, 5e10), 'ERC20Votes: block not yet mined');
-      });
+        it('reverts if block number >= current block', async function () {
+          await expectRevert(this.token.getPastTotalSupply(5e10), 'ERC20Votes: future lookup');
+        });
 
-      it('returns 0 if there are no checkpoints', async function () {
-        expect(await this.token.getPriorVotes(other1, 0)).to.be.bignumber.equal('0');
-      });
+        it('returns 0 if there are no checkpoints', async function () {
+          expect(await this.token.getPastTotalSupply(0)).to.be.bignumber.equal('0');
+        });
 
-      it('returns the latest block if >= last checkpoint block', async function () {
-        const t1 = await this.token.delegate(other1, { from: holder });
-        await time.advanceBlock();
-        await time.advanceBlock();
-
-        expect(await this.token.getPriorVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal(
-          '10000000000000000000000000',
-        );
-        expect(await this.token.getPriorVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal(
-          '10000000000000000000000000',
-        );
-      });
+        it('returns the latest block if >= last checkpoint block', async function () {
+          const { receipt } = await this.token.$_mint(holder, supply);
+          const timepoint = await clockFromReceipt[mode](receipt);
+          await time.advanceBlock();
+          await time.advanceBlock();
 
-      it('returns zero if < first checkpoint block', async function () {
-        await time.advanceBlock();
-        const t1 = await this.token.delegate(other1, { from: holder });
-        await time.advanceBlock();
-        await time.advanceBlock();
+          expect(await this.token.getPastTotalSupply(timepoint)).to.be.bignumber.equal(supply);
+          expect(await this.token.getPastTotalSupply(timepoint + 1)).to.be.bignumber.equal(supply);
+        });
 
-        expect(await this.token.getPriorVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0');
-        expect(await this.token.getPriorVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal(
-          '10000000000000000000000000',
-        );
-      });
+        it('returns zero if < first checkpoint block', async function () {
+          await time.advanceBlock();
+          const { receipt } = await this.token.$_mint(holder, supply);
+          const timepoint = await clockFromReceipt[mode](receipt);
+          await time.advanceBlock();
+          await time.advanceBlock();
+
+          expect(await this.token.getPastTotalSupply(timepoint - 1)).to.be.bignumber.equal('0');
+          expect(await this.token.getPastTotalSupply(timepoint + 1)).to.be.bignumber.equal(
+            '10000000000000000000000000',
+          );
+        });
 
-      it('generally returns the voting balance at the appropriate checkpoint', async function () {
-        const t1 = await this.token.delegate(other1, { from: holder });
-        await time.advanceBlock();
-        await time.advanceBlock();
-        const t2 = await this.token.transfer(other2, 10, { from: holder });
-        await time.advanceBlock();
-        await time.advanceBlock();
-        const t3 = await this.token.transfer(other2, 10, { from: holder });
-        await time.advanceBlock();
-        await time.advanceBlock();
-        const t4 = await this.token.transfer(holder, 20, { from: other2 });
-        await time.advanceBlock();
-        await time.advanceBlock();
-
-        expect(await this.token.getPriorVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0');
-        expect(await this.token.getPriorVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal(
-          '10000000000000000000000000',
-        );
-        expect(await this.token.getPriorVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal(
-          '10000000000000000000000000',
-        );
-        expect(await this.token.getPriorVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal(
-          '9999999999999999999999990',
-        );
-        expect(await this.token.getPriorVotes(other1, t2.receipt.blockNumber + 1)).to.be.bignumber.equal(
-          '9999999999999999999999990',
-        );
-        expect(await this.token.getPriorVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal(
-          '9999999999999999999999980',
-        );
-        expect(await this.token.getPriorVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal(
-          '9999999999999999999999980',
-        );
-        expect(await this.token.getPriorVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal(
-          '10000000000000000000000000',
-        );
-        expect(await this.token.getPriorVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal(
-          '10000000000000000000000000',
-        );
+        it('generally returns the voting balance at the appropriate checkpoint', async function () {
+          const t1 = await this.token.$_mint(holder, supply);
+          await time.advanceBlock();
+          await time.advanceBlock();
+          const t2 = await this.token.$_burn(holder, 10);
+          await time.advanceBlock();
+          await time.advanceBlock();
+          const t3 = await this.token.$_burn(holder, 10);
+          await time.advanceBlock();
+          await time.advanceBlock();
+          const t4 = await this.token.$_mint(holder, 20);
+          await time.advanceBlock();
+          await time.advanceBlock();
+
+          t1.timepoint = await clockFromReceipt[mode](t1.receipt);
+          t2.timepoint = await clockFromReceipt[mode](t2.receipt);
+          t3.timepoint = await clockFromReceipt[mode](t3.receipt);
+          t4.timepoint = await clockFromReceipt[mode](t4.receipt);
+
+          expect(await this.token.getPastTotalSupply(t1.timepoint - 1)).to.be.bignumber.equal('0');
+          expect(await this.token.getPastTotalSupply(t1.timepoint)).to.be.bignumber.equal('10000000000000000000000000');
+          expect(await this.token.getPastTotalSupply(t1.timepoint + 1)).to.be.bignumber.equal(
+            '10000000000000000000000000',
+          );
+          expect(await this.token.getPastTotalSupply(t2.timepoint)).to.be.bignumber.equal('9999999999999999999999990');
+          expect(await this.token.getPastTotalSupply(t2.timepoint + 1)).to.be.bignumber.equal(
+            '9999999999999999999999990',
+          );
+          expect(await this.token.getPastTotalSupply(t3.timepoint)).to.be.bignumber.equal('9999999999999999999999980');
+          expect(await this.token.getPastTotalSupply(t3.timepoint + 1)).to.be.bignumber.equal(
+            '9999999999999999999999980',
+          );
+          expect(await this.token.getPastTotalSupply(t4.timepoint)).to.be.bignumber.equal('10000000000000000000000000');
+          expect(await this.token.getPastTotalSupply(t4.timepoint + 1)).to.be.bignumber.equal(
+            '10000000000000000000000000',
+          );
+        });
       });
     });
-  });
-
-  describe('getPastTotalSupply', function () {
-    beforeEach(async function () {
-      await this.token.delegate(holder, { from: holder });
-    });
-
-    it('reverts if block number >= current block', async function () {
-      await expectRevert(this.token.getPastTotalSupply(5e10), 'ERC20Votes: block not yet mined');
-    });
-
-    it('returns 0 if there are no checkpoints', async function () {
-      expect(await this.token.getPastTotalSupply(0)).to.be.bignumber.equal('0');
-    });
-
-    it('returns the latest block if >= last checkpoint block', async function () {
-      t1 = await this.token.$_mint(holder, supply);
-
-      await time.advanceBlock();
-      await time.advanceBlock();
-
-      expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal(supply);
-      expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal(supply);
-    });
-
-    it('returns zero if < first checkpoint block', async function () {
-      await time.advanceBlock();
-      const t1 = await this.token.$_mint(holder, supply);
-      await time.advanceBlock();
-      await time.advanceBlock();
-
-      expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0');
-      expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal(
-        '10000000000000000000000000',
-      );
-    });
-
-    it('generally returns the voting balance at the appropriate checkpoint', async function () {
-      const t1 = await this.token.$_mint(holder, supply);
-      await time.advanceBlock();
-      await time.advanceBlock();
-      const t2 = await this.token.$_burn(holder, 10);
-      await time.advanceBlock();
-      await time.advanceBlock();
-      const t3 = await this.token.$_burn(holder, 10);
-      await time.advanceBlock();
-      await time.advanceBlock();
-      const t4 = await this.token.$_mint(holder, 20);
-      await time.advanceBlock();
-      await time.advanceBlock();
-
-      expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0');
-      expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal(
-        '10000000000000000000000000',
-      );
-      expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal(
-        '10000000000000000000000000',
-      );
-      expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal(
-        '9999999999999999999999990',
-      );
-      expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal(
-        '9999999999999999999999990',
-      );
-      expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal(
-        '9999999999999999999999980',
-      );
-      expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal(
-        '9999999999999999999999980',
-      );
-      expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal(
-        '10000000000000000000000000',
-      );
-      expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal(
-        '10000000000000000000000000',
-      );
-    });
-  });
+  }
 });

+ 1 - 0
test/token/ERC721/extensions/ERC721Votes.test.js

@@ -178,6 +178,7 @@ contract('ERC721Votes', function (accounts) {
       this.name = 'My Vote';
     });
 
+    // includes EIP6372 behavior check
     shouldBehaveLikeVotes();
   });
 });

+ 27 - 2
test/utils/Checkpoints.test.js

@@ -117,8 +117,10 @@ contract('Checkpoints', function () {
       const latestCheckpoint = (self, ...args) =>
         self.methods[`$latestCheckpoint_Checkpoints_Trace${length}(uint256)`](0, ...args);
       const push = (self, ...args) => self.methods[`$push(uint256,uint${256 - length},uint${length})`](0, ...args);
-      const upperLookup = (self, ...args) => self.methods[`$upperLookup(uint256,uint${256 - length})`](0, ...args);
       const lowerLookup = (self, ...args) => self.methods[`$lowerLookup(uint256,uint${256 - length})`](0, ...args);
+      const upperLookup = (self, ...args) => self.methods[`$upperLookup(uint256,uint${256 - length})`](0, ...args);
+      const upperLookupRecent = (self, ...args) =>
+        self.methods[`$upperLookupRecent(uint256,uint${256 - length})`](0, ...args);
       const getLength = (self, ...args) => self.methods[`$length_Checkpoints_Trace${length}(uint256)`](0, ...args);
 
       describe('without checkpoints', function () {
@@ -134,6 +136,7 @@ contract('Checkpoints', function () {
         it('lookup returns 0', async function () {
           expect(await lowerLookup(this.mock, 0)).to.be.bignumber.equal('0');
           expect(await upperLookup(this.mock, 0)).to.be.bignumber.equal('0');
+          expect(await upperLookupRecent(this.mock, 0)).to.be.bignumber.equal('0');
         });
       });
 
@@ -190,11 +193,33 @@ contract('Checkpoints', function () {
           }
         });
 
-        it('upper lookup', async function () {
+        it('upper lookup & upperLookupRecent', async function () {
           for (let i = 0; i < 14; ++i) {
             const value = last(this.checkpoints.filter(x => i >= x.key))?.value || '0';
 
             expect(await upperLookup(this.mock, i)).to.be.bignumber.equal(value);
+            expect(await upperLookupRecent(this.mock, i)).to.be.bignumber.equal(value);
+          }
+        });
+
+        it('upperLookupRecent with more than 5 checkpoints', async function () {
+          const moreCheckpoints = [
+            { key: '12', value: '22' },
+            { key: '13', value: '131' },
+            { key: '17', value: '45' },
+            { key: '19', value: '31452' },
+            { key: '21', value: '0' },
+          ];
+          const allCheckpoints = [].concat(this.checkpoints, moreCheckpoints);
+
+          for (const { key, value } of moreCheckpoints) {
+            await push(this.mock, key, value);
+          }
+
+          for (let i = 0; i < 25; ++i) {
+            const value = last(allCheckpoints.filter(x => i >= x.key))?.value || '0';
+            expect(await upperLookup(this.mock, i)).to.be.bignumber.equal(value);
+            expect(await upperLookupRecent(this.mock, i)).to.be.bignumber.equal(value);
           }
         });
       });