Skip to content

Add Governor extension GovernorNoncesKeyed to use NoncesKeyed for vote by sig #5574

New issue

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

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

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
5 changes: 5 additions & 0 deletions .changeset/popular-geese-tan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`GovernorNoncesKeyed`: Extension of `Governor` that adds support for keyed nonces when voting by sig.
78 changes: 52 additions & 26 deletions contracts/governance/Governor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -578,13 +578,15 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72
address voter,
bytes memory signature
) public virtual returns (uint256) {
bool valid = SignatureChecker.isValidSignatureNow(
voter,
_hashTypedDataV4(keccak256(abi.encode(BALLOT_TYPEHASH, proposalId, support, voter, _useNonce(voter)))),
signature
);

if (!valid) {
if (
!_validateVoteSignature(
voter,
proposalId,
signature,
abi.encode(BALLOT_TYPEHASH, proposalId, support, voter, uint256(0)),
0xA0
)
) {
revert GovernorInvalidSignature(voter);
}

Expand All @@ -602,31 +604,55 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72
bytes memory params,
bytes memory signature
) public virtual returns (uint256) {
bool valid = SignatureChecker.isValidSignatureNow(
voter,
_hashTypedDataV4(
keccak256(
abi.encode(
EXTENDED_BALLOT_TYPEHASH,
proposalId,
support,
voter,
_useNonce(voter),
keccak256(bytes(reason)),
keccak256(params)
)
)
),
signature
);

if (!valid) {
if (
!_validateVoteSignature(
voter,
proposalId,
signature,
abi.encode(
EXTENDED_BALLOT_TYPEHASH,
proposalId,
support,
voter,
uint256(0),
keccak256(bytes(reason)),
keccak256(params)
),
0xA0
)
) {
revert GovernorInvalidSignature(voter);
}

return _castVote(proposalId, voter, support, reason, params);
}

/**
* @dev Validate the `signature` used in {castVoteBySig} and {castVoteWithReasonAndParamsBySig} functions. The `digestPreimage`
* is the EIP712 digest prior to hashing with a 32 bytes gap left at `noncePositionOffset` for the nonce to be inserted
* (note the offset includes the first 32 bytes storing the size of the bytes array).
*/
function _validateVoteSignature(
address voter,
uint256 /* proposalId */,
bytes memory signature,
bytes memory digestPreimage,
uint256 noncePositionOffset
) internal virtual returns (bool) {
uint256 nonce = nonces(voter);
assembly ("memory-safe") {
mstore(add(digestPreimage, noncePositionOffset), nonce)
}

bool isValid = SignatureChecker.isValidSignatureNow(
voter,
_hashTypedDataV4(keccak256(digestPreimage)),
signature
);
if (isValid) _useNonce(voter);
return isValid;
}

/**
* @dev Internal vote casting mechanism: Check that the vote is pending, that it has not been cast yet, retrieve
* voting weight using {IGovernor-getVotes} and call the {_countVote} internal function. Uses the _defaultParams().
Expand Down
4 changes: 4 additions & 0 deletions contracts/governance/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ Other extensions can customize the behavior or interface in multiple ways.

* {GovernorSuperQuorum}: Extension of {Governor} with a super quorum. Proposals that meet the super quorum (and have a majority of for votes) advance to the `Succeeded` state before the proposal deadline.

* {GovernorNoncesKeyed}: An extension of {Governor} with support for keyed nonces in addition to traditional nonces when voting by signature.

In addition to modules and extensions, the core contract requires a few virtual functions to be implemented to your particular specifications:

* <<Governor-votingDelay-,`votingDelay()`>>: Delay (in ERC-6372 clock) since the proposal is submitted until voting power is fixed and voting starts. This can be used to enforce a delay after a proposal is published for users to buy tokens, or delegate their votes.
Expand Down Expand Up @@ -100,6 +102,8 @@ NOTE: Functions of the `Governor` contract do not include access control. If you

{{GovernorSuperQuorum}}

{{GovernorNoncesKeyed}}

== Utils

{{Votes}}
Expand Down
47 changes: 47 additions & 0 deletions contracts/governance/extensions/GovernorNoncesKeyed.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {Governor} from "../Governor.sol";
import {Nonces} from "../../utils/Nonces.sol";
import {NoncesKeyed} from "../../utils/NoncesKeyed.sol";
import {SignatureChecker} from "../../utils/cryptography/SignatureChecker.sol";

/**
* @dev An extension of {Governor} that extends existing nonce management to use {NoncesKeyed}, where the key is the first 192 bits of the `proposalId`.
* This is useful for voting by signature while maintaining separate sequences of nonces for each proposal.
*
* NOTE: Traditional (un-keyed) nonces are still supported and can continue to be used as if this extension was not present.
*/
abstract contract GovernorNoncesKeyed is Governor, NoncesKeyed {
function _useCheckedNonce(address owner, uint256 nonce) internal virtual override(Nonces, NoncesKeyed) {
super._useCheckedNonce(owner, nonce);
}

/// @dev Check the signature against the traditional nonce and then the keyed nonce.
function _validateVoteSignature(
address voter,
uint256 proposalId,
bytes memory signature,
bytes memory digestPreimage,
uint256 noncePositionOffset
) internal virtual override returns (bool) {
if (super._validateVoteSignature(voter, proposalId, signature, digestPreimage, noncePositionOffset)) {
return true;
} else {
// uint192 is sufficient entropy for proposalId within nonce keys.
uint256 keyedNonce = nonces(voter, uint192(proposalId));
assembly ("memory-safe") {
mstore(add(digestPreimage, noncePositionOffset), keyedNonce)
}

bool isValid = SignatureChecker.isValidSignatureNow(
voter,
_hashTypedDataV4(keccak256(digestPreimage)),
signature
);
if (isValid) _useNonce(voter, uint192(proposalId));
return isValid;
}
}
}
35 changes: 35 additions & 0 deletions contracts/mocks/governance/GovernorNoncesKeyedMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {Governor, Nonces} from "../../governance/Governor.sol";
import {GovernorSettings} from "../../governance/extensions/GovernorSettings.sol";
import {GovernorCountingSimple} from "../../governance/extensions/GovernorCountingSimple.sol";
import {GovernorVotesQuorumFraction} from "../../governance/extensions/GovernorVotesQuorumFraction.sol";
import {GovernorProposalGuardian} from "../../governance/extensions/GovernorProposalGuardian.sol";
import {GovernorNoncesKeyed} from "../../governance/extensions/GovernorNoncesKeyed.sol";

abstract contract GovernorNoncesKeyedMock is
GovernorSettings,
GovernorVotesQuorumFraction,
GovernorCountingSimple,
GovernorNoncesKeyed
{
function proposalThreshold() public view override(Governor, GovernorSettings) returns (uint256) {
return super.proposalThreshold();
}

function _validateVoteSignature(
address voter,
uint256 proposalId,
bytes memory signature,
bytes memory rawSignatureDigestData,
uint256 noncePositionOffset
) internal virtual override(Governor, GovernorNoncesKeyed) returns (bool) {
return super._validateVoteSignature(voter, proposalId, signature, rawSignatureDigestData, noncePositionOffset);
}

function _useCheckedNonce(address owner, uint256 nonce) internal virtual override(Nonces, GovernorNoncesKeyed) {
super._useCheckedNonce(owner, nonce);
}
}
48 changes: 26 additions & 22 deletions test/governance/Governor.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,31 +198,35 @@ describe('Governor', function () {
});

describe('vote with signature', function () {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about adding stress test edge cases (e.g. signature replay on multiple proposals) or extend the extension to support batch signatures per user?

it('votes with an EOA signature', async function () {
it('votes with an EOA signature on two proposals', async function () {
await this.token.connect(this.voter1).delegate(this.userEOA);

const nonce = await this.mock.nonces(this.userEOA);

// Run proposal
await this.helper.propose();
await this.helper.waitForSnapshot();
await expect(
this.helper.vote({
support: VoteType.For,
voter: this.userEOA.address,
nonce,
signature: signBallot(this.userEOA),
}),
)
.to.emit(this.mock, 'VoteCast')
.withArgs(this.userEOA, this.proposal.id, VoteType.For, ethers.parseEther('10'), '');

await this.helper.waitForDeadline();
await this.helper.execute();
for (let i = 0; i < 2; i++) {
const nonce = await this.mock.nonces(this.userEOA);

// After
expect(await this.mock.hasVoted(this.proposal.id, this.userEOA)).to.be.true;
expect(await this.mock.nonces(this.userEOA)).to.equal(nonce + 1n);
// Run proposal
await this.helper.propose();
await this.helper.waitForSnapshot();
await expect(
this.helper.vote({
support: VoteType.For,
voter: this.userEOA.address,
nonce,
signature: signBallot(this.userEOA),
}),
)
.to.emit(this.mock, 'VoteCast')
.withArgs(this.userEOA, this.proposal.id, VoteType.For, ethers.parseEther('10'), '');

// After
expect(await this.mock.hasVoted(this.proposal.id, this.userEOA)).to.be.true;
expect(await this.mock.nonces(this.userEOA)).to.equal(nonce + 1n);

// Update proposal to allow for re-propose
this.helper.description += ' - updated';
}

await expect(this.mock.nonces(this.userEOA)).to.eventually.equal(2n);
});

it('votes with a valid EIP-1271 signature', async function () {
Expand Down
Loading
Loading