Skip to content

Commit 8bff2a7

Browse files
arr00Amxxernestognw
authored
Add Governor extension GovernorNoncesKeyed to use NoncesKeyed for vote by sig (#5574)
Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com> Co-authored-by: ernestognw <ernestognw@gmail.com>
1 parent b6a5e89 commit 8bff2a7

File tree

7 files changed

+460
-50
lines changed

7 files changed

+460
-50
lines changed

.changeset/popular-geese-tan.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`GovernorNoncesKeyed`: Extension of `Governor` that adds support for keyed nonces when voting by sig.

contracts/governance/Governor.sol

Lines changed: 46 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -538,16 +538,9 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72
538538
address voter,
539539
bytes memory signature
540540
) public virtual returns (uint256) {
541-
bool valid = SignatureChecker.isValidSignatureNow(
542-
voter,
543-
_hashTypedDataV4(keccak256(abi.encode(BALLOT_TYPEHASH, proposalId, support, voter, _useNonce(voter)))),
544-
signature
545-
);
546-
547-
if (!valid) {
541+
if (!_validateVoteSig(proposalId, support, voter, signature)) {
548542
revert GovernorInvalidSignature(voter);
549543
}
550-
551544
return _castVote(proposalId, voter, support, "");
552545
}
553546

@@ -560,31 +553,56 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72
560553
bytes memory params,
561554
bytes memory signature
562555
) public virtual returns (uint256) {
563-
bool valid = SignatureChecker.isValidSignatureNow(
564-
voter,
565-
_hashTypedDataV4(
566-
keccak256(
567-
abi.encode(
568-
EXTENDED_BALLOT_TYPEHASH,
569-
proposalId,
570-
support,
571-
voter,
572-
_useNonce(voter),
573-
keccak256(bytes(reason)),
574-
keccak256(params)
575-
)
576-
)
577-
),
578-
signature
579-
);
580-
581-
if (!valid) {
556+
if (!_validateExtendedVoteSig(proposalId, support, voter, reason, params, signature)) {
582557
revert GovernorInvalidSignature(voter);
583558
}
584-
585559
return _castVote(proposalId, voter, support, reason, params);
586560
}
587561

562+
/// @dev Validate the `signature` used in {castVoteBySig} function.
563+
function _validateVoteSig(
564+
uint256 proposalId,
565+
uint8 support,
566+
address voter,
567+
bytes memory signature
568+
) internal virtual returns (bool) {
569+
return
570+
SignatureChecker.isValidSignatureNow(
571+
voter,
572+
_hashTypedDataV4(keccak256(abi.encode(BALLOT_TYPEHASH, proposalId, support, voter, _useNonce(voter)))),
573+
signature
574+
);
575+
}
576+
577+
/// @dev Validate the `signature` used in {castVoteWithReasonAndParamsBySig} function.
578+
function _validateExtendedVoteSig(
579+
uint256 proposalId,
580+
uint8 support,
581+
address voter,
582+
string memory reason,
583+
bytes memory params,
584+
bytes memory signature
585+
) internal virtual returns (bool) {
586+
return
587+
SignatureChecker.isValidSignatureNow(
588+
voter,
589+
_hashTypedDataV4(
590+
keccak256(
591+
abi.encode(
592+
EXTENDED_BALLOT_TYPEHASH,
593+
proposalId,
594+
support,
595+
voter,
596+
_useNonce(voter),
597+
keccak256(bytes(reason)),
598+
keccak256(params)
599+
)
600+
)
601+
),
602+
signature
603+
);
604+
}
605+
588606
/**
589607
* @dev Internal vote casting mechanism: Check that the vote is pending, that it has not been cast yet, retrieve
590608
* voting weight using {IGovernor-getVotes} and call the {_countVote} internal function. Uses the _defaultParams().

contracts/governance/README.adoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ Other extensions can customize the behavior or interface in multiple ways.
5454

5555
* {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.
5656

57+
* {GovernorNoncesKeyed}: An extension of {Governor} with support for keyed nonces in addition to traditional nonces when voting by signature.
58+
5759
In addition to modules and extensions, the core contract requires a few virtual functions to be implemented to your particular specifications:
5860

5961
* <<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.
@@ -100,6 +102,8 @@ NOTE: Functions of the `Governor` contract do not include access control. If you
100102

101103
{{GovernorSuperQuorum}}
102104

105+
{{GovernorNoncesKeyed}}
106+
103107
== Utils
104108

105109
{{Votes}}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.24;
4+
5+
import {Governor} from "../Governor.sol";
6+
import {Nonces} from "../../utils/Nonces.sol";
7+
import {NoncesKeyed} from "../../utils/NoncesKeyed.sol";
8+
import {SignatureChecker} from "../../utils/cryptography/SignatureChecker.sol";
9+
10+
/**
11+
* @dev An extension of {Governor} that extends existing nonce management to use {NoncesKeyed}, where the key is the first 192 bits of the `proposalId`.
12+
* This is useful for voting by signature while maintaining separate sequences of nonces for each proposal.
13+
*
14+
* NOTE: Traditional (un-keyed) nonces are still supported and can continue to be used as if this extension was not present.
15+
*/
16+
abstract contract GovernorNoncesKeyed is Governor, NoncesKeyed {
17+
function _useCheckedNonce(address owner, uint256 nonce) internal virtual override(Nonces, NoncesKeyed) {
18+
super._useCheckedNonce(owner, nonce);
19+
}
20+
21+
/**
22+
* @dev Check the signature against keyed nonce and falls back to the traditional nonce.
23+
*
24+
* NOTE: This function won't call `super._validateVoteSig` if the keyed nonce is valid.
25+
* Side effects may be skipped depending on the linearization of the function.
26+
*/
27+
function _validateVoteSig(
28+
uint256 proposalId,
29+
uint8 support,
30+
address voter,
31+
bytes memory signature
32+
) internal virtual override returns (bool) {
33+
if (
34+
SignatureChecker.isValidSignatureNow(
35+
voter,
36+
_hashTypedDataV4(
37+
keccak256(
38+
abi.encode(BALLOT_TYPEHASH, proposalId, support, voter, nonces(voter, uint192(proposalId)))
39+
)
40+
),
41+
signature
42+
)
43+
) {
44+
_useNonce(voter, uint192(proposalId));
45+
return true;
46+
} else {
47+
return super._validateVoteSig(proposalId, support, voter, signature);
48+
}
49+
}
50+
51+
/**
52+
* @dev Check the signature against keyed nonce and falls back to the traditional nonce.
53+
*
54+
* NOTE: This function won't call `super._validateExtendedVoteSig` if the keyed nonce is valid.
55+
* Side effects may be skipped depending on the linearization of the function.
56+
*/
57+
function _validateExtendedVoteSig(
58+
uint256 proposalId,
59+
uint8 support,
60+
address voter,
61+
string memory reason,
62+
bytes memory params,
63+
bytes memory signature
64+
) internal virtual override returns (bool) {
65+
if (
66+
SignatureChecker.isValidSignatureNow(
67+
voter,
68+
_hashTypedDataV4(
69+
keccak256(
70+
abi.encode(
71+
EXTENDED_BALLOT_TYPEHASH,
72+
proposalId,
73+
support,
74+
voter,
75+
nonces(voter, uint192(proposalId)),
76+
keccak256(bytes(reason)),
77+
keccak256(params)
78+
)
79+
)
80+
),
81+
signature
82+
)
83+
) {
84+
_useNonce(voter, uint192(proposalId));
85+
return true;
86+
} else {
87+
return super._validateExtendedVoteSig(proposalId, support, voter, reason, params, signature);
88+
}
89+
}
90+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.24;
4+
5+
import {Governor, Nonces} from "../../governance/Governor.sol";
6+
import {GovernorSettings} from "../../governance/extensions/GovernorSettings.sol";
7+
import {GovernorCountingSimple} from "../../governance/extensions/GovernorCountingSimple.sol";
8+
import {GovernorVotesQuorumFraction} from "../../governance/extensions/GovernorVotesQuorumFraction.sol";
9+
import {GovernorProposalGuardian} from "../../governance/extensions/GovernorProposalGuardian.sol";
10+
import {GovernorNoncesKeyed} from "../../governance/extensions/GovernorNoncesKeyed.sol";
11+
12+
abstract contract GovernorNoncesKeyedMock is
13+
GovernorSettings,
14+
GovernorVotesQuorumFraction,
15+
GovernorCountingSimple,
16+
GovernorNoncesKeyed
17+
{
18+
function proposalThreshold() public view override(Governor, GovernorSettings) returns (uint256) {
19+
return super.proposalThreshold();
20+
}
21+
22+
function _validateVoteSig(
23+
uint256 proposalId,
24+
uint8 support,
25+
address voter,
26+
bytes memory signature
27+
) internal virtual override(Governor, GovernorNoncesKeyed) returns (bool) {
28+
return super._validateVoteSig(proposalId, support, voter, signature);
29+
}
30+
31+
function _validateExtendedVoteSig(
32+
uint256 proposalId,
33+
uint8 support,
34+
address voter,
35+
string memory reason,
36+
bytes memory params,
37+
bytes memory signature
38+
) internal virtual override(Governor, GovernorNoncesKeyed) returns (bool) {
39+
return super._validateExtendedVoteSig(proposalId, support, voter, reason, params, signature);
40+
}
41+
42+
function _useCheckedNonce(address owner, uint256 nonce) internal virtual override(Nonces, GovernorNoncesKeyed) {
43+
super._useCheckedNonce(owner, nonce);
44+
}
45+
}

test/governance/Governor.test.js

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -198,31 +198,35 @@ describe('Governor', function () {
198198
});
199199

200200
describe('vote with signature', function () {
201-
it('votes with an EOA signature', async function () {
201+
it('votes with an EOA signature on two proposals', async function () {
202202
await this.token.connect(this.voter1).delegate(this.userEOA);
203203

204-
const nonce = await this.mock.nonces(this.userEOA);
205-
206-
// Run proposal
207-
await this.helper.propose();
208-
await this.helper.waitForSnapshot();
209-
await expect(
210-
this.helper.vote({
211-
support: VoteType.For,
212-
voter: this.userEOA.address,
213-
nonce,
214-
signature: signBallot(this.userEOA),
215-
}),
216-
)
217-
.to.emit(this.mock, 'VoteCast')
218-
.withArgs(this.userEOA, this.proposal.id, VoteType.For, ethers.parseEther('10'), '');
219-
220-
await this.helper.waitForDeadline();
221-
await this.helper.execute();
204+
for (let i = 0; i < 2; i++) {
205+
const nonce = await this.mock.nonces(this.userEOA);
222206

223-
// After
224-
expect(await this.mock.hasVoted(this.proposal.id, this.userEOA)).to.be.true;
225-
expect(await this.mock.nonces(this.userEOA)).to.equal(nonce + 1n);
207+
// Run proposal
208+
await this.helper.propose();
209+
await this.helper.waitForSnapshot();
210+
await expect(
211+
this.helper.vote({
212+
support: VoteType.For,
213+
voter: this.userEOA.address,
214+
nonce,
215+
signature: signBallot(this.userEOA),
216+
}),
217+
)
218+
.to.emit(this.mock, 'VoteCast')
219+
.withArgs(this.userEOA, this.proposal.id, VoteType.For, ethers.parseEther('10'), '');
220+
221+
// After
222+
expect(await this.mock.hasVoted(this.proposal.id, this.userEOA)).to.be.true;
223+
expect(await this.mock.nonces(this.userEOA)).to.equal(nonce + 1n);
224+
225+
// Update proposal to allow for re-propose
226+
this.helper.description += ' - updated';
227+
}
228+
229+
await expect(this.mock.nonces(this.userEOA)).to.eventually.equal(2n);
226230
});
227231

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

0 commit comments

Comments
 (0)