Skip to content

Commit

Permalink
Delegate override vote (#5192)
Browse files Browse the repository at this point in the history
Co-authored-by: Arr00 <[email protected]>
  • Loading branch information
Amxx and arr00 authored Oct 18, 2024
1 parent 0034c30 commit 378914c
Show file tree
Hide file tree
Showing 15 changed files with 924 additions and 22 deletions.
5 changes: 5 additions & 0 deletions .changeset/great-lions-hear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': patch
---

`VotesExtended`: Create an extension of `Votes` which checkpoints balances and delegates.
5 changes: 5 additions & 0 deletions .changeset/pink-wasps-hammer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': patch
---

`GovernorCountingOverridable`: Add a governor counting module that enables token holders to override the vote of their delegate.
11 changes: 10 additions & 1 deletion contracts/governance/Governor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,13 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72
bytes memory params
) internal virtual returns (uint256);

/**
* @dev Hook that should be called every time the tally for a proposal is updated.
*
* Note: This function must run successfully. Reverts will result in the bricking of governance
*/
function _tallyUpdated(uint256 proposalId) internal virtual {}

/**
* @dev Default additional encoded parameters used by castVote methods that don't include them
*
Expand Down Expand Up @@ -649,6 +656,8 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72
emit VoteCastWithParams(account, proposalId, support, votedWeight, reason, params);
}

_tallyUpdated(proposalId);

return votedWeight;
}

Expand Down Expand Up @@ -732,7 +741,7 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72
*
* If requirements are not met, reverts with a {GovernorUnexpectedProposalState} error.
*/
function _validateStateBitmap(uint256 proposalId, bytes32 allowedStates) private view returns (ProposalState) {
function _validateStateBitmap(uint256 proposalId, bytes32 allowedStates) internal view returns (ProposalState) {
ProposalState currentState = state(proposalId);
if (_encodeStateBitmap(currentState) & allowedStates == bytes32(0)) {
revert GovernorUnexpectedProposalState(proposalId, currentState, allowedStates);
Expand Down
6 changes: 6 additions & 0 deletions contracts/governance/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ Counting modules determine valid voting options.

* {GovernorCountingFractional}: A more modular voting system that allows a user to vote with only part of its voting power, and to split that weight arbitrarily between the 3 different options (Against, For and Abstain).

* {GovernorCountingOverridable}: An extended version of `GovernorCountingSimple` which allows delegatees to override their delegates while the vote is live.

Timelock extensions add a delay for governance decisions to be executed. The workflow is extended to require a `queue` step before execution. With these modules, proposals are executed by the external timelock contract, thus it is the timelock that has to hold the assets that are being governed.

* {GovernorTimelockAccess}: Connects with an instance of an {AccessManager}. This allows restrictions (and delays) enforced by the manager to be considered by the Governor and integrated into the AccessManager's "schedule + execute" workflow.
Expand Down Expand Up @@ -66,6 +68,8 @@ NOTE: Functions of the `Governor` contract do not include access control. If you

{{GovernorCountingFractional}}

{{GovernorCountingOverride}}

{{GovernorVotes}}

{{GovernorVotesQuorumFraction}}
Expand All @@ -88,6 +92,8 @@ NOTE: Functions of the `Governor` contract do not include access control. If you

{{Votes}}

{{VotesExtended}}

== Timelock

In a governance system, the {TimelockController} contract is in charge of introducing a delay between a proposal and its execution. It can be used with or without a {Governor}.
Expand Down
212 changes: 212 additions & 0 deletions contracts/governance/extensions/GovernorCountingOverridable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {SignatureChecker} from "../../utils/cryptography/SignatureChecker.sol";
import {SafeCast} from "../../utils/math/SafeCast.sol";
import {VotesExtended} from "../utils/VotesExtended.sol";
import {GovernorVotes} from "./GovernorVotes.sol";

/**
* @dev Extension of {Governor} which enables delegatees to override the vote of their delegates. This module requires a
* token token that inherits `VotesExtended`.
*/
abstract contract GovernorCountingOverridable is GovernorVotes {
bytes32 public constant OVERRIDE_BALLOT_TYPEHASH =
keccak256("OverrideBallot(uint256 proposalId,uint8 support,address voter,uint256 nonce,string reason)");

/**
* @dev Supported vote types. Matches Governor Bravo ordering.
*/
enum VoteType {
Against,
For,
Abstain
}

struct VoteReceipt {
uint8 casted; // 0 if vote was not casted. Otherwise: support + 1
bool hasOverriden;
uint208 overridenWeight;
}

struct ProposalVote {
uint256[3] votes;
mapping(address voter => VoteReceipt) voteReceipt;
}

event VoteReduced(address indexed voter, uint256 proposalId, uint8 support, uint256 weight);
event OverrideVoteCast(address indexed voter, uint256 proposalId, uint8 support, uint256 weight, string reason);

error GovernorAlreadyOverridenVote(address account);

mapping(uint256 proposalId => ProposalVote) private _proposalVotes;

/**
* @dev See {IGovernor-COUNTING_MODE}.
*/
// solhint-disable-next-line func-name-mixedcase
function COUNTING_MODE() public pure virtual override returns (string memory) {
return "support=bravo,override&quorum=for,abstain&overridable=true";
}

/**
* @dev See {IGovernor-hasVoted}.
*/
function hasVoted(uint256 proposalId, address account) public view virtual override returns (bool) {
return _proposalVotes[proposalId].voteReceipt[account].casted != 0;
}

/**
* @dev Check if an `account` has overridden their delegate for a proposal.
*/
function hasVotedOverride(uint256 proposalId, address account) public view virtual returns (bool) {
return _proposalVotes[proposalId].voteReceipt[account].hasOverriden;
}

/**
* @dev Accessor to the internal vote counts.
*/
function proposalVotes(
uint256 proposalId
) public view virtual returns (uint256 againstVotes, uint256 forVotes, uint256 abstainVotes) {
uint256[3] storage votes = _proposalVotes[proposalId].votes;
return (votes[uint8(VoteType.Against)], votes[uint8(VoteType.For)], votes[uint8(VoteType.Abstain)]);
}

/**
* @dev See {Governor-_quorumReached}.
*/
function _quorumReached(uint256 proposalId) internal view virtual override returns (bool) {
uint256[3] storage votes = _proposalVotes[proposalId].votes;
return quorum(proposalSnapshot(proposalId)) <= votes[uint8(VoteType.For)] + votes[uint8(VoteType.Abstain)];
}

/**
* @dev See {Governor-_voteSucceeded}. In this module, the forVotes must be strictly over the againstVotes.
*/
function _voteSucceeded(uint256 proposalId) internal view virtual override returns (bool) {
uint256[3] storage votes = _proposalVotes[proposalId].votes;
return votes[uint8(VoteType.For)] > votes[uint8(VoteType.Against)];
}

/**
* @dev See {Governor-_countVote}. In this module, the support follows the `VoteType` enum (from Governor Bravo).
*
* NOTE: called by {Governor-_castVote} which emits the {IGovernor-VoteCast} (or {IGovernor-VoteCastWithParams})
* event.
*/
function _countVote(
uint256 proposalId,
address account,
uint8 support,
uint256 totalWeight,
bytes memory /*params*/
) internal virtual override returns (uint256) {
ProposalVote storage proposalVote = _proposalVotes[proposalId];

if (support > uint8(VoteType.Abstain)) {
revert GovernorInvalidVoteType();
}

if (proposalVote.voteReceipt[account].casted != 0) {
revert GovernorAlreadyCastVote(account);
}

totalWeight -= proposalVote.voteReceipt[account].overridenWeight;
proposalVote.votes[support] += totalWeight;
proposalVote.voteReceipt[account].casted = support + 1;

return totalWeight;
}

/// @dev Variant of {Governor-_countVote} that deals with vote overrides.
function _countOverride(uint256 proposalId, address account, uint8 support) internal virtual returns (uint256) {
ProposalVote storage proposalVote = _proposalVotes[proposalId];

if (support > uint8(VoteType.Abstain)) {
revert GovernorInvalidVoteType();
}

if (proposalVote.voteReceipt[account].hasOverriden) {
revert GovernorAlreadyOverridenVote(account);
}

uint256 proposalSnapshot = proposalSnapshot(proposalId);
uint256 overridenWeight = VotesExtended(address(token())).getPastBalanceOf(account, proposalSnapshot);
address delegate = VotesExtended(address(token())).getPastDelegate(account, proposalSnapshot);
uint8 delegateCasted = proposalVote.voteReceipt[delegate].casted;

proposalVote.voteReceipt[account].hasOverriden = true;
proposalVote.votes[support] += overridenWeight;
if (delegateCasted == 0) {
proposalVote.voteReceipt[delegate].overridenWeight += SafeCast.toUint208(overridenWeight);
} else {
uint8 delegateSupport = delegateCasted - 1;
proposalVote.votes[delegateSupport] -= overridenWeight;
emit VoteReduced(delegate, proposalId, delegateSupport, overridenWeight);
}

return overridenWeight;
}

/// @dev variant of {Governor-_castVote} that deals with vote overrides.
function _castOverride(
uint256 proposalId,
address account,
uint8 support,
string calldata reason
) internal virtual returns (uint256) {
_validateStateBitmap(proposalId, _encodeStateBitmap(ProposalState.Active));

uint256 overridenWeight = _countOverride(proposalId, account, support);

emit OverrideVoteCast(account, proposalId, support, overridenWeight, reason);

_tallyUpdated(proposalId);

return overridenWeight;
}

/// @dev Public function for casting an override vote
function castOverrideVote(
uint256 proposalId,
uint8 support,
string calldata reason
) public virtual returns (uint256) {
address voter = _msgSender();
return _castOverride(proposalId, voter, support, reason);
}

/// @dev Public function for casting an override vote using a voter's signature
function castOverrideVoteBySig(
uint256 proposalId,
uint8 support,
address voter,
string calldata reason,
bytes calldata signature
) public virtual returns (uint256) {
bool valid = SignatureChecker.isValidSignatureNow(
voter,
_hashTypedDataV4(
keccak256(
abi.encode(
OVERRIDE_BALLOT_TYPEHASH,
proposalId,
support,
voter,
_useNonce(voter),
keccak256(bytes(reason))
)
)
),
signature
);

if (!valid) {
revert GovernorInvalidSignature(voter);
}

return _castOverride(proposalId, voter, support, reason);
}
}
16 changes: 3 additions & 13 deletions contracts/governance/extensions/GovernorPreventLateQuorum.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,20 +44,12 @@ abstract contract GovernorPreventLateQuorum is Governor {
}

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

function _tallyUpdated(uint256 proposalId) internal virtual override {
super._tallyUpdated(proposalId);
if (_extendedDeadlines[proposalId] == 0 && _quorumReached(proposalId)) {
uint48 extendedDeadline = clock() + lateQuorumVoteExtension();

Expand All @@ -67,8 +59,6 @@ abstract contract GovernorPreventLateQuorum is Governor {

_extendedDeadlines[proposalId] = extendedDeadline;
}

return result;
}

/**
Expand Down
70 changes: 70 additions & 0 deletions contracts/governance/utils/VotesExtended.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Checkpoints} from "../../utils/structs/Checkpoints.sol";
import {Votes} from "./Votes.sol";
import {SafeCast} from "../../utils/math/SafeCast.sol";

/**
* @dev Extension of {Votes} that adds exposes checkpoints for delegations and balances.
*/
abstract contract VotesExtended is Votes {
using SafeCast for uint256;
using Checkpoints for Checkpoints.Trace160;
using Checkpoints for Checkpoints.Trace208;

mapping(address delegatee => Checkpoints.Trace160) private _delegateCheckpoints;
mapping(address account => Checkpoints.Trace208) private _balanceOfCheckpoints;

/**
* @dev Returns the delegate of an `account` at a specific moment in the past. If the `clock()` is
* configured to use block numbers, this will return the value at the end of the corresponding block.
*
* Requirements:
*
* - `timepoint` must be in the past. If operating using block numbers, the block must be already mined.
*/
function getPastDelegate(address account, uint256 timepoint) public view virtual returns (address) {
uint48 currentTimepoint = clock();
if (timepoint >= currentTimepoint) {
revert ERC5805FutureLookup(timepoint, currentTimepoint);
}
return address(_delegateCheckpoints[account].upperLookupRecent(timepoint.toUint48()));
}

/**
* @dev Returns the `balanceOf` of an `account` at a specific moment in the past. If the `clock()` is
* configured to use block numbers, this will return the value at the end of the corresponding block.
*
* Requirements:
*
* - `timepoint` must be in the past. If operating using block numbers, the block must be already mined.
*/
function getPastBalanceOf(address account, uint256 timepoint) public view virtual returns (uint256) {
uint48 currentTimepoint = clock();
if (timepoint >= currentTimepoint) {
revert ERC5805FutureLookup(timepoint, currentTimepoint);
}
return _balanceOfCheckpoints[account].upperLookupRecent(timepoint.toUint48());
}

/// @inheritdoc Votes
function _delegate(address account, address delegatee) internal virtual override {
super._delegate(account, delegatee);

_delegateCheckpoints[account].push(clock(), uint160(delegatee));
}

/// @inheritdoc Votes
function _transferVotingUnits(address from, address to, uint256 amount) internal virtual override {
super._transferVotingUnits(from, to, amount);
if (from != to) {
if (from != address(0)) {
_balanceOfCheckpoints[from].push(clock(), _getVotingUnits(from).toUint208());
}
if (to != address(0)) {
_balanceOfCheckpoints[to].push(clock(), _getVotingUnits(to).toUint208());
}
}
}
}
Loading

0 comments on commit 378914c

Please sign in to comment.