Per-proposal vote-signature nonce - #23
Merged
Merged
Conversation
A cast on proposal A now invalidates outstanding signed ballots for A only, not across all open proposals. Single spend point in _castVote; bySig validation reads the per-proposal nonce; account-global Nonces orphaned at 0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…alidators, ERC-1271 coverage Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Scopes the EIP-712 vote-signature nonce from account-global to (proposalId, voter): an applied cast on proposal A now invalidates the voter's outstanding signed ballots for proposal A only — held signatures for other open proposals stay valid. Closes the documented UX cost of the account-global design (one direct vote killed a delegate's pending ballots across every open proposal).
Design
_voteNonces[proposalId][voter]+ publicvoteNonce(uint256,address)— the source of truth for building ballots. The inherited OZ account-globalnonces(address)is orphaned at 0 (can't be removed from the inheritance; documented in README)._castVotespends (DR-2/DR-4):_validateVoteSig/_validateExtendedVoteSigare overridden asview, building the digest from the per-proposal nonce via the virtual getter. The single spend point is_castVote, beforesuper._castVotedispatches to the ruleset — every cast path (direct, bySig, extended bySig, batch item) converges there, so the invariantvoteNonce == applied castsholds by construction and the in-flight signature is dead during the ruleset's externalcountVote.nonces(voter)still produces valid signatures for a voter's first cast on each proposal (both counters start at 0); stale re-vote ballots revert withGovernorInvalidSignature, same as before.castVote*overrides and the batch's_useNonceexisted only to spend the account-global nonce — all deleted; the contract shrinks.VoteCastalready tells relayers which (proposal, voter) was invalidated.Testing
GovernorNexus.voteNonce.t.sol(9 tests): cross-proposal independence (the target case), same-proposal invalidation, per-path nonce accounting incl. batch duplicates, orphanednonces()regression guard, fresh re-sign after direct vote, extended-ballot path (previously zero coverage repo-wide), and ERC-1271 contract-signer validate + replay-reject through the new validators.staticcall), and a scoped re-review of the hardening commit.🤖 Generated with Claude Code