Skip to content

Commit

Permalink
Add a governor module to protect against late quorum (OpenZeppelin#2973)
Browse files Browse the repository at this point in the history
Co-authored-by: Francisco Giordano <frangio.1@gmail.com>
  • Loading branch information
2 people authored and EchoicDeveloper committed Jan 9, 2022
1 parent ed0784a commit a07c0a2
Show file tree
Hide file tree
Showing 7 changed files with 444 additions and 11 deletions.
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

* `GovernorTimelockControl`: improve the `state()` function to have it reflect cases where a proposal has been canceled directly on the timelock. ([#2977](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2977))
Expand Down
33 changes: 23 additions & 10 deletions contracts/governance/Governor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -110,23 +110,36 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor {
* @dev See {IGovernor-state}.
*/
function state(uint256 proposalId) public view virtual override returns (ProposalState) {
ProposalCore memory proposal = _proposals[proposalId];
ProposalCore storage proposal = _proposals[proposalId];

if (proposal.executed) {
return ProposalState.Executed;
} else if (proposal.canceled) {
}

if (proposal.canceled) {
return ProposalState.Canceled;
} else if (proposal.voteStart.getDeadline() >= block.number) {
}

uint256 snapshot = proposalSnapshot(proposalId);

if (snapshot == 0) {
revert("Governor: unknown proposal id");
}

if (snapshot >= block.number) {
return ProposalState.Pending;
} else if (proposal.voteEnd.getDeadline() >= block.number) {
}

uint256 deadline = proposalDeadline(proposalId);

if (deadline >= block.number) {
return ProposalState.Active;
} else if (proposal.voteEnd.isExpired()) {
return
_quorumReached(proposalId) && _voteSucceeded(proposalId)
? ProposalState.Succeeded
: ProposalState.Defeated;
}

if (_quorumReached(proposalId) && _voteSucceeded(proposalId)) {
return ProposalState.Succeeded;
} else {
revert("Governor: unknown proposal id");
return ProposalState.Defeated;
}
}

Expand Down
4 changes: 4 additions & 0 deletions contracts/governance/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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.

* {GovernorPreventLateQuorum}: Ensures there is a minimum voting period after quorum is reached as a security protection against large voters.

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.
Expand Down Expand Up @@ -74,6 +76,8 @@ NOTE: Functions of the `Governor` contract do not include access control. If you

{{GovernorSettings}}

{{GovernorPreventLateQuorum}}

{{GovernorCompatibilityBravo}}

=== Deprecated
Expand Down
106 changes: 106 additions & 0 deletions contracts/governance/extensions/GovernorPreventLateQuorum.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "../Governor.sol";
import "../../utils/math/Math.sol";

/**
* @dev A module that ensures there is a minimum voting period after quorum is reached. This prevents a large voter from
* swaying a vote and triggering quorum at the last minute, by ensuring there is always time for other voters to react
* and try to oppose the decision.
*
* If a vote causes quorum to be reached, the proposal's voting period may be extended so that it does not end before at
* least a given number of blocks have passed (the "vote extension" parameter). This parameter can be set by the
* governance executor (e.g. through a governance proposal).
*
* _Available since v4.5._
*/
abstract contract GovernorPreventLateQuorum is Governor {
using SafeCast for uint256;
using Timers for Timers.BlockNumber;

uint64 private _voteExtension;
mapping(uint256 => Timers.BlockNumber) 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);

/// @dev Emitted when the {lateQuorumVoteExtension} parameter is changed.
event LateQuorumVoteExtensionSet(uint64 oldVoteExtension, uint64 newVoteExtension);

/**
* @dev Initializes the vote extension parameter: the number of blocks that are required to pass since a proposal
* reaches quorum until its voting period ends. If necessary the voting period will be extended beyond the one set
* at proposal creation.
*/
constructor(uint64 initialVoteExtension) {
_setLateQuorumVoteExtension(initialVoteExtension);
}

/**
* @dev Returns the proposal deadline, which may have been extended beyond that set at proposal creation, if the
* 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());
}

/**
* @dev Casts a vote and detects if it caused quorum to be reached, potentially extending the voting period. See
* {Governor-_castVote}.
*
* May emit a {ProposalExtended} event.
*/
function _castVote(
uint256 proposalId,
address account,
uint8 support,
string memory reason
) internal virtual override returns (uint256) {
uint256 result = super._castVote(proposalId, account, support, reason);

Timers.BlockNumber storage extendedDeadline = _extendedDeadlines[proposalId];

if (extendedDeadline.isUnset() && _quorumReached(proposalId)) {
uint64 extendedDeadlineValue = block.number.toUint64() + lateQuorumVoteExtension();

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

extendedDeadline.setDeadline(extendedDeadlineValue);
}

return result;
}

/**
* @dev Returns the current value of the vote extension parameter: the number of blocks that are required to pass
* from the time a proposal reaches quorum until its voting period ends.
*/
function lateQuorumVoteExtension() public view virtual returns (uint64) {
return _voteExtension;
}

/**
* @dev Changes the {lateQuorumVoteExtension}. This operation can only be performed by the governance executor,
* generally through a governance proposal.
*
* Emits a {LateQuorumVoteExtensionSet} event.
*/
function setLateQuorumVoteExtension(uint64 newVoteExtension) public virtual onlyGovernance {
_setLateQuorumVoteExtension(newVoteExtension);
}

/**
* @dev Changes the {lateQuorumVoteExtension}. This is an internal function that can be exposed in a public function
* like {setLateQuorumVoteExtension} if another access control mechanism is needed.
*
* Emits a {LateQuorumVoteExtensionSet} event.
*/
function _setLateQuorumVoteExtension(uint64 newVoteExtension) internal virtual {
emit LateQuorumVoteExtensionSet(_voteExtension, newVoteExtension);
_voteExtension = newVoteExtension;
}
}
60 changes: 60 additions & 0 deletions contracts/mocks/GovernorPreventLateQuorumMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "../governance/extensions/GovernorPreventLateQuorum.sol";
import "../governance/extensions/GovernorSettings.sol";
import "../governance/extensions/GovernorCountingSimple.sol";
import "../governance/extensions/GovernorVotes.sol";

contract GovernorPreventLateQuorumMock is
GovernorSettings,
GovernorVotes,
GovernorCountingSimple,
GovernorPreventLateQuorum
{
uint256 private _quorum;

constructor(
string memory name_,
ERC20Votes token_,
uint256 votingDelay_,
uint256 votingPeriod_,
uint256 quorum_,
uint64 voteExtension_
)
Governor(name_)
GovernorSettings(votingDelay_, votingPeriod_, 0)
GovernorVotes(token_)
GovernorPreventLateQuorum(voteExtension_)
{
_quorum = quorum_;
}

function quorum(uint256) public view virtual override returns (uint256) {
return _quorum;
}

function proposalDeadline(uint256 proposalId)
public
view
virtual
override(Governor, GovernorPreventLateQuorum)
returns (uint256)
{
return super.proposalDeadline(proposalId);
}

function proposalThreshold() public view virtual override(Governor, GovernorSettings) returns (uint256) {
return super.proposalThreshold();
}

function _castVote(
uint256 proposalId,
address account,
uint8 support,
string memory reason
) internal virtual override(Governor, GovernorPreventLateQuorum) returns (uint256) {
return super._castVote(proposalId, account, support, reason);
}
}
2 changes: 1 addition & 1 deletion test/governance/GovernorWorkflow.behavior.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ function runGovernorWorkflow () {
// vote
if (tryGet(this.settings, 'voters')) {
this.receipts.castVote = [];
for (const voter of this.settings.voters) {
for (const voter of this.settings.voters.filter(({ support }) => !!support)) {
if (!voter.signature) {
this.receipts.castVote.push(
await getReceiptOrRevert(
Expand Down
Loading

0 comments on commit a07c0a2

Please sign in to comment.