Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a governor module to protect against late quorum #2973

Merged
merged 14 commits into from
Dec 1, 2021
Prev Previous commit
Next Next commit
changelog, documentation and event
Amxx committed Nov 16, 2021
commit 143cacc2483958124c4f538cff5bc7b4323ee141
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changelog

## Unreleased
* `GovernorExtendedVoting`: add new module to ensure a minimum voting duration is available after the quorum is reached. ([#2973](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2973))

## Unreleased

* `Ownable`: add an internal `_transferOwnership(address)`. ([#2568](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2568))
2 changes: 2 additions & 0 deletions contracts/governance/README.adoc
Original file line number Diff line number Diff line change
@@ -42,6 +42,8 @@ Other extensions can customize the behavior or interface in multiple ways.

* {GovernorSettings}: Manages some of the settings (voting delay, voting period duration, and proposal threshold) in a way that can be updated through a governance proposal, without requiering an upgrade.

* {GovernorExtendedVoting}: Extend the voting deadline if the quorum is reached late. This prevents a voter with significant voting power from voting at the last minute, reaching the quorum, and leaving no time for other voters to react. This module guarantees a minimum duration for other voters to contribute and try to oppose the decision.

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.
10 changes: 9 additions & 1 deletion contracts/governance/extensions/GovernorExtendedVoting.sol
Original file line number Diff line number Diff line change
@@ -18,6 +18,7 @@ abstract contract GovernorExtendedVoting is Governor {
uint64 private _votingDelayExtension;
Amxx marked this conversation as resolved.
Show resolved Hide resolved
mapping(uint256 => Timers.BlockNumber) private _extendedVoteEnd;

event ProposalExtended(uint256 indexed proposalId, uint64 extendedDeadline);
event VotingDelayExtentionSet(uint64 oldVotingDelayExtention, uint64 newVotingDelayExtention);

/**
@@ -47,8 +48,15 @@ abstract contract GovernorExtendedVoting is Governor {

if (_quorumReached(proposalId)) {
Timers.BlockNumber storage extension = _extendedVoteEnd[proposalId];

if (extension.isUnset()) {
extension.setDeadline(block.number.toUint64() + votingDelayExtention());
uint64 extendedDeadline = block.number.toUint64() + votingDelayExtention();

if (extendedDeadline > proposalDeadline(proposalId)) {
emit ProposalExtended(proposalId, extendedDeadline);
}

extension.setDeadline(extendedDeadline);
}
}

10 changes: 10 additions & 0 deletions test/governance/extensions/GovernorExtendedVoting.test.js
Original file line number Diff line number Diff line change
@@ -107,6 +107,10 @@ contract('GovernorExtendedVoting', function (accounts) {
'VoteCast',
this.settings.voters.find(({ address }) => address === voter),
);
expectEvent.notEmitted(
vote,
'ProposalExtended',
);
});
expectEvent(
this.receipts.execute,
@@ -164,6 +168,12 @@ contract('GovernorExtendedVoting', function (accounts) {
const extendedBlock = new BN(tx.receipt.blockNumber).add(votingDelayExtention);
expect(await this.mock.proposalDeadline(this.id)).to.be.bignumber.equal(extendedBlock);

expectEvent(
tx,
'ProposalExtended',
{ proposalId: this.id, extendedDeadline: extendedBlock },
);

// vote is still active after expected end
await time.advanceBlockTo(endBlock.addn(1));
expect(await this.mock.state(this.id)).to.be.bignumber.equal(Enums.ProposalState.Active);