From 729ef4c8235d91de5160409aff1973446280f052 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 15 May 2024 15:05:42 +0200 Subject: [PATCH 01/26] GovernorCountingFractional --- .../extensions/GovernorCountingFractional.sol | 187 +++++++++++++++++ .../governance/GovernorFractionalMock.sol | 14 ++ .../GovernorCountingFractional.test.js | 195 ++++++++++++++++++ 3 files changed, 396 insertions(+) create mode 100644 contracts/governance/extensions/GovernorCountingFractional.sol create mode 100644 contracts/mocks/governance/GovernorFractionalMock.sol create mode 100644 test/governance/extensions/GovernorCountingFractional.test.js diff --git a/contracts/governance/extensions/GovernorCountingFractional.sol b/contracts/governance/extensions/GovernorCountingFractional.sol new file mode 100644 index 00000000000..4e172e8c235 --- /dev/null +++ b/contracts/governance/extensions/GovernorCountingFractional.sol @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Governor} from "../Governor.sol"; +import {GovernorCountingSimple} from "./GovernorCountingSimple.sol"; +import {Math} from "../../utils/math/Math.sol"; + +/** + * @notice Extension of {Governor} for 3 option fractional vote counting. When voting, a delegate may split their vote + * weight between Against/For/Abstain. This is most useful when the delegate is itself a contract, implementing its + * own rules for voting. By allowing a contract-delegate to split its vote weight, the voting preferences of many + * disparate token holders can be rolled up into a single vote to the Governor itself. Some example use cases include + * voting with tokens that are held by a DeFi pool, voting from L2 with tokens held by a bridge, or voting privately + * from a shielded pool using zero knowledge proofs. + * + * Based on ScopeLift's https://github.com/ScopeLift/flexible-voting/blob/master/src/GovernorCountingFractional.sol + */ +abstract contract GovernorCountingFractional is Governor { + using Math for *; + + struct ProposalVote { + uint256 againstVotes; + uint256 forVotes; + uint256 abstainVotes; + mapping(address voter => uint256) usedVotes; + } + + /** + * @dev Mapping from proposal ID to vote tallies for that proposal. + */ + mapping(uint256 => ProposalVote) private _proposalVotes; + + error GovernorInvalidParamsFormat(address voter); + error GovernorUsedVotesExceedRemainingWeight(address voter, uint256 usedVotes, uint256 remainingWeight); + + /** + * @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&quorum=for,abstain¶ms=fractional"; + } + + /** + * @dev See {IGovernor-hasVoted}. + */ + function hasVoted(uint256 proposalId, address account) public view virtual override returns (bool) { + return voteWeightCast(proposalId, account) > 0; + } + + /** + * @dev Get the number of votes cast thus far on proposal `proposalId` by account `account`. Useful for + * integrations that allow delegates to cast rolling, partial votes. + */ + function voteWeightCast(uint256 proposalId, address account) public view virtual returns (uint256) { + return _proposalVotes[proposalId].usedVotes[account]; + } + + /** + * @dev Accessor to the internal vote counts. + */ + function proposalVotes( + uint256 proposalId + ) public view virtual returns (uint256 againstVotes, uint256 forVotes, uint256 abstainVotes) { + ProposalVote storage proposalVote = _proposalVotes[proposalId]; + return (proposalVote.againstVotes, proposalVote.forVotes, proposalVote.abstainVotes); + } + + /** + * @dev See {Governor-_quorumReached}. + */ + function _quorumReached(uint256 proposalId) internal view virtual override returns (bool) { + ProposalVote storage proposalVote = _proposalVotes[proposalId]; + return quorum(proposalSnapshot(proposalId)) <= proposalVote.forVotes + proposalVote.abstainVotes; + } + + /** + * @dev See {Governor-_voteSucceeded}. In this module, forVotes must be > againstVotes. + */ + function _voteSucceeded(uint256 proposalId) internal view virtual override returns (bool) { + ProposalVote storage proposalVote = _proposalVotes[proposalId]; + return proposalVote.forVotes > proposalVote.againstVotes; + } + + /** + * @notice See {Governor-_countVote}. + * + * @dev Function that records the delegate's votes. + * + * If the `params` bytes parameter is empty, then this module behaves identically to {GovernorCountingSimple}. + * That is, it assigns the remaining weight of the delegate to the `support` parameter, which follows the + * `VoteType` enum from Governor Bravo (as defined in {GovernorCountingSimple}). + * + * If the `params` bytes parameter is not zero, then it _must_ be tree packed uint128s, totaling 48 bytes, + * representing the weight the delegate assigns to Against, For, and Abstain respectively. This format can be + * produced using: + * + * `abi.encodePacked(uint128(againstVotes), uint128(forVotes), uint128(abstainVotes))` + * + * The sum total of the three decoded vote weights _must_ be less than or equal to the delegate's remaining weight + * on the proposal, i.e. their checkpointed total weight minus votes already cast on the proposal. + * + * See `_countVoteNominal` and `_countVoteFractional` for more details. + */ + function _countVote( + uint256 proposalId, + address account, + uint8 support, + uint256 totalWeight, + bytes memory params + ) internal virtual override { + // Compute number of remaining votes. Returns 0 on overflow. + (, uint256 remainingWeight) = totalWeight.trySub(voteWeightCast(proposalId, account)); + if (remainingWeight == 0) { + revert GovernorAlreadyCastVote(account); + } + + if (params.length == 0) { + _countVoteNominal(proposalId, account, support, remainingWeight); + } else if (params.length == 0x30) { + _countVoteFractional(proposalId, account, params, remainingWeight); + } else { + revert GovernorInvalidParamsFormat(account); + } + } + + /** + * @dev Record votes with full weight cast for `support`. + * + * Because this function votes with the delegate's remaining weight, it can only be called once per proposal and + * thus does not require any replay protection. + */ + function _countVoteNominal(uint256 proposalId, address account, uint8 support, uint256 weight) private { + ProposalVote storage details = _proposalVotes[proposalId]; + details.usedVotes[account] += weight; + + if (support == uint8(GovernorCountingSimple.VoteType.Against)) { + details.againstVotes += weight; + } else if (support == uint8(GovernorCountingSimple.VoteType.For)) { + details.forVotes += weight; + } else if (support == uint8(GovernorCountingSimple.VoteType.Abstain)) { + details.abstainVotes += weight; + } else { + revert GovernorInvalidVoteType(); + } + } + + /** + * @dev Count votes with fractional weight. + * + * `params` is expected to be tree packed uint128s: + * `abi.encodePacked(uint128(againstVotes), uint128(forVotes), uint128(abstainVotes))` + * + * This function can be called multiple times for the same account and proposal, i.e. partial/rolling votes are + * allowed. For example, an account with total weight of 10 could call this function three times with the + * following vote data: + * - against: 1, for: 0, abstain: 2 + * - against: 3, for: 1, abstain: 0 + * - against: 1, for: 1, abstain: 1 + * The result of these three calls would be that the account casts 5 votes AGAINST, 2 votes FOR, and 3 votes + * ABSTAIN on the proposal. Though partial, votes are still final once cast and cannot be changed or overridden. + * Subsequent partial votes simply increment existing totals. + */ + function _countVoteFractional(uint256 proposalId, address account, bytes memory params, uint256 weight) private { + uint128 againstVotes = _extractUint128(params, 0); + uint128 forVotes = _extractUint128(params, 1); + uint128 abstainVotes = _extractUint128(params, 2); + + uint256 usedWeight = againstVotes + forVotes + abstainVotes; + if (usedWeight > weight) { + revert GovernorUsedVotesExceedRemainingWeight(account, usedWeight, weight); + } + + ProposalVote storage details = _proposalVotes[proposalId]; + details.againstVotes += againstVotes; + details.forVotes += forVotes; + details.abstainVotes += abstainVotes; + details.usedVotes[account] += usedWeight; + } + + function _extractUint128(bytes memory data, uint256 pos) private pure returns (uint128 result) { + assembly { + result := shr(128, mload(add(data, add(0x20, mul(0x10, pos))))) + } + } +} diff --git a/contracts/mocks/governance/GovernorFractionalMock.sol b/contracts/mocks/governance/GovernorFractionalMock.sol new file mode 100644 index 00000000000..d6d6042a273 --- /dev/null +++ b/contracts/mocks/governance/GovernorFractionalMock.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Governor} from "../../governance/Governor.sol"; +import {GovernorSettings} from "../../governance/extensions/GovernorSettings.sol"; +import {GovernorCountingFractional} from "../../governance/extensions/GovernorCountingFractional.sol"; +import {GovernorVotesQuorumFraction} from "../../governance/extensions/GovernorVotesQuorumFraction.sol"; + +abstract contract GovernorFractionalMock is GovernorSettings, GovernorVotesQuorumFraction, GovernorCountingFractional { + function proposalThreshold() public view override(Governor, GovernorSettings) returns (uint256) { + return super.proposalThreshold(); + } +} diff --git a/test/governance/extensions/GovernorCountingFractional.test.js b/test/governance/extensions/GovernorCountingFractional.test.js new file mode 100644 index 00000000000..d3bb7ae789f --- /dev/null +++ b/test/governance/extensions/GovernorCountingFractional.test.js @@ -0,0 +1,195 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +const { GovernorHelper } = require('../../helpers/governance'); +const { VoteType } = require('../../helpers/enums'); +const { zip } = require('../../helpers/iterate'); +const { sum } = require('../../helpers/math'); + +const TOKENS = [ + { Token: '$ERC20Votes', mode: 'blocknumber' }, + { Token: '$ERC20VotesTimestampMock', mode: 'timestamp' }, +]; + +const name = 'OZ-Governor'; +const version = '1'; +const tokenName = 'MockToken'; +const tokenSymbol = 'MTKN'; +const tokenSupply = ethers.parseEther('100'); +const votingDelay = 4n; +const votingPeriod = 16n; +const value = ethers.parseEther('1'); + +describe('GovernorCountingFractional', function () { + for (const { Token, mode } of TOKENS) { + const fixture = async () => { + const [owner, proposer, voter1, voter2, voter3, voter4, other] = await ethers.getSigners(); + const receiver = await ethers.deployContract('CallReceiverMock'); + + const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]); + const mock = await ethers.deployContract('$GovernorFractionalMock', [ + name, // name + votingDelay, // initialVotingDelay + votingPeriod, // initialVotingPeriod + 0n, // initialProposalThreshold + token, // tokenAddress + 10n, // quorumNumeratorValue + ]); + + await owner.sendTransaction({ to: mock, value }); + await token.$_mint(owner, tokenSupply); + + const helper = new GovernorHelper(mock, mode); + await helper.connect(owner).delegate({ token, to: voter1, value: ethers.parseEther('10') }); + await helper.connect(owner).delegate({ token, to: voter2, value: ethers.parseEther('7') }); + await helper.connect(owner).delegate({ token, to: voter3, value: ethers.parseEther('5') }); + await helper.connect(owner).delegate({ token, to: voter4, value: ethers.parseEther('2') }); + + return { owner, proposer, voter1, voter2, voter3, voter4, other, receiver, token, mock, helper }; + }; + + describe(`using ${Token}`, function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + + // default proposal + this.proposal = this.helper.setProposal( + [ + { + target: this.receiver.target, + value, + data: this.receiver.interface.encodeFunctionData('mockFunction'), + }, + ], + '', + ); + }); + + it('deployment check', async function () { + expect(await this.mock.name()).to.equal(name); + expect(await this.mock.token()).to.equal(this.token); + expect(await this.mock.votingDelay()).to.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.equal(votingPeriod); + }); + + it('nominal is unaffected', async function () { + await this.helper.connect(this.proposer).propose(); + await this.helper.waitForSnapshot(); + await this.helper.connect(this.voter1).vote({ support: VoteType.For, reason: 'This is nice' }); + await this.helper.connect(this.voter2).vote({ support: VoteType.For }); + await this.helper.connect(this.voter3).vote({ support: VoteType.Against }); + await this.helper.connect(this.voter4).vote({ support: VoteType.Abstain }); + await this.helper.waitForDeadline(); + await this.helper.execute(); + + expect(await this.mock.hasVoted(this.proposal.id, this.owner)).to.be.false; + expect(await this.mock.hasVoted(this.proposal.id, this.voter1)).to.be.true; + expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.be.true; + expect(await ethers.provider.getBalance(this.mock)).to.equal(0n); + expect(await ethers.provider.getBalance(this.receiver)).to.equal(value); + }); + + it('voting with a fraction of the weight: twice', async function () { + await this.helper.connect(this.proposer).propose(); + await this.helper.waitForSnapshot(); + + expect(await this.mock.proposalVotes(this.proposal.id)).to.deep.equal([0n, 0n, 0n]); + expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.equal(false); + expect(await this.mock.voteWeightCast(this.proposal.id, this.voter2)).to.equal(0n); + + const steps = [['0', '2', '1'].map(ethers.parseEther), ['1', '0', '1'].map(ethers.parseEther)]; + + for (const votes of steps) { + const params = ethers.solidityPacked(['uint128', 'uint128', 'uint128'], votes); + await expect( + this.helper.connect(this.voter2).vote({ + support: 0, + reason: 'no particular reason', + params, + }), + ) + .to.emit(this.mock, 'VoteCastWithParams') + .withArgs( + this.voter2, + this.proposal.id, + 0, + ethers.parseEther('7'), // total weight for voter2 + 'no particular reason', + params, + ); + } + + expect(await this.mock.proposalVotes(this.proposal.id)).to.deep.equal(zip(...steps).map(v => sum(...v))); + expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.equal(true); + expect(await this.mock.voteWeightCast(this.proposal.id, this.voter2)).to.equal(sum(...[].concat(...steps))); + }); + + it('voting with a fraction of the weight, then finish with nominal', async function () { + await this.helper.connect(this.proposer).propose(); + await this.helper.waitForSnapshot(); + + expect(await this.mock.proposalVotes(this.proposal.id)).to.deep.equal([0n, 0n, 0n]); + expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.equal(false); + expect(await this.mock.voteWeightCast(this.proposal.id, this.voter2)).to.equal(0n); + + const weight = ethers.parseEther('7'); + const fractional = ['1', '2', '1'].map(ethers.parseEther); + + const params = ethers.solidityPacked(['uint128', 'uint128', 'uint128'], fractional); + await expect( + this.helper.connect(this.voter2).vote({ + support: 0, + reason: 'no particular reason', + params, + }), + ) + .to.emit(this.mock, 'VoteCastWithParams') + .withArgs( + this.voter2, + this.proposal.id, + 0, + weight, // total weight for voter2 + 'no particular reason', + params, + ); + + await expect(this.helper.connect(this.voter2).vote({ support: VoteType.Against })) + .to.emit(this.mock, 'VoteCast') + .withArgs( + this.voter2, + this.proposal.id, + VoteType.Against, + weight, // total weight for voter2 + '', + ); + + expect(await this.mock.proposalVotes(this.proposal.id)).to.deep.equal([ + weight - sum(...fractional.slice(1)), + ...fractional.slice(1), + ]); + expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.equal(true); + expect(await this.mock.voteWeightCast(this.proposal.id, this.voter2)).to.equal(weight); + }); + + it('voting with a fraction of the weight: params spend more than available', async function () { + await this.helper.connect(this.proposer).propose(); + await this.helper.waitForSnapshot(); + + const weight = ethers.parseEther('7'); + const fractional = ['0', '1000', '0'].map(ethers.parseEther); + + const params = ethers.solidityPacked(['uint128', 'uint128', 'uint128'], fractional); + await expect( + this.helper.connect(this.voter2).vote({ + support: 0, + reason: 'no particular reason', + params, + }), + ) + .to.be.revertedWithCustomError(this.mock, 'GovernorUsedVotesExceedRemainingWeight') + .withArgs(this.voter2, sum(...fractional), weight); + }); + }); + } +}); From b56632acead0c6de2895413db335f77e8b3d8982 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 15 May 2024 15:12:27 +0200 Subject: [PATCH 02/26] changeset --- .changeset/eight-eyes-burn.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/eight-eyes-burn.md diff --git a/.changeset/eight-eyes-burn.md b/.changeset/eight-eyes-burn.md new file mode 100644 index 00000000000..b72d5437319 --- /dev/null +++ b/.changeset/eight-eyes-burn.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`GovernorCountingFlexible`: Add a governor counting module that support more flexible voting (voting with part of the weight, splitting the votes) From 66f2b3be604339071996962eba24c5e1c807154f Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 15 May 2024 15:21:14 +0200 Subject: [PATCH 03/26] documentation --- contracts/governance/README.adoc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contracts/governance/README.adoc b/contracts/governance/README.adoc index 3528c84ab07..da678f4abb2 100644 --- a/contracts/governance/README.adoc +++ b/contracts/governance/README.adoc @@ -28,6 +28,8 @@ Counting modules determine valid voting options. * {GovernorCountingSimple}: Simple voting mechanism with 3 voting options: Against, For and Abstain. +* {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). + 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. @@ -62,6 +64,8 @@ NOTE: Functions of the `Governor` contract do not include access control. If you {{GovernorCountingSimple}} +{{GovernorCountingFractional}} + {{GovernorVotes}} {{GovernorVotesQuorumFraction}} From 459ce71669489034c848c4deaf08dee53ed27d98 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 15 May 2024 16:40:59 +0200 Subject: [PATCH 04/26] coverage --- .../GovernorCountingFractional.test.js | 197 +++++++++++------- 1 file changed, 120 insertions(+), 77 deletions(-) diff --git a/test/governance/extensions/GovernorCountingFractional.test.js b/test/governance/extensions/GovernorCountingFractional.test.js index d3bb7ae789f..af839418e5c 100644 --- a/test/governance/extensions/GovernorCountingFractional.test.js +++ b/test/governance/extensions/GovernorCountingFractional.test.js @@ -71,6 +71,7 @@ describe('GovernorCountingFractional', function () { expect(await this.mock.token()).to.equal(this.token); expect(await this.mock.votingDelay()).to.equal(votingDelay); expect(await this.mock.votingPeriod()).to.equal(votingPeriod); + expect(await this.mock.COUNTING_MODE()).to.equal('support=bravo&quorum=for,abstain¶ms=fractional'); }); it('nominal is unaffected', async function () { @@ -90,18 +91,54 @@ describe('GovernorCountingFractional', function () { expect(await ethers.provider.getBalance(this.receiver)).to.equal(value); }); - it('voting with a fraction of the weight: twice', async function () { - await this.helper.connect(this.proposer).propose(); - await this.helper.waitForSnapshot(); - - expect(await this.mock.proposalVotes(this.proposal.id)).to.deep.equal([0n, 0n, 0n]); - expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.equal(false); - expect(await this.mock.voteWeightCast(this.proposal.id, this.voter2)).to.equal(0n); - - const steps = [['0', '2', '1'].map(ethers.parseEther), ['1', '0', '1'].map(ethers.parseEther)]; - - for (const votes of steps) { - const params = ethers.solidityPacked(['uint128', 'uint128', 'uint128'], votes); + describe('voting with a fraction of the weight', function () { + it('twice', async function () { + await this.helper.connect(this.proposer).propose(); + await this.helper.waitForSnapshot(); + + expect(await this.mock.proposalVotes(this.proposal.id)).to.deep.equal([0n, 0n, 0n]); + expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.equal(false); + expect(await this.mock.voteWeightCast(this.proposal.id, this.voter2)).to.equal(0n); + + const steps = [['0', '2', '1'].map(ethers.parseEther), ['1', '0', '1'].map(ethers.parseEther)]; + + for (const votes of steps) { + const params = ethers.solidityPacked(['uint128', 'uint128', 'uint128'], votes); + await expect( + this.helper.connect(this.voter2).vote({ + support: 0, + reason: 'no particular reason', + params, + }), + ) + .to.emit(this.mock, 'VoteCastWithParams') + .withArgs( + this.voter2, + this.proposal.id, + 0, + ethers.parseEther('7'), // total weight for voter2 + 'no particular reason', + params, + ); + } + + expect(await this.mock.proposalVotes(this.proposal.id)).to.deep.equal(zip(...steps).map(v => sum(...v))); + expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.equal(true); + expect(await this.mock.voteWeightCast(this.proposal.id, this.voter2)).to.equal(sum(...[].concat(...steps))); + }); + + it('fractional then nominal', async function () { + await this.helper.connect(this.proposer).propose(); + await this.helper.waitForSnapshot(); + + expect(await this.mock.proposalVotes(this.proposal.id)).to.deep.equal([0n, 0n, 0n]); + expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.equal(false); + expect(await this.mock.voteWeightCast(this.proposal.id, this.voter2)).to.equal(0n); + + const weight = ethers.parseEther('7'); + const fractional = ['1', '2', '1'].map(ethers.parseEther); + + const params = ethers.solidityPacked(['uint128', 'uint128', 'uint128'], fractional); await expect( this.helper.connect(this.voter2).vote({ support: 0, @@ -114,81 +151,87 @@ describe('GovernorCountingFractional', function () { this.voter2, this.proposal.id, 0, - ethers.parseEther('7'), // total weight for voter2 + weight, // total weight for voter2 'no particular reason', params, ); - } - expect(await this.mock.proposalVotes(this.proposal.id)).to.deep.equal(zip(...steps).map(v => sum(...v))); - expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.equal(true); - expect(await this.mock.voteWeightCast(this.proposal.id, this.voter2)).to.equal(sum(...[].concat(...steps))); - }); + await expect(this.helper.connect(this.voter2).vote({ support: VoteType.Against })) + .to.emit(this.mock, 'VoteCast') + .withArgs( + this.voter2, + this.proposal.id, + VoteType.Against, + weight, // total weight for voter2 + '', + ); - it('voting with a fraction of the weight, then finish with nominal', async function () { - await this.helper.connect(this.proposer).propose(); - await this.helper.waitForSnapshot(); + expect(await this.mock.proposalVotes(this.proposal.id)).to.deep.equal([ + weight - sum(...fractional.slice(1)), + ...fractional.slice(1), + ]); + expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.equal(true); + expect(await this.mock.voteWeightCast(this.proposal.id, this.voter2)).to.equal(weight); + }); - expect(await this.mock.proposalVotes(this.proposal.id)).to.deep.equal([0n, 0n, 0n]); - expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.equal(false); - expect(await this.mock.voteWeightCast(this.proposal.id, this.voter2)).to.equal(0n); - - const weight = ethers.parseEther('7'); - const fractional = ['1', '2', '1'].map(ethers.parseEther); - - const params = ethers.solidityPacked(['uint128', 'uint128', 'uint128'], fractional); - await expect( - this.helper.connect(this.voter2).vote({ - support: 0, - reason: 'no particular reason', - params, - }), - ) - .to.emit(this.mock, 'VoteCastWithParams') - .withArgs( - this.voter2, - this.proposal.id, - 0, - weight, // total weight for voter2 - 'no particular reason', - params, - ); + it('revert if params spend more than available', async function () { + await this.helper.connect(this.proposer).propose(); + await this.helper.waitForSnapshot(); - await expect(this.helper.connect(this.voter2).vote({ support: VoteType.Against })) - .to.emit(this.mock, 'VoteCast') - .withArgs( - this.voter2, - this.proposal.id, - VoteType.Against, - weight, // total weight for voter2 - '', - ); + const weight = ethers.parseEther('7'); + const fractional = ['0', '1000', '0'].map(ethers.parseEther); - expect(await this.mock.proposalVotes(this.proposal.id)).to.deep.equal([ - weight - sum(...fractional.slice(1)), - ...fractional.slice(1), - ]); - expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.equal(true); - expect(await this.mock.voteWeightCast(this.proposal.id, this.voter2)).to.equal(weight); - }); + await expect( + this.helper.connect(this.voter2).vote({ + support: 0, + reason: 'no particular reason', + params: ethers.solidityPacked(['uint128', 'uint128', 'uint128'], fractional), + }), + ) + .to.be.revertedWithCustomError(this.mock, 'GovernorUsedVotesExceedRemainingWeight') + .withArgs(this.voter2, sum(...fractional), weight); + }); - it('voting with a fraction of the weight: params spend more than available', async function () { - await this.helper.connect(this.proposer).propose(); - await this.helper.waitForSnapshot(); + it('revert if no weight remaining', async function () { + await this.helper.connect(this.proposer).propose(); + await this.helper.waitForSnapshot(); + await this.helper.connect(this.voter2).vote({ support: VoteType.For }); + + await expect( + this.helper.connect(this.voter2).vote({ + support: 0, + reason: 'no particular reason', + params: ethers.solidityPacked(['uint128', 'uint128', 'uint128'], [0n, 1n, 0n]), + }), + ) + .to.be.revertedWithCustomError(this.mock, 'GovernorAlreadyCastVote') + .withArgs(this.voter2); + }); - const weight = ethers.parseEther('7'); - const fractional = ['0', '1000', '0'].map(ethers.parseEther); - - const params = ethers.solidityPacked(['uint128', 'uint128', 'uint128'], fractional); - await expect( - this.helper.connect(this.voter2).vote({ - support: 0, - reason: 'no particular reason', - params, - }), - ) - .to.be.revertedWithCustomError(this.mock, 'GovernorUsedVotesExceedRemainingWeight') - .withArgs(this.voter2, sum(...fractional), weight); + it('revert if params are not properly formated', async function () { + await this.helper.connect(this.proposer).propose(); + await this.helper.waitForSnapshot(); + + await expect( + this.helper.connect(this.voter2).vote({ + support: 0, + reason: 'no particular reason', + params: ethers.solidityPacked(['uint128', 'uint128'], [0n, 1n]), + }), + ) + .to.be.revertedWithCustomError(this.mock, 'GovernorInvalidParamsFormat') + .withArgs(this.voter2); + }); + + it('revert if vote type is invalid', async function () { + await this.helper.connect(this.proposer).propose(); + await this.helper.waitForSnapshot(); + + await expect(this.helper.connect(this.voter2).vote({ support: 128n })).to.be.revertedWithCustomError( + this.mock, + 'GovernorInvalidVoteType', + ); + }); }); }); } From 7cff638e76de57f44fbf165214317e6d08dd84eb Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 15 May 2024 16:50:11 +0200 Subject: [PATCH 05/26] update --- .../governance/extensions/GovernorCountingFractional.test.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/governance/extensions/GovernorCountingFractional.test.js b/test/governance/extensions/GovernorCountingFractional.test.js index af839418e5c..36fc6b12c0d 100644 --- a/test/governance/extensions/GovernorCountingFractional.test.js +++ b/test/governance/extensions/GovernorCountingFractional.test.js @@ -100,7 +100,10 @@ describe('GovernorCountingFractional', function () { expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.equal(false); expect(await this.mock.voteWeightCast(this.proposal.id, this.voter2)).to.equal(0n); - const steps = [['0', '2', '1'].map(ethers.parseEther), ['1', '0', '1'].map(ethers.parseEther)]; + const steps = [ + ['0', '2', '1'], + ['1', '0', '1'], + ].map(votes => votes.map(vote => ethers.parseEther(vote))); for (const votes of steps) { const params = ethers.solidityPacked(['uint128', 'uint128', 'uint128'], votes); From 12123b0255e9bd5abd7e7511975d26e79c326a7b Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 15 May 2024 18:45:04 +0200 Subject: [PATCH 06/26] fix event, in a breaking way --- contracts/governance/Governor.sol | 14 +++++----- .../extensions/GovernorCountingFractional.sol | 24 +++++++++++++---- .../extensions/GovernorCountingSimple.sol | 12 +++++---- .../governance/GovernorWithParamsMock.sol | 2 +- .../GovernorCountingFractional.test.js | 26 +++---------------- 5 files changed, 37 insertions(+), 41 deletions(-) diff --git a/contracts/governance/Governor.sol b/contracts/governance/Governor.sol index 3edc71a4069..be2cf79d263 100644 --- a/contracts/governance/Governor.sol +++ b/contracts/governance/Governor.sol @@ -255,9 +255,9 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72 uint256 proposalId, address account, uint8 support, - uint256 weight, + uint256 totalWeight, bytes memory params - ) internal virtual; + ) internal virtual returns (uint256); /** * @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 ) internal virtual returns (uint256) { _validateStateBitmap(proposalId, _encodeStateBitmap(ProposalState.Active)); - uint256 weight = _getVotes(account, proposalSnapshot(proposalId), params); - _countVote(proposalId, account, support, weight, params); + uint256 totalWeight = _getVotes(account, proposalSnapshot(proposalId), params); + uint256 votedWeight = _countVote(proposalId, account, support, totalWeight, params); if (params.length == 0) { - emit VoteCast(account, proposalId, support, weight, reason); + emit VoteCast(account, proposalId, support, votedWeight, reason); } else { - emit VoteCastWithParams(account, proposalId, support, weight, reason, params); + emit VoteCastWithParams(account, proposalId, support, votedWeight, reason, params); } - return weight; + return votedWeight; } /** diff --git a/contracts/governance/extensions/GovernorCountingFractional.sol b/contracts/governance/extensions/GovernorCountingFractional.sol index 4e172e8c235..a510623a61a 100644 --- a/contracts/governance/extensions/GovernorCountingFractional.sol +++ b/contracts/governance/extensions/GovernorCountingFractional.sol @@ -109,7 +109,7 @@ abstract contract GovernorCountingFractional is Governor { uint8 support, uint256 totalWeight, bytes memory params - ) internal virtual override { + ) internal virtual override returns (uint256) { // Compute number of remaining votes. Returns 0 on overflow. (, uint256 remainingWeight) = totalWeight.trySub(voteWeightCast(proposalId, account)); if (remainingWeight == 0) { @@ -117,9 +117,9 @@ abstract contract GovernorCountingFractional is Governor { } if (params.length == 0) { - _countVoteNominal(proposalId, account, support, remainingWeight); + return _countVoteNominal(proposalId, account, support, remainingWeight); } else if (params.length == 0x30) { - _countVoteFractional(proposalId, account, params, remainingWeight); + return _countVoteFractional(proposalId, account, params, remainingWeight); } else { revert GovernorInvalidParamsFormat(account); } @@ -131,7 +131,12 @@ abstract contract GovernorCountingFractional is Governor { * Because this function votes with the delegate's remaining weight, it can only be called once per proposal and * thus does not require any replay protection. */ - function _countVoteNominal(uint256 proposalId, address account, uint8 support, uint256 weight) private { + function _countVoteNominal( + uint256 proposalId, + address account, + uint8 support, + uint256 weight + ) private returns (uint256) { ProposalVote storage details = _proposalVotes[proposalId]; details.usedVotes[account] += weight; @@ -144,6 +149,8 @@ abstract contract GovernorCountingFractional is Governor { } else { revert GovernorInvalidVoteType(); } + + return weight; } /** @@ -162,7 +169,12 @@ abstract contract GovernorCountingFractional is Governor { * ABSTAIN on the proposal. Though partial, votes are still final once cast and cannot be changed or overridden. * Subsequent partial votes simply increment existing totals. */ - function _countVoteFractional(uint256 proposalId, address account, bytes memory params, uint256 weight) private { + function _countVoteFractional( + uint256 proposalId, + address account, + bytes memory params, + uint256 weight + ) private returns (uint256) { uint128 againstVotes = _extractUint128(params, 0); uint128 forVotes = _extractUint128(params, 1); uint128 abstainVotes = _extractUint128(params, 2); @@ -177,6 +189,8 @@ abstract contract GovernorCountingFractional is Governor { details.forVotes += forVotes; details.abstainVotes += abstainVotes; details.usedVotes[account] += usedWeight; + + return usedWeight; } function _extractUint128(bytes memory data, uint256 pos) private pure returns (uint128 result) { diff --git a/contracts/governance/extensions/GovernorCountingSimple.sol b/contracts/governance/extensions/GovernorCountingSimple.sol index ac9c22aab07..def29e34e61 100644 --- a/contracts/governance/extensions/GovernorCountingSimple.sol +++ b/contracts/governance/extensions/GovernorCountingSimple.sol @@ -77,9 +77,9 @@ abstract contract GovernorCountingSimple is Governor { uint256 proposalId, address account, uint8 support, - uint256 weight, + uint256 totalWeight, bytes memory // params - ) internal virtual override { + ) internal virtual override returns (uint256) { ProposalVote storage proposalVote = _proposalVotes[proposalId]; if (proposalVote.hasVoted[account]) { @@ -88,13 +88,15 @@ abstract contract GovernorCountingSimple is Governor { proposalVote.hasVoted[account] = true; if (support == uint8(VoteType.Against)) { - proposalVote.againstVotes += weight; + proposalVote.againstVotes += totalWeight; } else if (support == uint8(VoteType.For)) { - proposalVote.forVotes += weight; + proposalVote.forVotes += totalWeight; } else if (support == uint8(VoteType.Abstain)) { - proposalVote.abstainVotes += weight; + proposalVote.abstainVotes += totalWeight; } else { revert GovernorInvalidVoteType(); } + + return totalWeight; } } diff --git a/contracts/mocks/governance/GovernorWithParamsMock.sol b/contracts/mocks/governance/GovernorWithParamsMock.sol index d535f811ca6..29b738e9d6b 100644 --- a/contracts/mocks/governance/GovernorWithParamsMock.sol +++ b/contracts/mocks/governance/GovernorWithParamsMock.sol @@ -41,7 +41,7 @@ abstract contract GovernorWithParamsMock is GovernorVotes, GovernorCountingSimpl uint8 support, uint256 weight, bytes memory params - ) internal override(Governor, GovernorCountingSimple) { + ) internal override(Governor, GovernorCountingSimple) returns (uint256) { if (params.length > 0) { (uint256 _uintParam, string memory _strParam) = abi.decode(params, (uint256, string)); emit CountParams(_uintParam, _strParam); diff --git a/test/governance/extensions/GovernorCountingFractional.test.js b/test/governance/extensions/GovernorCountingFractional.test.js index 36fc6b12c0d..43893c8c3dd 100644 --- a/test/governance/extensions/GovernorCountingFractional.test.js +++ b/test/governance/extensions/GovernorCountingFractional.test.js @@ -115,14 +115,7 @@ describe('GovernorCountingFractional', function () { }), ) .to.emit(this.mock, 'VoteCastWithParams') - .withArgs( - this.voter2, - this.proposal.id, - 0, - ethers.parseEther('7'), // total weight for voter2 - 'no particular reason', - params, - ); + .withArgs(this.voter2, this.proposal.id, 0, sum(...votes), 'no particular reason', params); } expect(await this.mock.proposalVotes(this.proposal.id)).to.deep.equal(zip(...steps).map(v => sum(...v))); @@ -150,24 +143,11 @@ describe('GovernorCountingFractional', function () { }), ) .to.emit(this.mock, 'VoteCastWithParams') - .withArgs( - this.voter2, - this.proposal.id, - 0, - weight, // total weight for voter2 - 'no particular reason', - params, - ); + .withArgs(this.voter2, this.proposal.id, 0, sum(...fractional), 'no particular reason', params); await expect(this.helper.connect(this.voter2).vote({ support: VoteType.Against })) .to.emit(this.mock, 'VoteCast') - .withArgs( - this.voter2, - this.proposal.id, - VoteType.Against, - weight, // total weight for voter2 - '', - ); + .withArgs(this.voter2, this.proposal.id, VoteType.Against, weight - sum(...fractional), ''); expect(await this.mock.proposalVotes(this.proposal.id)).to.deep.equal([ weight - sum(...fractional.slice(1)), From bc9f9700c1bfa7fc3e7e76d25151605926ba9564 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 15 May 2024 18:48:58 +0200 Subject: [PATCH 07/26] fix fuzzing tests --- test/governance/Governor.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/governance/Governor.t.sol b/test/governance/Governor.t.sol index e3c24ecc616..958461abb95 100644 --- a/test/governance/Governor.t.sol +++ b/test/governance/Governor.t.sol @@ -51,5 +51,5 @@ contract GovernorInternalTest is Test, Governor { function _getVotes(address, uint256, bytes memory) internal pure virtual override returns (uint256) {} - function _countVote(uint256, address, uint8, uint256, bytes memory) internal virtual override {} + function _countVote(uint256, address, uint8, uint256, bytes memory) internal virtual override returns (uint256) {} } From 93f460f8854bbfcdd0d1150c786b61ce7646330c Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 16 May 2024 09:08:08 +0200 Subject: [PATCH 08/26] address PR comments --- contracts/governance/IGovernor.sol | 5 +++++ .../extensions/GovernorCountingFractional.sol | 16 +++++++++------- .../GovernorCountingFractional.test.js | 6 ++---- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/contracts/governance/IGovernor.sol b/contracts/governance/IGovernor.sol index 63589612e2f..f84965bc769 100644 --- a/contracts/governance/IGovernor.sol +++ b/contracts/governance/IGovernor.sol @@ -83,6 +83,11 @@ interface IGovernor is IERC165, IERC6372 { */ error GovernorInvalidVoteType(); + /** + * @dev The provided params buffer is not supported by the counting module. + */ + error GovernorInvalidVoteParams(); + /** * @dev Queue operation is not implemented for this governor. Execute should be called directly. */ diff --git a/contracts/governance/extensions/GovernorCountingFractional.sol b/contracts/governance/extensions/GovernorCountingFractional.sol index a510623a61a..9feb6b4c992 100644 --- a/contracts/governance/extensions/GovernorCountingFractional.sol +++ b/contracts/governance/extensions/GovernorCountingFractional.sol @@ -31,8 +31,10 @@ abstract contract GovernorCountingFractional is Governor { */ mapping(uint256 => ProposalVote) private _proposalVotes; - error GovernorInvalidParamsFormat(address voter); - error GovernorUsedVotesExceedRemainingWeight(address voter, uint256 usedVotes, uint256 remainingWeight); + /** + * @dev A fractional vote params uses more votes than are available for that user. + */ + error GovernorExceedRemainingWeight(address voter, uint256 usedVotes, uint256 remainingWeight); /** * @dev See {IGovernor-COUNTING_MODE}. @@ -88,9 +90,9 @@ abstract contract GovernorCountingFractional is Governor { * * @dev Function that records the delegate's votes. * - * If the `params` bytes parameter is empty, then this module behaves identically to {GovernorCountingSimple}. - * That is, it assigns the remaining weight of the delegate to the `support` parameter, which follows the - * `VoteType` enum from Governor Bravo (as defined in {GovernorCountingSimple}). + * If the `params` bytes parameter is empty, then this module behaves identically to {GovernorCountingSimple} for + * the remaining weight. That is, it assigns the remaining weight of the delegate to the `support` parameter, + * which follows the `VoteType` enum from Governor Bravo (as defined in {GovernorCountingSimple}). * * If the `params` bytes parameter is not zero, then it _must_ be tree packed uint128s, totaling 48 bytes, * representing the weight the delegate assigns to Against, For, and Abstain respectively. This format can be @@ -121,7 +123,7 @@ abstract contract GovernorCountingFractional is Governor { } else if (params.length == 0x30) { return _countVoteFractional(proposalId, account, params, remainingWeight); } else { - revert GovernorInvalidParamsFormat(account); + revert GovernorInvalidVoteParams(); } } @@ -181,7 +183,7 @@ abstract contract GovernorCountingFractional is Governor { uint256 usedWeight = againstVotes + forVotes + abstainVotes; if (usedWeight > weight) { - revert GovernorUsedVotesExceedRemainingWeight(account, usedWeight, weight); + revert GovernorExceedRemainingWeight(account, usedWeight, weight); } ProposalVote storage details = _proposalVotes[proposalId]; diff --git a/test/governance/extensions/GovernorCountingFractional.test.js b/test/governance/extensions/GovernorCountingFractional.test.js index 43893c8c3dd..e60353f8c82 100644 --- a/test/governance/extensions/GovernorCountingFractional.test.js +++ b/test/governance/extensions/GovernorCountingFractional.test.js @@ -171,7 +171,7 @@ describe('GovernorCountingFractional', function () { params: ethers.solidityPacked(['uint128', 'uint128', 'uint128'], fractional), }), ) - .to.be.revertedWithCustomError(this.mock, 'GovernorUsedVotesExceedRemainingWeight') + .to.be.revertedWithCustomError(this.mock, 'GovernorExceedRemainingWeight') .withArgs(this.voter2, sum(...fractional), weight); }); @@ -201,9 +201,7 @@ describe('GovernorCountingFractional', function () { reason: 'no particular reason', params: ethers.solidityPacked(['uint128', 'uint128'], [0n, 1n]), }), - ) - .to.be.revertedWithCustomError(this.mock, 'GovernorInvalidParamsFormat') - .withArgs(this.voter2); + ).to.be.revertedWithCustomError(this.mock, 'GovernorInvalidVoteParams'); }); it('revert if vote type is invalid', async function () { From e792599d23cb6019d374161654cee7a49ad6ec7a Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 16 May 2024 17:44:56 +0200 Subject: [PATCH 09/26] renaming --- .../extensions/GovernorCountingFractional.sol | 18 ++++++++++++------ .../GovernorCountingFractional.test.js | 8 ++++---- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/contracts/governance/extensions/GovernorCountingFractional.sol b/contracts/governance/extensions/GovernorCountingFractional.sol index 9feb6b4c992..0b704c74ba7 100644 --- a/contracts/governance/extensions/GovernorCountingFractional.sol +++ b/contracts/governance/extensions/GovernorCountingFractional.sol @@ -48,14 +48,14 @@ abstract contract GovernorCountingFractional is Governor { * @dev See {IGovernor-hasVoted}. */ function hasVoted(uint256 proposalId, address account) public view virtual override returns (bool) { - return voteWeightCast(proposalId, account) > 0; + return usedVotes(proposalId, account) > 0; } /** * @dev Get the number of votes cast thus far on proposal `proposalId` by account `account`. Useful for * integrations that allow delegates to cast rolling, partial votes. */ - function voteWeightCast(uint256 proposalId, address account) public view virtual returns (uint256) { + function usedVotes(uint256 proposalId, address account) public view virtual returns (uint256) { return _proposalVotes[proposalId].usedVotes[account]; } @@ -113,7 +113,7 @@ abstract contract GovernorCountingFractional is Governor { bytes memory params ) internal virtual override returns (uint256) { // Compute number of remaining votes. Returns 0 on overflow. - (, uint256 remainingWeight) = totalWeight.trySub(voteWeightCast(proposalId, account)); + (, uint256 remainingWeight) = totalWeight.trySub(usedVotes(proposalId, account)); if (remainingWeight == 0) { revert GovernorAlreadyCastVote(account); } @@ -187,9 +187,15 @@ abstract contract GovernorCountingFractional is Governor { } ProposalVote storage details = _proposalVotes[proposalId]; - details.againstVotes += againstVotes; - details.forVotes += forVotes; - details.abstainVotes += abstainVotes; + if (againstVotes > 0) { + details.againstVotes += againstVotes; + } + if (forVotes > 0) { + details.forVotes += forVotes; + } + if (abstainVotes > 0) { + details.abstainVotes += abstainVotes; + } details.usedVotes[account] += usedWeight; return usedWeight; diff --git a/test/governance/extensions/GovernorCountingFractional.test.js b/test/governance/extensions/GovernorCountingFractional.test.js index e60353f8c82..9518460ce8c 100644 --- a/test/governance/extensions/GovernorCountingFractional.test.js +++ b/test/governance/extensions/GovernorCountingFractional.test.js @@ -98,7 +98,7 @@ describe('GovernorCountingFractional', function () { expect(await this.mock.proposalVotes(this.proposal.id)).to.deep.equal([0n, 0n, 0n]); expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.equal(false); - expect(await this.mock.voteWeightCast(this.proposal.id, this.voter2)).to.equal(0n); + expect(await this.mock.usedVotes(this.proposal.id, this.voter2)).to.equal(0n); const steps = [ ['0', '2', '1'], @@ -120,7 +120,7 @@ describe('GovernorCountingFractional', function () { expect(await this.mock.proposalVotes(this.proposal.id)).to.deep.equal(zip(...steps).map(v => sum(...v))); expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.equal(true); - expect(await this.mock.voteWeightCast(this.proposal.id, this.voter2)).to.equal(sum(...[].concat(...steps))); + expect(await this.mock.usedVotes(this.proposal.id, this.voter2)).to.equal(sum(...[].concat(...steps))); }); it('fractional then nominal', async function () { @@ -129,7 +129,7 @@ describe('GovernorCountingFractional', function () { expect(await this.mock.proposalVotes(this.proposal.id)).to.deep.equal([0n, 0n, 0n]); expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.equal(false); - expect(await this.mock.voteWeightCast(this.proposal.id, this.voter2)).to.equal(0n); + expect(await this.mock.usedVotes(this.proposal.id, this.voter2)).to.equal(0n); const weight = ethers.parseEther('7'); const fractional = ['1', '2', '1'].map(ethers.parseEther); @@ -154,7 +154,7 @@ describe('GovernorCountingFractional', function () { ...fractional.slice(1), ]); expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.equal(true); - expect(await this.mock.voteWeightCast(this.proposal.id, this.voter2)).to.equal(weight); + expect(await this.mock.usedVotes(this.proposal.id, this.voter2)).to.equal(weight); }); it('revert if params spend more than available', async function () { From 5ae1d9e794b8083e4a2bf70d36a6ebd500fb3932 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Fri, 24 May 2024 15:55:45 -0600 Subject: [PATCH 10/26] Nits --- .changeset/eight-eyes-burn.md | 2 +- .../extensions/GovernorCountingFractional.sol | 43 ++++++++++++------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/.changeset/eight-eyes-burn.md b/.changeset/eight-eyes-burn.md index b72d5437319..908c90c7bbf 100644 --- a/.changeset/eight-eyes-burn.md +++ b/.changeset/eight-eyes-burn.md @@ -2,4 +2,4 @@ 'openzeppelin-solidity': minor --- -`GovernorCountingFlexible`: Add a governor counting module that support more flexible voting (voting with part of the weight, splitting the votes) +`GovernorCountingFractional`: Add a governor counting module that allows distributing voting power amongst 3 options (For, Against, Abstain). diff --git a/contracts/governance/extensions/GovernorCountingFractional.sol b/contracts/governance/extensions/GovernorCountingFractional.sol index 0b704c74ba7..c01e4cd8ee2 100644 --- a/contracts/governance/extensions/GovernorCountingFractional.sol +++ b/contracts/governance/extensions/GovernorCountingFractional.sol @@ -7,12 +7,17 @@ import {GovernorCountingSimple} from "./GovernorCountingSimple.sol"; import {Math} from "../../utils/math/Math.sol"; /** - * @notice Extension of {Governor} for 3 option fractional vote counting. When voting, a delegate may split their vote - * weight between Against/For/Abstain. This is most useful when the delegate is itself a contract, implementing its - * own rules for voting. By allowing a contract-delegate to split its vote weight, the voting preferences of many - * disparate token holders can be rolled up into a single vote to the Governor itself. Some example use cases include - * voting with tokens that are held by a DeFi pool, voting from L2 with tokens held by a bridge, or voting privately - * from a shielded pool using zero knowledge proofs. + * @dev Extension of {Governor} for fractional 3 option vote counting. + * + * Voters can split their voting power amongst 3 options: For, Against and Abstain. + * This is mostly useful when the delegate is a contract that implements its own rules for voting. These delegate-contracts + * can cast fractional votes according to the preferences of multiple entities delegating their voting power. + * + * Some example use cases include: + * + * * Voting from tokens that are held by a DeFi pool + * * Voting from an L2 with tokens held by a bridge + * * voting privately from a shielded pool using zero knowledge proofs. * * Based on ScopeLift's https://github.com/ScopeLift/flexible-voting/blob/master/src/GovernorCountingFractional.sol */ @@ -52,7 +57,7 @@ abstract contract GovernorCountingFractional is Governor { } /** - * @dev Get the number of votes cast thus far on proposal `proposalId` by account `account`. Useful for + * @dev Get the number of votes already cast by `account` for a proposal with `proposalId`. Useful for * integrations that allow delegates to cast rolling, partial votes. */ function usedVotes(uint256 proposalId, address account) public view virtual returns (uint256) { @@ -60,7 +65,7 @@ abstract contract GovernorCountingFractional is Governor { } /** - * @dev Accessor to the internal vote counts. + * @dev Get current distribution of votes for a given proposal. */ function proposalVotes( uint256 proposalId @@ -103,6 +108,10 @@ abstract contract GovernorCountingFractional is Governor { * The sum total of the three decoded vote weights _must_ be less than or equal to the delegate's remaining weight * on the proposal, i.e. their checkpointed total weight minus votes already cast on the proposal. * + * NOTE: Consider the number of votes is restricted to 128 bits. Depending on how many decimals the underlying token + * has, a single voter may require to split their vote into multiple transactions. For precision higher than + * ~30 decimals, large token holders may require an exponentially large number of transactions to cast their vote. + * * See `_countVoteNominal` and `_countVoteFractional` for more details. */ function _countVote( @@ -158,17 +167,19 @@ abstract contract GovernorCountingFractional is Governor { /** * @dev Count votes with fractional weight. * - * `params` is expected to be tree packed uint128s: + * The `params` argument is expected to be three packed `uint128`: * `abi.encodePacked(uint128(againstVotes), uint128(forVotes), uint128(abstainVotes))` * - * This function can be called multiple times for the same account and proposal, i.e. partial/rolling votes are - * allowed. For example, an account with total weight of 10 could call this function three times with the + * This function can be called multiple times for the same account and proposal (i.e. partial/rolling votes are + * allowed). For example, an account with total weight of 10 could call this function three times with the * following vote data: - * - against: 1, for: 0, abstain: 2 - * - against: 3, for: 1, abstain: 0 - * - against: 1, for: 1, abstain: 1 - * The result of these three calls would be that the account casts 5 votes AGAINST, 2 votes FOR, and 3 votes - * ABSTAIN on the proposal. Though partial, votes are still final once cast and cannot be changed or overridden. + * + * * Against: 1, For: 0, Abstain: 2 + * * Against: 3, For: 1, Abstain: 0 + * * Against: 1, For: 1, Abstain: 1 + * + * Casting votes like this will make the calling account to cast a total of 5 `Against` votes, 2 `For` votes + * and 3 `Abstain` votes. Though partial, votes are still final once cast and cannot be changed or overridden. * Subsequent partial votes simply increment existing totals. */ function _countVoteFractional( From 2bf4d2d8f6f741975ce94548cdc3d6fb2a2767cd Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 27 May 2024 10:51:32 +0200 Subject: [PATCH 11/26] Update contracts/governance/extensions/GovernorCountingFractional.sol --- contracts/governance/extensions/GovernorCountingFractional.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/governance/extensions/GovernorCountingFractional.sol b/contracts/governance/extensions/GovernorCountingFractional.sol index c01e4cd8ee2..3882ce44ea9 100644 --- a/contracts/governance/extensions/GovernorCountingFractional.sol +++ b/contracts/governance/extensions/GovernorCountingFractional.sol @@ -19,7 +19,7 @@ import {Math} from "../../utils/math/Math.sol"; * * Voting from an L2 with tokens held by a bridge * * voting privately from a shielded pool using zero knowledge proofs. * - * Based on ScopeLift's https://github.com/ScopeLift/flexible-voting/blob/master/src/GovernorCountingFractional.sol + * Based on ScopeLift's https://github.com/ScopeLift/flexible-voting/blob/e5de2efd1368387b840931f19f3c184c85842761/src/GovernorCountingFractional.sol */ abstract contract GovernorCountingFractional is Governor { using Math for *; From f9db0440793da2c9efc8ccde124e39f0b6b0b029 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 27 May 2024 10:57:39 +0200 Subject: [PATCH 12/26] Update contracts/governance/extensions/GovernorCountingFractional.sol --- .../governance/extensions/GovernorCountingFractional.sol | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/contracts/governance/extensions/GovernorCountingFractional.sol b/contracts/governance/extensions/GovernorCountingFractional.sol index 3882ce44ea9..e9f8c850413 100644 --- a/contracts/governance/extensions/GovernorCountingFractional.sol +++ b/contracts/governance/extensions/GovernorCountingFractional.sol @@ -91,9 +91,7 @@ abstract contract GovernorCountingFractional is Governor { } /** - * @notice See {Governor-_countVote}. - * - * @dev Function that records the delegate's votes. + * @dev See {Governor-_countVote}. Function that records the delegate's votes. * * If the `params` bytes parameter is empty, then this module behaves identically to {GovernorCountingSimple} for * the remaining weight. That is, it assigns the remaining weight of the delegate to the `support` parameter, From 7ffc6d2b3f3dcf27cafca021c080ee81055f50db Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 27 May 2024 09:11:33 -0600 Subject: [PATCH 13/26] Add breaking change note --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b607313585e..2c2ae293eb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### Breaking changes - `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. +- `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 will get a compilation error that can be fixed by adding a return statement to the `_countVotes` function. ### Custom error changes From e4d4fad66675bcaec989ad943191a1a9f58e2d5c Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 27 May 2024 18:04:02 +0200 Subject: [PATCH 14/26] add Fractional vote type (support) --- .../extensions/GovernorCountingFractional.sol | 11 ++--- .../extensions/GovernorCountingSimple.sol | 3 +- .../GovernorCountingFractional.test.js | 43 +++++++++++++++---- test/helpers/enums.js | 2 +- 4 files changed, 44 insertions(+), 15 deletions(-) diff --git a/contracts/governance/extensions/GovernorCountingFractional.sol b/contracts/governance/extensions/GovernorCountingFractional.sol index e9f8c850413..440d53f156a 100644 --- a/contracts/governance/extensions/GovernorCountingFractional.sol +++ b/contracts/governance/extensions/GovernorCountingFractional.sol @@ -125,12 +125,15 @@ abstract contract GovernorCountingFractional is Governor { revert GovernorAlreadyCastVote(account); } - if (params.length == 0) { + // For clarity of event indexing, fractional voting must be clearly advertised in the "support" field. + if (support <= uint8(GovernorCountingSimple.VoteType.Abstain)) { + if (params.length != 0) revert GovernorInvalidVoteParams(); return _countVoteNominal(proposalId, account, support, remainingWeight); - } else if (params.length == 0x30) { + } else if (support == uint8(GovernorCountingSimple.VoteType.Fractional)) { + if (params.length != 0x30) revert GovernorInvalidVoteParams(); return _countVoteFractional(proposalId, account, params, remainingWeight); } else { - revert GovernorInvalidVoteParams(); + revert GovernorInvalidVoteType(); } } @@ -155,8 +158,6 @@ abstract contract GovernorCountingFractional is Governor { details.forVotes += weight; } else if (support == uint8(GovernorCountingSimple.VoteType.Abstain)) { details.abstainVotes += weight; - } else { - revert GovernorInvalidVoteType(); } return weight; diff --git a/contracts/governance/extensions/GovernorCountingSimple.sol b/contracts/governance/extensions/GovernorCountingSimple.sol index def29e34e61..c19ea474a56 100644 --- a/contracts/governance/extensions/GovernorCountingSimple.sol +++ b/contracts/governance/extensions/GovernorCountingSimple.sol @@ -15,7 +15,8 @@ abstract contract GovernorCountingSimple is Governor { enum VoteType { Against, For, - Abstain + Abstain, + Fractional } struct ProposalVote { diff --git a/test/governance/extensions/GovernorCountingFractional.test.js b/test/governance/extensions/GovernorCountingFractional.test.js index 9518460ce8c..0d5b8ac25ac 100644 --- a/test/governance/extensions/GovernorCountingFractional.test.js +++ b/test/governance/extensions/GovernorCountingFractional.test.js @@ -109,13 +109,20 @@ describe('GovernorCountingFractional', function () { const params = ethers.solidityPacked(['uint128', 'uint128', 'uint128'], votes); await expect( this.helper.connect(this.voter2).vote({ - support: 0, + support: VoteType.Fractional, reason: 'no particular reason', params, }), ) .to.emit(this.mock, 'VoteCastWithParams') - .withArgs(this.voter2, this.proposal.id, 0, sum(...votes), 'no particular reason', params); + .withArgs( + this.voter2, + this.proposal.id, + VoteType.Fractional, + sum(...votes), + 'no particular reason', + params, + ); } expect(await this.mock.proposalVotes(this.proposal.id)).to.deep.equal(zip(...steps).map(v => sum(...v))); @@ -137,13 +144,20 @@ describe('GovernorCountingFractional', function () { const params = ethers.solidityPacked(['uint128', 'uint128', 'uint128'], fractional); await expect( this.helper.connect(this.voter2).vote({ - support: 0, + support: VoteType.Fractional, reason: 'no particular reason', params, }), ) .to.emit(this.mock, 'VoteCastWithParams') - .withArgs(this.voter2, this.proposal.id, 0, sum(...fractional), 'no particular reason', params); + .withArgs( + this.voter2, + this.proposal.id, + VoteType.Fractional, + sum(...fractional), + 'no particular reason', + params, + ); await expect(this.helper.connect(this.voter2).vote({ support: VoteType.Against })) .to.emit(this.mock, 'VoteCast') @@ -166,7 +180,7 @@ describe('GovernorCountingFractional', function () { await expect( this.helper.connect(this.voter2).vote({ - support: 0, + support: VoteType.Fractional, reason: 'no particular reason', params: ethers.solidityPacked(['uint128', 'uint128', 'uint128'], fractional), }), @@ -182,7 +196,7 @@ describe('GovernorCountingFractional', function () { await expect( this.helper.connect(this.voter2).vote({ - support: 0, + support: VoteType.Fractional, reason: 'no particular reason', params: ethers.solidityPacked(['uint128', 'uint128', 'uint128'], [0n, 1n, 0n]), }), @@ -191,19 +205,32 @@ describe('GovernorCountingFractional', function () { .withArgs(this.voter2); }); - it('revert if params are not properly formated', async function () { + it('revert if params are not properly formated #1', async function () { await this.helper.connect(this.proposer).propose(); await this.helper.waitForSnapshot(); await expect( this.helper.connect(this.voter2).vote({ - support: 0, + support: VoteType.Fractional, reason: 'no particular reason', params: ethers.solidityPacked(['uint128', 'uint128'], [0n, 1n]), }), ).to.be.revertedWithCustomError(this.mock, 'GovernorInvalidVoteParams'); }); + it('revert if params are not properly formated #2', async function () { + await this.helper.connect(this.proposer).propose(); + await this.helper.waitForSnapshot(); + + await expect( + this.helper.connect(this.voter2).vote({ + support: VoteType.Against, + reason: 'no particular reason', + params: ethers.solidityPacked(['uint128', 'uint128', 'uint128'], [0n, 1n, 0n]), + }), + ).to.be.revertedWithCustomError(this.mock, 'GovernorInvalidVoteParams'); + }); + it('revert if vote type is invalid', async function () { await this.helper.connect(this.proposer).propose(); await this.helper.waitForSnapshot(); diff --git a/test/helpers/enums.js b/test/helpers/enums.js index bb237796adb..4eb872215fb 100644 --- a/test/helpers/enums.js +++ b/test/helpers/enums.js @@ -5,7 +5,7 @@ function Enum(...options) { module.exports = { Enum, ProposalState: Enum('Pending', 'Active', 'Canceled', 'Defeated', 'Succeeded', 'Queued', 'Expired', 'Executed'), - VoteType: Enum('Against', 'For', 'Abstain'), + VoteType: Enum('Against', 'For', 'Abstain', 'Fractional'), Rounding: Enum('Floor', 'Ceil', 'Trunc', 'Expand'), OperationState: Enum('Unset', 'Waiting', 'Ready', 'Done'), RevertType: Enum('None', 'RevertWithoutMessage', 'RevertWithMessage', 'RevertWithCustomError', 'Panic'), From 19c8142c9cfda4660302ee74e04fb189e73c4d63 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 27 May 2024 18:05:54 +0200 Subject: [PATCH 15/26] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c2ae293eb6..3babbc331bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ### Breaking changes - `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. -- `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 will get a compilation error that can be fixed by adding a return statement to the `_countVotes` function. +- `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. ### Custom error changes From 0420d9a36ebdf9bc2c98030f9eaaa9777983cd04 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 27 May 2024 12:45:07 -0600 Subject: [PATCH 16/26] Nits --- .../extensions/GovernorCountingFractional.sol | 25 +++++++++++++------ .../GovernorCountingFractional.test.js | 4 +-- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/contracts/governance/extensions/GovernorCountingFractional.sol b/contracts/governance/extensions/GovernorCountingFractional.sol index 440d53f156a..cd359e90d13 100644 --- a/contracts/governance/extensions/GovernorCountingFractional.sol +++ b/contracts/governance/extensions/GovernorCountingFractional.sol @@ -7,9 +7,16 @@ import {GovernorCountingSimple} from "./GovernorCountingSimple.sol"; import {Math} from "../../utils/math/Math.sol"; /** - * @dev Extension of {Governor} for fractional 3 option vote counting. + * @dev Extension of {Governor} for fractional voting. + * + * Similar to {GovernorCountingSimple}, this contract is a votes counting module for {Governor} that supports 3 options: + * Against, For, Abstain. Additionally, it includes a fourth option: Fractional, which allows voters to split their voting + * power amongst the other 3 options. + * + * Votes cast with the Fractional support must be accompanied by a `params` argument that is tree packed `uint128` values + * representing the weight the delegate assigns to Against, For, and Abstain respectively. For those votes cast for the other + * 3 options, the `params` argument must be empty. * - * Voters can split their voting power amongst 3 options: For, Against and Abstain. * This is mostly useful when the delegate is a contract that implements its own rules for voting. These delegate-contracts * can cast fractional votes according to the preferences of multiple entities delegating their voting power. * @@ -93,13 +100,15 @@ abstract contract GovernorCountingFractional is Governor { /** * @dev See {Governor-_countVote}. Function that records the delegate's votes. * - * If the `params` bytes parameter is empty, then this module behaves identically to {GovernorCountingSimple} for - * the remaining weight. That is, it assigns the remaining weight of the delegate to the `support` parameter, - * which follows the `VoteType` enum from Governor Bravo (as defined in {GovernorCountingSimple}). + * Executing this function consume's the delegate's weight on the proposal. This weight can be distributed amongst + * the 3 options (Against, For, Abstain) by specifying a Fractional `support`. + * + * When support is anything other than Fractiona`, the `params` argument must be empty and the delegate's full remaining + * weight is cast for the specified `support` option, as in {GovernorCountingSimple} and following the `VoteType` + * enum from Governor Bravo. * - * If the `params` bytes parameter is not zero, then it _must_ be tree packed uint128s, totaling 48 bytes, - * representing the weight the delegate assigns to Against, For, and Abstain respectively. This format can be - * produced using: + * Given a Fractional `support`, the `params` argument must be tree packed `uint128` values representing the weight + * the delegate assigns to Against, For, and Abstain respectively. This format can be produced using: * * `abi.encodePacked(uint128(againstVotes), uint128(forVotes), uint128(abstainVotes))` * diff --git a/test/governance/extensions/GovernorCountingFractional.test.js b/test/governance/extensions/GovernorCountingFractional.test.js index 0d5b8ac25ac..903b950b12f 100644 --- a/test/governance/extensions/GovernorCountingFractional.test.js +++ b/test/governance/extensions/GovernorCountingFractional.test.js @@ -205,7 +205,7 @@ describe('GovernorCountingFractional', function () { .withArgs(this.voter2); }); - it('revert if params are not properly formated #1', async function () { + it('revert if params are not properly formatted #1', async function () { await this.helper.connect(this.proposer).propose(); await this.helper.waitForSnapshot(); @@ -218,7 +218,7 @@ describe('GovernorCountingFractional', function () { ).to.be.revertedWithCustomError(this.mock, 'GovernorInvalidVoteParams'); }); - it('revert if params are not properly formated #2', async function () { + it('revert if params are not properly formatted #2', async function () { await this.helper.connect(this.proposer).propose(); await this.helper.waitForSnapshot(); From 8e95e965024894ea0f9f0abe9d4973260f37feb7 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 27 May 2024 12:56:29 -0600 Subject: [PATCH 17/26] up --- .../governance/extensions/GovernorCountingFractional.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/governance/extensions/GovernorCountingFractional.sol b/contracts/governance/extensions/GovernorCountingFractional.sol index cd359e90d13..0b0e364d404 100644 --- a/contracts/governance/extensions/GovernorCountingFractional.sol +++ b/contracts/governance/extensions/GovernorCountingFractional.sol @@ -24,9 +24,9 @@ import {Math} from "../../utils/math/Math.sol"; * * * Voting from tokens that are held by a DeFi pool * * Voting from an L2 with tokens held by a bridge - * * voting privately from a shielded pool using zero knowledge proofs. + * * Voting privately from a shielded pool using zero knowledge proofs. * - * Based on ScopeLift's https://github.com/ScopeLift/flexible-voting/blob/e5de2efd1368387b840931f19f3c184c85842761/src/GovernorCountingFractional.sol + * Based on ScopeLift's GovernorCountingFractional[https://github.com/ScopeLift/flexible-voting/blob/e5de2efd1368387b840931f19f3c184c85842761/src/GovernorCountingFractional.sol] */ abstract contract GovernorCountingFractional is Governor { using Math for *; From 95f3fb26236b6050cf0bddcf9db8abdca32c962b Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 27 May 2024 21:20:33 +0200 Subject: [PATCH 18/26] Update GovernorCountingFractional.sol --- contracts/governance/extensions/GovernorCountingFractional.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/governance/extensions/GovernorCountingFractional.sol b/contracts/governance/extensions/GovernorCountingFractional.sol index 0b0e364d404..3b7f6f8ee69 100644 --- a/contracts/governance/extensions/GovernorCountingFractional.sol +++ b/contracts/governance/extensions/GovernorCountingFractional.sol @@ -103,7 +103,7 @@ abstract contract GovernorCountingFractional is Governor { * Executing this function consume's the delegate's weight on the proposal. This weight can be distributed amongst * the 3 options (Against, For, Abstain) by specifying a Fractional `support`. * - * When support is anything other than Fractiona`, the `params` argument must be empty and the delegate's full remaining + * When support is anything other than Fractional, the `params` argument must be empty and the delegate's full remaining * weight is cast for the specified `support` option, as in {GovernorCountingSimple} and following the `VoteType` * enum from Governor Bravo. * From 68033f5bc3788f256f5987e92bc5dc750d30a91b Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 28 May 2024 10:26:12 +0200 Subject: [PATCH 19/26] use support = 255 for parameter driven voting --- .../extensions/GovernorCountingFractional.sol | 6 +++++- .../extensions/GovernorCountingSimple.sol | 3 +-- .../extensions/GovernorCountingFractional.test.js | 14 +++++++------- test/helpers/enums.js | 2 +- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/contracts/governance/extensions/GovernorCountingFractional.sol b/contracts/governance/extensions/GovernorCountingFractional.sol index 3b7f6f8ee69..207a53da551 100644 --- a/contracts/governance/extensions/GovernorCountingFractional.sol +++ b/contracts/governance/extensions/GovernorCountingFractional.sol @@ -135,10 +135,14 @@ abstract contract GovernorCountingFractional is Governor { } // For clarity of event indexing, fractional voting must be clearly advertised in the "support" field. + // + // Supported `support` value must be: + // - "Full" voting: `support = 0` (Against), `1` (For) or `2` (Abstain), with empty params. + // - "Fractional" voting: `support = 255`, with 48 bytes params. if (support <= uint8(GovernorCountingSimple.VoteType.Abstain)) { if (params.length != 0) revert GovernorInvalidVoteParams(); return _countVoteNominal(proposalId, account, support, remainingWeight); - } else if (support == uint8(GovernorCountingSimple.VoteType.Fractional)) { + } else if (support == type(uint8).max) { if (params.length != 0x30) revert GovernorInvalidVoteParams(); return _countVoteFractional(proposalId, account, params, remainingWeight); } else { diff --git a/contracts/governance/extensions/GovernorCountingSimple.sol b/contracts/governance/extensions/GovernorCountingSimple.sol index c19ea474a56..def29e34e61 100644 --- a/contracts/governance/extensions/GovernorCountingSimple.sol +++ b/contracts/governance/extensions/GovernorCountingSimple.sol @@ -15,8 +15,7 @@ abstract contract GovernorCountingSimple is Governor { enum VoteType { Against, For, - Abstain, - Fractional + Abstain } struct ProposalVote { diff --git a/test/governance/extensions/GovernorCountingFractional.test.js b/test/governance/extensions/GovernorCountingFractional.test.js index 903b950b12f..e7fe39d090c 100644 --- a/test/governance/extensions/GovernorCountingFractional.test.js +++ b/test/governance/extensions/GovernorCountingFractional.test.js @@ -109,7 +109,7 @@ describe('GovernorCountingFractional', function () { const params = ethers.solidityPacked(['uint128', 'uint128', 'uint128'], votes); await expect( this.helper.connect(this.voter2).vote({ - support: VoteType.Fractional, + support: VoteType.Parameters, reason: 'no particular reason', params, }), @@ -118,7 +118,7 @@ describe('GovernorCountingFractional', function () { .withArgs( this.voter2, this.proposal.id, - VoteType.Fractional, + VoteType.Parameters, sum(...votes), 'no particular reason', params, @@ -144,7 +144,7 @@ describe('GovernorCountingFractional', function () { const params = ethers.solidityPacked(['uint128', 'uint128', 'uint128'], fractional); await expect( this.helper.connect(this.voter2).vote({ - support: VoteType.Fractional, + support: VoteType.Parameters, reason: 'no particular reason', params, }), @@ -153,7 +153,7 @@ describe('GovernorCountingFractional', function () { .withArgs( this.voter2, this.proposal.id, - VoteType.Fractional, + VoteType.Parameters, sum(...fractional), 'no particular reason', params, @@ -180,7 +180,7 @@ describe('GovernorCountingFractional', function () { await expect( this.helper.connect(this.voter2).vote({ - support: VoteType.Fractional, + support: VoteType.Parameters, reason: 'no particular reason', params: ethers.solidityPacked(['uint128', 'uint128', 'uint128'], fractional), }), @@ -196,7 +196,7 @@ describe('GovernorCountingFractional', function () { await expect( this.helper.connect(this.voter2).vote({ - support: VoteType.Fractional, + support: VoteType.Parameters, reason: 'no particular reason', params: ethers.solidityPacked(['uint128', 'uint128', 'uint128'], [0n, 1n, 0n]), }), @@ -211,7 +211,7 @@ describe('GovernorCountingFractional', function () { await expect( this.helper.connect(this.voter2).vote({ - support: VoteType.Fractional, + support: VoteType.Parameters, reason: 'no particular reason', params: ethers.solidityPacked(['uint128', 'uint128'], [0n, 1n]), }), diff --git a/test/helpers/enums.js b/test/helpers/enums.js index 4eb872215fb..f95767ab7e7 100644 --- a/test/helpers/enums.js +++ b/test/helpers/enums.js @@ -5,7 +5,7 @@ function Enum(...options) { module.exports = { Enum, ProposalState: Enum('Pending', 'Active', 'Canceled', 'Defeated', 'Succeeded', 'Queued', 'Expired', 'Executed'), - VoteType: Enum('Against', 'For', 'Abstain', 'Fractional'), + VoteType: Object.assign(Enum('Against', 'For', 'Abstain'), { Parameters: 255n }), Rounding: Enum('Floor', 'Ceil', 'Trunc', 'Expand'), OperationState: Enum('Unset', 'Waiting', 'Ready', 'Done'), RevertType: Enum('None', 'RevertWithoutMessage', 'RevertWithMessage', 'RevertWithCustomError', 'Panic'), From 37e9a0ee272511dbcb84a4431eaece4fe3cf2457 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 28 May 2024 10:50:09 +0200 Subject: [PATCH 20/26] document params as a support option --- contracts/governance/extensions/GovernorCountingFractional.sol | 2 +- test/governance/extensions/GovernorCountingFractional.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/governance/extensions/GovernorCountingFractional.sol b/contracts/governance/extensions/GovernorCountingFractional.sol index 207a53da551..03b1d25353a 100644 --- a/contracts/governance/extensions/GovernorCountingFractional.sol +++ b/contracts/governance/extensions/GovernorCountingFractional.sol @@ -53,7 +53,7 @@ abstract contract GovernorCountingFractional is Governor { */ // solhint-disable-next-line func-name-mixedcase function COUNTING_MODE() public pure virtual override returns (string memory) { - return "support=bravo&quorum=for,abstain¶ms=fractional"; + return "support=bravo,params&quorum=for,abstain¶ms=fractional"; } /** diff --git a/test/governance/extensions/GovernorCountingFractional.test.js b/test/governance/extensions/GovernorCountingFractional.test.js index e7fe39d090c..74ff86d6416 100644 --- a/test/governance/extensions/GovernorCountingFractional.test.js +++ b/test/governance/extensions/GovernorCountingFractional.test.js @@ -71,7 +71,7 @@ describe('GovernorCountingFractional', function () { expect(await this.mock.token()).to.equal(this.token); expect(await this.mock.votingDelay()).to.equal(votingDelay); expect(await this.mock.votingPeriod()).to.equal(votingPeriod); - expect(await this.mock.COUNTING_MODE()).to.equal('support=bravo&quorum=for,abstain¶ms=fractional'); + expect(await this.mock.COUNTING_MODE()).to.equal('support=bravo,params&quorum=for,abstain¶ms=fractional'); }); it('nominal is unaffected', async function () { From dce6fba82305c5745de2b8262afeb55ca754b756 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 29 May 2024 20:20:37 +0200 Subject: [PATCH 21/26] update COUNTING_MODE --- .../governance/extensions/GovernorCountingFractional.sol | 6 ++++-- .../extensions/GovernorCountingFractional.test.js | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/contracts/governance/extensions/GovernorCountingFractional.sol b/contracts/governance/extensions/GovernorCountingFractional.sol index 03b1d25353a..a05f49f3968 100644 --- a/contracts/governance/extensions/GovernorCountingFractional.sol +++ b/contracts/governance/extensions/GovernorCountingFractional.sol @@ -31,6 +31,8 @@ import {Math} from "../../utils/math/Math.sol"; abstract contract GovernorCountingFractional is Governor { using Math for *; + uint8 internal constant VOTE_TYPE_FRACTIONAL = 255; + struct ProposalVote { uint256 againstVotes; uint256 forVotes; @@ -53,7 +55,7 @@ abstract contract GovernorCountingFractional is Governor { */ // solhint-disable-next-line func-name-mixedcase function COUNTING_MODE() public pure virtual override returns (string memory) { - return "support=bravo,params&quorum=for,abstain¶ms=fractional"; + return "support=bravo,fractional&quorum=for,abstain¶ms=fractional"; } /** @@ -142,7 +144,7 @@ abstract contract GovernorCountingFractional is Governor { if (support <= uint8(GovernorCountingSimple.VoteType.Abstain)) { if (params.length != 0) revert GovernorInvalidVoteParams(); return _countVoteNominal(proposalId, account, support, remainingWeight); - } else if (support == type(uint8).max) { + } else if (support == VOTE_TYPE_FRACTIONAL) { if (params.length != 0x30) revert GovernorInvalidVoteParams(); return _countVoteFractional(proposalId, account, params, remainingWeight); } else { diff --git a/test/governance/extensions/GovernorCountingFractional.test.js b/test/governance/extensions/GovernorCountingFractional.test.js index 74ff86d6416..393dbad79d5 100644 --- a/test/governance/extensions/GovernorCountingFractional.test.js +++ b/test/governance/extensions/GovernorCountingFractional.test.js @@ -71,7 +71,9 @@ describe('GovernorCountingFractional', function () { expect(await this.mock.token()).to.equal(this.token); expect(await this.mock.votingDelay()).to.equal(votingDelay); expect(await this.mock.votingPeriod()).to.equal(votingPeriod); - expect(await this.mock.COUNTING_MODE()).to.equal('support=bravo,params&quorum=for,abstain¶ms=fractional'); + expect(await this.mock.COUNTING_MODE()).to.equal( + 'support=bravo,fractional&quorum=for,abstain¶ms=fractional', + ); }); it('nominal is unaffected', async function () { From e0f5b71f24f1228affb5caea90e5469a07115bf7 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Wed, 29 May 2024 14:11:02 -0600 Subject: [PATCH 22/26] Unpack votes from a single helper --- .../extensions/GovernorCountingFractional.sol | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/contracts/governance/extensions/GovernorCountingFractional.sol b/contracts/governance/extensions/GovernorCountingFractional.sol index a05f49f3968..383023a6ced 100644 --- a/contracts/governance/extensions/GovernorCountingFractional.sol +++ b/contracts/governance/extensions/GovernorCountingFractional.sol @@ -145,7 +145,6 @@ abstract contract GovernorCountingFractional is Governor { if (params.length != 0) revert GovernorInvalidVoteParams(); return _countVoteNominal(proposalId, account, support, remainingWeight); } else if (support == VOTE_TYPE_FRACTIONAL) { - if (params.length != 0x30) revert GovernorInvalidVoteParams(); return _countVoteFractional(proposalId, account, params, remainingWeight); } else { revert GovernorInvalidVoteType(); @@ -202,9 +201,7 @@ abstract contract GovernorCountingFractional is Governor { bytes memory params, uint256 weight ) private returns (uint256) { - uint128 againstVotes = _extractUint128(params, 0); - uint128 forVotes = _extractUint128(params, 1); - uint128 abstainVotes = _extractUint128(params, 2); + (uint128 againstVotes, uint128 forVotes, uint128 abstainVotes) = _unpackParams(params); uint256 usedWeight = againstVotes + forVotes + abstainVotes; if (usedWeight > weight) { @@ -226,9 +223,18 @@ abstract contract GovernorCountingFractional is Governor { return usedWeight; } - function _extractUint128(bytes memory data, uint256 pos) private pure returns (uint128 result) { - assembly { - result := shr(128, mload(add(data, add(0x20, mul(0x10, pos))))) + function _unpackParams( + bytes memory data + ) private pure returns (uint128 againstVotes, uint128 forVotes, uint128 abstainVotes) { + if (data.length != 0x30) { + revert GovernorInvalidVoteParams(); + } + + assembly ("memory-safe") { + let ptr := add(data, 0x20) + againstVotes := shr(128, mload(ptr)) + forVotes := shr(128, mload(add(ptr, 0x10))) + abstainVotes := shr(128, mload(add(ptr, 0x20))) } } } From f1b71bc13763f1b5dd80505ea2d9afe2369f418f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Wed, 29 May 2024 14:12:37 -0600 Subject: [PATCH 23/26] Update contracts/governance/extensions/GovernorCountingFractional.sol Co-authored-by: Francisco --- contracts/governance/extensions/GovernorCountingFractional.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/governance/extensions/GovernorCountingFractional.sol b/contracts/governance/extensions/GovernorCountingFractional.sol index 383023a6ced..88ec77486a0 100644 --- a/contracts/governance/extensions/GovernorCountingFractional.sol +++ b/contracts/governance/extensions/GovernorCountingFractional.sol @@ -13,7 +13,7 @@ import {Math} from "../../utils/math/Math.sol"; * Against, For, Abstain. Additionally, it includes a fourth option: Fractional, which allows voters to split their voting * power amongst the other 3 options. * - * Votes cast with the Fractional support must be accompanied by a `params` argument that is tree packed `uint128` values + * Votes cast with the Fractional support must be accompanied by a `params` argument that is three packed `uint128` values * representing the weight the delegate assigns to Against, For, and Abstain respectively. For those votes cast for the other * 3 options, the `params` argument must be empty. * From 37391ce6992829002e999247df9956989b860c84 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 30 May 2024 10:34:19 +0200 Subject: [PATCH 24/26] refactor: inline logic --- .../extensions/GovernorCountingFractional.sol | 134 ++++++------------ 1 file changed, 46 insertions(+), 88 deletions(-) diff --git a/contracts/governance/extensions/GovernorCountingFractional.sol b/contracts/governance/extensions/GovernorCountingFractional.sol index 88ec77486a0..1c02aea349b 100644 --- a/contracts/governance/extensions/GovernorCountingFractional.sol +++ b/contracts/governance/extensions/GovernorCountingFractional.sol @@ -102,26 +102,31 @@ abstract contract GovernorCountingFractional is Governor { /** * @dev See {Governor-_countVote}. Function that records the delegate's votes. * - * Executing this function consume's the delegate's weight on the proposal. This weight can be distributed amongst - * the 3 options (Against, For, Abstain) by specifying a Fractional `support`. + * Executing this function consumes (part of) the delegate's weight on the proposal. This weight can be + * distributed amongst the 3 options (Against, For, Abstain) by specifying a Fractional `support`. * - * When support is anything other than Fractional, the `params` argument must be empty and the delegate's full remaining - * weight is cast for the specified `support` option, as in {GovernorCountingSimple} and following the `VoteType` - * enum from Governor Bravo. + * This counting module supports two vote casting modes: nominal and fractional. * - * Given a Fractional `support`, the `params` argument must be tree packed `uint128` values representing the weight - * the delegate assigns to Against, For, and Abstain respectively. This format can be produced using: + * - Voting in "nomainal" mode is achieved by setting `support` to on of the 3 bravo options (Against, For, + * Abstain). In this mode, the `params` argument must be empty and the delegate's full remaining weight is cast + * for the specified `support` option, as in {GovernorCountingSimple} and following the `VoteType` enum from + * Governor Bravo. As a consequence, no vote weight remains unspend so no further voting is possible (for this + * `proposalId` and this `account`). + * + * - Voting in "fractional" mode is achieved by setting `support` to `type(uint8).max` (255). In this mode, the + * `params` argument must be tree packed `uint128` values representing the weight the delegate assigns to each + * support option (Against, For, and Abstain respectively). This format can be produced using: * * `abi.encodePacked(uint128(againstVotes), uint128(forVotes), uint128(abstainVotes))` * * The sum total of the three decoded vote weights _must_ be less than or equal to the delegate's remaining weight * on the proposal, i.e. their checkpointed total weight minus votes already cast on the proposal. * - * NOTE: Consider the number of votes is restricted to 128 bits. Depending on how many decimals the underlying token - * has, a single voter may require to split their vote into multiple transactions. For precision higher than - * ~30 decimals, large token holders may require an exponentially large number of transactions to cast their vote. - * - * See `_countVoteNominal` and `_countVoteFractional` for more details. + * NOTE: Consider that fractional voting restricts the number of casted vote (in each category) to 128 bits. + * Depending on how many decimals the underlying token has, a single voter may require to split their vote into + * multiple vote operations. For precision higher than ~30 decimals, large token holders may require an + * potentially large number of calls to cast all their votes. The voter has the possibility to cast all the + * remaining votes in a single operation using the traditional "bravo" vote. */ function _countVote( uint256 proposalId, @@ -136,78 +141,46 @@ abstract contract GovernorCountingFractional is Governor { revert GovernorAlreadyCastVote(account); } + uint256 againstVotes = 0; + uint256 forVotes = 0; + uint256 abstainVotes = 0; + uint256 usedWeight; + // For clarity of event indexing, fractional voting must be clearly advertised in the "support" field. // // Supported `support` value must be: // - "Full" voting: `support = 0` (Against), `1` (For) or `2` (Abstain), with empty params. // - "Fractional" voting: `support = 255`, with 48 bytes params. - if (support <= uint8(GovernorCountingSimple.VoteType.Abstain)) { + if (support == uint8(GovernorCountingSimple.VoteType.Against)) { + if (params.length != 0) revert GovernorInvalidVoteParams(); + usedWeight = againstVotes = remainingWeight; + } else if (support == uint8(GovernorCountingSimple.VoteType.For)) { + if (params.length != 0) revert GovernorInvalidVoteParams(); + usedWeight = forVotes = remainingWeight; + } else if (support == uint8(GovernorCountingSimple.VoteType.Abstain)) { if (params.length != 0) revert GovernorInvalidVoteParams(); - return _countVoteNominal(proposalId, account, support, remainingWeight); + usedWeight = abstainVotes = remainingWeight; } else if (support == VOTE_TYPE_FRACTIONAL) { - return _countVoteFractional(proposalId, account, params, remainingWeight); + // The `params` argument is expected to be three packed `uint128`: + // `abi.encodePacked(uint128(againstVotes), uint128(forVotes), uint128(abstainVotes))` + if (params.length != 0x30) revert GovernorInvalidVoteParams(); + + assembly ("memory-safe") { + againstVotes := shr(128, mload(add(params, 0x20))) + forVotes := shr(128, mload(add(params, 0x30))) + abstainVotes := shr(128, mload(add(params, 0x40))) + } + + // check parsed arguments are valid + usedWeight = againstVotes + forVotes + abstainVotes; + if (usedWeight > remainingWeight) { + revert GovernorExceedRemainingWeight(account, usedWeight, remainingWeight); + } } else { revert GovernorInvalidVoteType(); } - } - - /** - * @dev Record votes with full weight cast for `support`. - * - * Because this function votes with the delegate's remaining weight, it can only be called once per proposal and - * thus does not require any replay protection. - */ - function _countVoteNominal( - uint256 proposalId, - address account, - uint8 support, - uint256 weight - ) private returns (uint256) { - ProposalVote storage details = _proposalVotes[proposalId]; - details.usedVotes[account] += weight; - - if (support == uint8(GovernorCountingSimple.VoteType.Against)) { - details.againstVotes += weight; - } else if (support == uint8(GovernorCountingSimple.VoteType.For)) { - details.forVotes += weight; - } else if (support == uint8(GovernorCountingSimple.VoteType.Abstain)) { - details.abstainVotes += weight; - } - - return weight; - } - - /** - * @dev Count votes with fractional weight. - * - * The `params` argument is expected to be three packed `uint128`: - * `abi.encodePacked(uint128(againstVotes), uint128(forVotes), uint128(abstainVotes))` - * - * This function can be called multiple times for the same account and proposal (i.e. partial/rolling votes are - * allowed). For example, an account with total weight of 10 could call this function three times with the - * following vote data: - * - * * Against: 1, For: 0, Abstain: 2 - * * Against: 3, For: 1, Abstain: 0 - * * Against: 1, For: 1, Abstain: 1 - * - * Casting votes like this will make the calling account to cast a total of 5 `Against` votes, 2 `For` votes - * and 3 `Abstain` votes. Though partial, votes are still final once cast and cannot be changed or overridden. - * Subsequent partial votes simply increment existing totals. - */ - function _countVoteFractional( - uint256 proposalId, - address account, - bytes memory params, - uint256 weight - ) private returns (uint256) { - (uint128 againstVotes, uint128 forVotes, uint128 abstainVotes) = _unpackParams(params); - - uint256 usedWeight = againstVotes + forVotes + abstainVotes; - if (usedWeight > weight) { - revert GovernorExceedRemainingWeight(account, usedWeight, weight); - } + // update votes tracking ProposalVote storage details = _proposalVotes[proposalId]; if (againstVotes > 0) { details.againstVotes += againstVotes; @@ -222,19 +195,4 @@ abstract contract GovernorCountingFractional is Governor { return usedWeight; } - - function _unpackParams( - bytes memory data - ) private pure returns (uint128 againstVotes, uint128 forVotes, uint128 abstainVotes) { - if (data.length != 0x30) { - revert GovernorInvalidVoteParams(); - } - - assembly ("memory-safe") { - let ptr := add(data, 0x20) - againstVotes := shr(128, mload(ptr)) - forVotes := shr(128, mload(add(ptr, 0x10))) - abstainVotes := shr(128, mload(add(ptr, 0x20))) - } - } } From 72a420b150eaf9326988a5d398dcd5b66d985517 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 30 May 2024 14:34:19 +0200 Subject: [PATCH 25/26] slither disable locally --- .../extensions/GovernorCountingFractional.sol | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/contracts/governance/extensions/GovernorCountingFractional.sol b/contracts/governance/extensions/GovernorCountingFractional.sol index 1c02aea349b..4f83d9c8b2d 100644 --- a/contracts/governance/extensions/GovernorCountingFractional.sol +++ b/contracts/governance/extensions/GovernorCountingFractional.sol @@ -128,6 +128,7 @@ abstract contract GovernorCountingFractional is Governor { * potentially large number of calls to cast all their votes. The voter has the possibility to cast all the * remaining votes in a single operation using the traditional "bravo" vote. */ + // slither-disable-next-line cyclomatic-complexity function _countVote( uint256 proposalId, address account, @@ -169,10 +170,10 @@ abstract contract GovernorCountingFractional is Governor { againstVotes := shr(128, mload(add(params, 0x20))) forVotes := shr(128, mload(add(params, 0x30))) abstainVotes := shr(128, mload(add(params, 0x40))) + usedWeight := add(add(againstVotes, forVotes), abstainVotes) // inputs are uint128: cannot overflow } // check parsed arguments are valid - usedWeight = againstVotes + forVotes + abstainVotes; if (usedWeight > remainingWeight) { revert GovernorExceedRemainingWeight(account, usedWeight, remainingWeight); } @@ -182,15 +183,9 @@ abstract contract GovernorCountingFractional is Governor { // update votes tracking ProposalVote storage details = _proposalVotes[proposalId]; - if (againstVotes > 0) { - details.againstVotes += againstVotes; - } - if (forVotes > 0) { - details.forVotes += forVotes; - } - if (abstainVotes > 0) { - details.abstainVotes += abstainVotes; - } + if (againstVotes > 0) details.againstVotes += againstVotes; + if (forVotes > 0) details.forVotes += forVotes; + if (abstainVotes > 0) details.abstainVotes += abstainVotes; details.usedVotes[account] += usedWeight; return usedWeight; From 149fe651bdcdd7f5aa3bfdda6b7301a7a01461fc Mon Sep 17 00:00:00 2001 From: ernestognw Date: Thu, 30 May 2024 13:22:43 -0600 Subject: [PATCH 26/26] Documentaton nits --- .../extensions/GovernorCountingFractional.sol | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/contracts/governance/extensions/GovernorCountingFractional.sol b/contracts/governance/extensions/GovernorCountingFractional.sol index 4f83d9c8b2d..5a553fbdb44 100644 --- a/contracts/governance/extensions/GovernorCountingFractional.sol +++ b/contracts/governance/extensions/GovernorCountingFractional.sol @@ -103,24 +103,24 @@ abstract contract GovernorCountingFractional is Governor { * @dev See {Governor-_countVote}. Function that records the delegate's votes. * * Executing this function consumes (part of) the delegate's weight on the proposal. This weight can be - * distributed amongst the 3 options (Against, For, Abstain) by specifying a Fractional `support`. + * distributed amongst the 3 options (Against, For, Abstain) by specifying a fractional `support`. * * This counting module supports two vote casting modes: nominal and fractional. * - * - Voting in "nomainal" mode is achieved by setting `support` to on of the 3 bravo options (Against, For, - * Abstain). In this mode, the `params` argument must be empty and the delegate's full remaining weight is cast - * for the specified `support` option, as in {GovernorCountingSimple} and following the `VoteType` enum from - * Governor Bravo. As a consequence, no vote weight remains unspend so no further voting is possible (for this - * `proposalId` and this `account`). + * - Nominal: A nominal vote is cast by setting `support` to one of the 3 bravo options (Against, For, Abstain). + * - Fractional: A fractional vote is cast by setting `support` to `type(uint8).max` (255). * - * - Voting in "fractional" mode is achieved by setting `support` to `type(uint8).max` (255). In this mode, the - * `params` argument must be tree packed `uint128` values representing the weight the delegate assigns to each - * support option (Against, For, and Abstain respectively). This format can be produced using: + * Casting a nominal vote requires `params` to be empty and consumes the delegate's full remaining weight on the + * proposal for the specified `support` option. This is similar to the {GovernorCountingSimple} module and follows + * the `VoteType` enum from Governor Bravo. As a consequence, no vote weight remains unspent so no further voting + * is possible (for this `proposalId` and this `account`). * - * `abi.encodePacked(uint128(againstVotes), uint128(forVotes), uint128(abstainVotes))` + * Casting a fractional vote consumes a fraction of the delegate's remaining weight on the proposal according to the + * weights the delegate assigns to each support option (Against, For, Abstain respectively). The sum total of the + * three decoded vote weights _must_ be less than or equal to the delegate's remaining weight on the proposal (i.e. + * their checkpointed total weight minus votes already cast on the proposal). This format can be produced using: * - * The sum total of the three decoded vote weights _must_ be less than or equal to the delegate's remaining weight - * on the proposal, i.e. their checkpointed total weight minus votes already cast on the proposal. + * `abi.encodePacked(uint128(againstVotes), uint128(forVotes), uint128(abstainVotes))` * * NOTE: Consider that fractional voting restricts the number of casted vote (in each category) to 128 bits. * Depending on how many decimals the underlying token has, a single voter may require to split their vote into