Skip to content

Commit c1d6ad5

Browse files
Amxxernestognwfrangio
authored
Add GovernorCountingFractional (#5045)
Co-authored-by: ernestognw <[email protected]> Co-authored-by: Francisco <[email protected]>
1 parent dd1e898 commit c1d6ad5

File tree

12 files changed

+487
-15
lines changed

12 files changed

+487
-15
lines changed

.changeset/eight-eyes-burn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`GovernorCountingFractional`: Add a governor counting module that allows distributing voting power amongst 3 options (For, Against, Abstain).

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
### Breaking changes
44

55
- `ERC1967Utils`: Removed duplicate declaration of the `Upgraded`, `AdminChanged` and `BeaconUpgraded` events. These events are still available through the `IERC1967` interface located under the `contracts/interfaces/` directory. Minimum pragma version is now 0.8.21.
6+
- `Governor`, `GovernorCountingSimple`: The `_countVotes` virtual function now returns an `uint256` with the total votes casted. This change allows for more flexibility for partial and fractional voting. Upgrading users may get a compilation error that can be fixed by adding a return statement to the `_countVotes` function.
67

78
### Custom error changes
89

contracts/governance/Governor.sol

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -255,9 +255,9 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72
255255
uint256 proposalId,
256256
address account,
257257
uint8 support,
258-
uint256 weight,
258+
uint256 totalWeight,
259259
bytes memory params
260-
) internal virtual;
260+
) internal virtual returns (uint256);
261261

262262
/**
263263
* @dev Default additional encoded parameters used by castVote methods that don't include them
@@ -639,16 +639,16 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72
639639
) internal virtual returns (uint256) {
640640
_validateStateBitmap(proposalId, _encodeStateBitmap(ProposalState.Active));
641641

642-
uint256 weight = _getVotes(account, proposalSnapshot(proposalId), params);
643-
_countVote(proposalId, account, support, weight, params);
642+
uint256 totalWeight = _getVotes(account, proposalSnapshot(proposalId), params);
643+
uint256 votedWeight = _countVote(proposalId, account, support, totalWeight, params);
644644

645645
if (params.length == 0) {
646-
emit VoteCast(account, proposalId, support, weight, reason);
646+
emit VoteCast(account, proposalId, support, votedWeight, reason);
647647
} else {
648-
emit VoteCastWithParams(account, proposalId, support, weight, reason, params);
648+
emit VoteCastWithParams(account, proposalId, support, votedWeight, reason, params);
649649
}
650650

651-
return weight;
651+
return votedWeight;
652652
}
653653

654654
/**

contracts/governance/IGovernor.sol

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,11 @@ interface IGovernor is IERC165, IERC6372 {
8383
*/
8484
error GovernorInvalidVoteType();
8585

86+
/**
87+
* @dev The provided params buffer is not supported by the counting module.
88+
*/
89+
error GovernorInvalidVoteParams();
90+
8691
/**
8792
* @dev Queue operation is not implemented for this governor. Execute should be called directly.
8893
*/

contracts/governance/README.adoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ Counting modules determine valid voting options.
2828

2929
* {GovernorCountingSimple}: Simple voting mechanism with 3 voting options: Against, For and Abstain.
3030

31+
* {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).
32+
3133
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.
3234

