Skip to content

Per-proposal vote-signature nonce - #23

Merged
LeonardoVieira1630 merged 6 commits into
devfrom
worktree-nonce-per-proposal
Aug 4, 2026
Merged

Per-proposal vote-signature nonce#23
LeonardoVieira1630 merged 6 commits into
devfrom
worktree-nonce-per-proposal

Conversation

@LeonardoVieira1630

Copy link
Copy Markdown
Member

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

  • Storage/API: _voteNonces[proposalId][voter] + public voteNonce(uint256,address) — the source of truth for building ballots. The inherited OZ account-global nonces(address) is orphaned at 0 (can't be removed from the inheritance; documented in README).
  • Validation reads, _castVote spends (DR-2/DR-4): _validateVoteSig/_validateExtendedVoteSig are overridden as view, building the digest from the per-proposal nonce via the virtual getter. The single spend point is _castVote, before super._castVote dispatches to the ruleset — every cast path (direct, bySig, extended bySig, batch item) converges there, so the invariant voteNonce == applied casts holds by construction and the in-flight signature is dead during the ruleset's external countVote.
  • OZ typehashes kept (DR-1): only the nonce source changes. OZ-standard tooling that reads 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 with GovernorInvalidSignature, same as before.
  • Removals: the three castVote* overrides and the batch's _useNonce existed only to spend the account-global nonce — all deleted; the contract shrinks.
  • No new events/errors (DR-3): VoteCast already tells relayers which (proposal, voter) was invalidated.

Testing

  • New suite GovernorNexus.voteNonce.t.sol (9 tests): cross-proposal independence (the target case), same-proposal invalidation, per-path nonce accounting incl. batch duplicates, orphaned nonces() 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.
  • Flipped the two tests that encoded the account-global behavior (lifecycle cross-proposal, batch); the batch test now asserts both halves (batched ballot dies, unbatched survives).
  • Full suite green: 307/307 unit + 19/19 fork.
  • Reviewed: per-task spec+quality reviews, whole-branch security review (replay, cross-proposal/cross-voter digest binding, reentrancy incl. ERC-1271 staticcall), and a scoped re-review of the hardening commit.

🤖 Generated with Claude Code

LeonardoVieira1630 and others added 6 commits August 3, 2026 17:30
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>
@LeonardoVieira1630
LeonardoVieira1630 merged commit e2e0b94 into dev Aug 4, 2026
6 checks passed
@LeonardoVieira1630
LeonardoVieira1630 deleted the worktree-nonce-per-proposal branch August 19, 2026 17:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant