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>
pull/4040/head
Hadrien Croubois 2 years ago committed by GitHub
parent 94cd8ef12e
commit 790cc5b65a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      .changeset/four-bats-sniff.md
  2. 5
      .changeset/ninety-hornets-kick.md
  3. 2
      .github/actions/storage-layout/action.yml
  4. 85
      contracts/governance/Governor.sol
  5. 62
      contracts/governance/IGovernor.sol
  6. 6
      contracts/governance/README.adoc
  7. 6
      contracts/governance/compatibility/GovernorCompatibilityBravo.sol
  8. 19
      contracts/governance/extensions/GovernorPreventLateQuorum.sol
  9. 17
      contracts/governance/extensions/GovernorTimelockCompound.sol
  10. 34
      contracts/governance/extensions/GovernorVotes.sol
  11. 30
      contracts/governance/extensions/GovernorVotesComp.sol
  12. 29
      contracts/governance/extensions/GovernorVotesQuorumFraction.sol
  13. 10
      contracts/governance/utils/IVotes.sol
  14. 81
      contracts/governance/utils/Votes.sol
  15. 9
      contracts/interfaces/IERC5805.sol
  16. 17
      contracts/interfaces/IERC6372.sol
  17. 11
      contracts/mocks/VotesMock.sol
  18. 262
      contracts/mocks/token/ERC20VotesLegacyMock.sol
  19. 40
      contracts/mocks/token/VotesTimestamp.sol
  20. 63
      contracts/token/ERC20/extensions/ERC20Votes.sol
  21. 50
      contracts/utils/Checkpoints.sol
  22. 1
      scripts/checks/compare-layout.js
  23. 16
      scripts/checks/compareGasReports.js
  24. 66
      scripts/generate/templates/Checkpoints.js
  25. 1202
      test/governance/Governor.test.js
  26. 429
      test/governance/compatibility/GovernorCompatibilityBravo.test.js
  27. 115
      test/governance/extensions/GovernorComp.test.js
  28. 186
      test/governance/extensions/GovernorERC721.test.js
  29. 311
      test/governance/extensions/GovernorPreventLateQuorum.test.js
  30. 551
      test/governance/extensions/GovernorTimelockCompound.test.js
  31. 721
      test/governance/extensions/GovernorTimelockControl.test.js
  32. 250
      test/governance/extensions/GovernorVotesQuorumFraction.test.js
  33. 280
      test/governance/extensions/GovernorWithParams.test.js
  34. 23
      test/governance/utils/EIP6372.behavior.js
  35. 91
      test/governance/utils/Votes.behavior.js
  36. 95
      test/governance/utils/Votes.test.js
  37. 15
      test/helpers/governance.js
  38. 16
      test/helpers/time.js
  39. 998
      test/token/ERC20/extensions/ERC20Votes.test.js
  40. 931
      test/token/ERC20/extensions/ERC20VotesComp.test.js
  41. 1
      test/token/ERC721/extensions/ERC721Votes.test.js
  42. 29
      test/utils/Checkpoints.test.js

@ -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.

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

@ -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'

@ -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) {

@ -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.
*/

@ -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.

@ -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 ====================================================

@ -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;

@ -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;

@ -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);
}
}

@ -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);
}
}

@ -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);
}

@ -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.

@ -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;
}

@ -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 {}

@ -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);
}

@ -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";
}
}

@ -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)
}
}
}

@ -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";
}
}

@ -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)}));
}
}
}

@ -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.
*/

@ -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;
}

@ -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':

@ -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(),

File diff suppressed because it is too large Load Diff

@ -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');
});
});
});
});
}
});

@ -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'));
});
});
});
});
}
});

@ -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');
});
});
});
});
}
});

@ -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');
});
});
});
});
}
});

@ -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);
});
});
}
});

@ -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.
});
}
});

@ -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',
);
});
});
});
});
}
});

@ -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);
});
}
});

@ -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,
};

@ -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');
});
});
});

@ -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);
});
});
}
});

@ -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)));
}
/**

@ -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,
},
};

File diff suppressed because it is too large Load Diff

@ -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',
);
});
});
}
});

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

@ -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);
}
});
});

Loading…
Cancel
Save