3335
* {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.
@@ -62,6 +64,8 @@ NOTE: Functions of the `Governor` contract do not include access control. If you
6264

6365
{{GovernorCountingSimple}}
6466

67+
{{GovernorCountingFractional}}
68+
6569
{{GovernorVotes}}
6670

6771
{{GovernorVotesQuorumFraction}}
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {Governor} from "../Governor.sol";
6+
import {GovernorCountingSimple} from "./GovernorCountingSimple.sol";
7+
import {Math} from "../../utils/math/Math.sol";
8+
9+
/**
10+
* @dev Extension of {Governor} for fractional voting.
11+
*
12+
* Similar to {GovernorCountingSimple}, this contract is a votes counting module for {Governor} that supports 3 options:
13+
* Against, For, Abstain. Additionally, it includes a fourth option: Fractional, which allows voters to split their voting
14+
* power amongst the other 3 options.
15+
*
16+
* Votes cast with the Fractional support must be accompanied by a `params` argument that is three packed `uint128` values
17+
* representing the weight the delegate assigns to Against, For, and Abstain respectively. For those votes cast for the other
18+
* 3 options, the `params` argument must be empty.
19+
*
20+
* This is mostly useful when the delegate is a contract that implements its own rules for voting. These delegate-contracts
21+
* can cast fractional votes according to the preferences of multiple entities delegating their voting power.
22+
*
23+
* Some example use cases include:
24+
*
25+
* * Voting from tokens that are held by a DeFi pool
26+
* * Voting from an L2 with tokens held by a bridge
27+
* * Voting privately from a shielded pool using zero knowledge proofs.
28+
*
29+
* Based on ScopeLift's GovernorCountingFractional[https://github.com/ScopeLift/flexible-voting/blob/e5de2efd1368387b840931f19f3c184c85842761/src/GovernorCountingFractional.sol]
30+
*/
31+
abstract contract GovernorCountingFractional is Governor {
32+
using Math for *;
33+
34+
uint8 internal constant VOTE_TYPE_FRACTIONAL = 255;
35+
36+
struct ProposalVote {
37+
uint256 againstVotes;
38+
uint256 forVotes;
39+
uint256 abstainVotes;
40+
mapping(address voter => uint256) usedVotes;
41+
}
42+
43+
/**
44+
* @dev Mapping from proposal ID to vote tallies for that proposal.
45+
*/
46+
mapping(uint256 => ProposalVote) private _proposalVotes;
47+
48+
/**
49+
* @dev A fractional vote params uses more votes than are available for that user.
50+
*/
51+
error GovernorExceedRemainingWeight(address voter, uint256 usedVotes, uint256 remainingWeight);
52+
53+
/**
54+
* @dev See {IGovernor-COUNTING_MODE}.
55+
*/
56+
// solhint-disable-next-line func-name-mixedcase
57+
function COUNTING_MODE() public pure virtual override returns (string memory) {
58+
return "support=bravo,fractional&quorum=for,abstain&params=fractional";
59+
}
60+
61+
/**
62+
* @dev See {IGovernor-hasVoted}.
63+
*/
64+
function hasVoted(uint256 proposalId, address account) public view virtual override returns (bool) {
65+
return usedVotes(proposalId, account) > 0;
66+
}
67+
68+
/**
69+
* @dev Get the number of votes already cast by `account` for a proposal with `proposalId`. Useful for
70+
* integrations that allow delegates to cast rolling, partial votes.
71+
*/
72+
function usedVotes(uint256 proposalId, address account) public view virtual returns (uint256) {
73+
return _proposalVotes[proposalId].usedVotes[account];
74+
}
75+
76+
/**
77+
* @dev Get current distribution of votes for a given proposal.
78+
*/
79+
function proposalVotes(
80+
uint256 proposalId
81+
) public view virtual returns (uint256 againstVotes, uint256 forVotes, uint256 abstainVotes) {
82+
ProposalVote storage proposalVote = _proposalVotes[proposalId];
83+
return (proposalVote.againstVotes, proposalVote.forVotes, proposalVote.abstainVotes);
84+
}
85+
86+
/**
87+
* @dev See {Governor-_quorumReached}.
88+
*/
89+
function _quorumReached(uint256 proposalId) internal view virtual override returns (bool) {
90+
ProposalVote storage proposalVote = _proposalVotes[proposalId];
91+
return quorum(proposalSnapshot(proposalId)) <= proposalVote.forVotes + proposalVote.abstainVotes;
92+
}
93+
94+
/**
95+
* @dev See {Governor-_voteSucceeded}. In this module, forVotes must be > againstVotes.
96+
*/
97+
function _voteSucceeded(uint256 proposalId) internal view virtual override returns (bool) {
98+
ProposalVote storage proposalVote = _proposalVotes[proposalId];
99+
return proposalVote.forVotes > proposalVote.againstVotes;
100+
}
101+
102+
/**
103+
* @dev See {Governor-_countVote}. Function that records the delegate's votes.
104+
*
105+
* Executing this function consumes (part of) the delegate's weight on the proposal. This weight can be
106+
* distributed amongst the 3 options (Against, For, Abstain) by specifying a fractional `support`.
107+
*
108+
* This counting module supports two vote casting modes: nominal and fractional.
109+
*
110+
* - Nominal: A nominal vote is cast by setting `support` to one of the 3 bravo options (Against, For, Abstain).
111+
* - Fractional: A fractional vote is cast by setting `support` to `type(uint8).max` (255).
112+
*
113+
* Casting a nominal vote requires `params` to be empty and consumes the delegate's full remaining weight on the
114+
* proposal for the specified `support` option. This is similar to the {GovernorCountingSimple} module and follows
115+
* the `VoteType` enum from Governor Bravo. As a consequence, no vote weight remains unspent so no further voting
116+
* is possible (for this `proposalId` and this `account`).
117+
*
118+
* Casting a fractional vote consumes a fraction of the delegate's remaining weight on the proposal according to the
119+
* weights the delegate assigns to each support option (Against, For, Abstain respectively). The sum total of the
120+
* three decoded vote weights _must_ be less than or equal to the delegate's remaining weight on the proposal (i.e.
121+
* their checkpointed total weight minus votes already cast on the proposal). This format can be produced using:
122+
*
123+
* `abi.encodePacked(uint128(againstVotes), uint128(forVotes), uint128(abstainVotes))`
124+
*
125+
* NOTE: Consider that fractional voting restricts the number of casted vote (in each category) to 128 bits.
126+
* Depending on how many decimals the underlying token has, a single voter may require to split their vote into
127+
* multiple vote operations. For precision higher than ~30 decimals, large token holders may require an
128+
* potentially large number of calls to cast all their votes. The voter has the possibility to cast all the
129+
* remaining votes in a single operation using the traditional "bravo" vote.
130+
*/
131+
// slither-disable-next-line cyclomatic-complexity
132+
function _countVote(
133+
uint256 proposalId,
134+
address account,
135+
uint8 support,
136+
uint256 totalWeight,
137+
bytes memory params
138+
) internal virtual override returns (uint256) {
139+
// Compute number of remaining votes. Returns 0 on overflow.
140+
(, uint256 remainingWeight) = totalWeight.trySub(usedVotes(proposalId, account));
141+
if (remainingWeight == 0) {
142+
revert GovernorAlreadyCastVote(account);
143+
}
144+
145+
uint256 againstVotes = 0;
146+
uint256 forVotes = 0;
147+
uint256 abstainVotes = 0;
148+
uint256 usedWeight;
149+
150+
// For clarity of event indexing, fractional voting must be clearly advertised in the "support" field.
151+
//
152+
// Supported `support` value must be:
153+
// - "Full" voting: `support = 0` (Against), `1` (For) or `2` (Abstain), with empty params.
154+
// - "Fractional" voting: `support = 255`, with 48 bytes params.
155+
if (support == uint8(GovernorCountingSimple.VoteType.Against)) {
156+
if (params.length != 0) revert GovernorInvalidVoteParams();
157+
usedWeight = againstVotes = remainingWeight;
158+
} else if (support == uint8(GovernorCountingSimple.VoteType.For)) {
159+
if (params.length != 0) revert GovernorInvalidVoteParams();
160+
usedWeight = forVotes = remainingWeight;
161+
} else if (support == uint8(GovernorCountingSimple.VoteType.Abstain)) {
162+
if (params.length != 0) revert GovernorInvalidVoteParams();
163+
usedWeight = abstainVotes = remainingWeight;
164+
} else if (support == VOTE_TYPE_FRACTIONAL) {
165+
// The `params` argument is expected to be three packed `uint128`:
166+
// `abi.encodePacked(uint128(againstVotes), uint128(forVotes), uint128(abstainVotes))`
167+
if (params.length != 0x30) revert GovernorInvalidVoteParams();
168+
169+
assembly ("memory-safe") {
170+
againstVotes := shr(128, mload(add(params, 0x20)))
171+
forVotes := shr(128, mload(add(params, 0x30)))
172+
abstainVotes := shr(128, mload(add(params, 0x40)))
173+
usedWeight := add(add(againstVotes, forVotes), abstainVotes) // inputs are uint128: cannot overflow
174+
}
175+
176+
// check parsed arguments are valid
177+
if (usedWeight > remainingWeight) {
178+
revert GovernorExceedRemainingWeight(account, usedWeight, remainingWeight);
179+
}
180+
} else {
181+
revert GovernorInvalidVoteType();
182+
}
183+
184+
// update votes tracking
185+
ProposalVote storage details = _proposalVotes[proposalId];
186+
if (againstVotes > 0) details.againstVotes += againstVotes;
187+
if (forVotes > 0) details.forVotes += forVotes;
188+
if (abstainVotes > 0) details.abstainVotes += abstainVotes;
189+
details.usedVotes[account] += usedWeight;
190+
191+
return usedWeight;
192+
}
193+
}

contracts/governance/extensions/GovernorCountingSimple.sol

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,9 @@ abstract contract GovernorCountingSimple is Governor {
7777
uint256 proposalId,
7878
address account,
7979
uint8 support,
80-
uint256 weight,
80+
uint256 totalWeight,
8181
bytes memory // params
82-
) internal virtual override {
82+
) internal virtual override returns (uint256) {
8383
ProposalVote storage proposalVote = _proposalVotes[proposalId];
8484

8585
if (proposalVote.hasVoted[account]) {
@@ -88,13 +88,15 @@ abstract contract GovernorCountingSimple is Governor {
8888
proposalVote.hasVoted[account] = true;
8989

9090
if (support == uint8(VoteType.Against)) {
91-
proposalVote.againstVotes += weight;
91+
proposalVote.againstVotes += totalWeight;
9292
} else if (support == uint8(VoteType.For)) {
93-
proposalVote.forVotes += weight;
93+
proposalVote.forVotes += totalWeight;
9494
} else if (support == uint8(VoteType.Abstain)) {
95-
proposalVote.abstainVotes += weight;
95+
proposalVote.abstainVotes += totalWeight;
9696
} else {
9797
revert GovernorInvalidVoteType();
9898
}
99+
100+
return totalWeight;
99101
}
100102
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {Governor} from "../../governance/Governor.sol";
6+
import {GovernorSettings} from "../../governance/extensions/GovernorSettings.sol";
7+
import {GovernorCountingFractional} from "../../governance/extensions/GovernorCountingFractional.sol";
8+
import {GovernorVotesQuorumFraction} from "../../governance/extensions/GovernorVotesQuorumFraction.sol";
9+
10+
abstract contract GovernorFractionalMock is GovernorSettings, GovernorVotesQuorumFraction, GovernorCountingFractional {
11+
function proposalThreshold() public view override(Governor, GovernorSettings) returns (uint256) {
12+
return super.proposalThreshold();
13+
}
14+
}

contracts/mocks/governance/GovernorWithParamsMock.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ abstract contract GovernorWithParamsMock is GovernorVotes, GovernorCountingSimpl
4141
uint8 support,
4242
uint256 weight,
4343
bytes memory params
44-
) internal override(Governor, GovernorCountingSimple) {
44+
) internal override(Governor, GovernorCountingSimple) returns (uint256) {
4545
if (params.length > 0) {
4646
(uint256 _uintParam, string memory _strParam) = abi.decode(params, (uint256, string));
4747
emit CountParams(_uintParam, _strParam);

test/governance/Governor.t.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,5 @@ contract GovernorInternalTest is Test, Governor {
5151

5252
function _getVotes(address, uint256, bytes memory) internal pure virtual override returns (uint256) {}
5353

54-
function _countVote(uint256, address, uint8, uint256, bytes memory) internal virtual override {}
54+
function _countVote(uint256, address, uint8, uint256, bytes memory) internal virtual override returns (uint256) {}
5555
}

0 commit comments

Comments
 (0)