Dev - #25
Merged
Merged
Conversation
The shared counting layer every ruleset inherits: Bravo buckets, per-voter
receipts, and re-vote-as-replace — countVote debits the receipt's recorded
weight from its recorded bucket before crediting the new vote, atomically, so
a voter's weight is never double-counted nor transiently missing.
Receipt packs {hasVoted, support, uint240 weight} into one slot with an explicit
WeightOverflow guard: the debit side needs the recorded weight to be exact, so a
weight that cannot be recorded must revert rather than truncate (closes the
uint240-vs-uint256 width-asymmetry lead).
Semantics ported from the subtract-old/add-new family (Aragon v1/OSx
VoteReplacement, Polkadot conviction-voting) — no prior art inside the
OZ/Bravo/Nouns lineage, where re-voting is uniformly a revert.
Tallies become non-monotonic: quorum/success can flip in both directions while
voting is open, so no consumer may arm one-shot state on a tally-crossing event
(finding F2 — the anti-snipe extension lands in Nexus 3). Pinned in natspec and
witnessed by the oscillation test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… become mutable StandardRuleset keeps only its rules (quorum fraction, for > against, counting mode, ERC165); buckets, receipts and countVote move to RulesetCounting. Its one semantic change: a re-vote replaces the standing vote instead of reverting AlreadyVoted (D13). Unauthorized/InvalidVoteType now come from the base — same selectors, same revert data, only the declaring contract moved. Fork parity: re-vote is no longer a parity assertion but the first deliberate divergence from the live ENS governor (live rejects, Nexus replaces), pinned in ParityDivergencesTest. Lifecycle suite pins the integration contract: the outcome follows the latest vote, each cast re-emits VoteCast (latest per (proposal, voter) in log order is canonical for indexers), the core's Active gate still closes the window, and a stale castVoteBySig ballot cannot be replayed to restore a superseded vote — the replay surface that bit ScopeLift's Flexible Voting, foreclosed here by OZ v5's per-account nonce. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Records the two things an integrator must know before building on the tallies: latest VoteCast per (proposal, voter) is canonical (do not sum), and tallies are non-monotonic so nothing may arm one-shot state on a tally crossing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two changes the differential run demanded:
1. Buckets move from a fixed {against, for_, abstain} struct to
mapping(proposalId => mapping(support => weight)), with a virtual
_isValidSupport hook each ruleset implements. The struct silently made D13
("the counting layer every ruleset shares") false for Bond, whose No+Slash is
a fourth support value (Nexus 8, frozen scope) — it could not have reused this
base without a storage-layout change. StandardRuleset keeps the Bravo-shaped
proposalVotes triple for OZ tooling.
2. tally(proposalId, support) is the per-support accessor the frozen vector ABI
requires (spec v1 §4, IStandardRulesetVector). Without it the mutable-vote
vectors cannot read tallies, so the milestone's own differential could not run;
it also closes the observability divergence recorded in the milestone-1 verdict.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Audit-panel finding (Medium): under mutable votes an outstanding pre-signed ballot a voter handed a relayer can be submitted AFTER they change their mind and vote directly, overriding that vote — a supersede a third party controls by timing. OZ spends the EIP-712 vote nonce only on the bySig paths, so a direct cast left outstanding signatures live. The three direct castVote* entry points now _useNonce(voter) before delegating to super, so acting directly invalidates any outstanding signed ballot — the governance analogue of Seaport incrementCounter / Permit2 invalidateUnorderedNonces. The research (docs/research/2026-07-14-revote-signature-ordering.md) found no governor combining OZ + gasless + re-voting had solved this. Account-global variant (chosen): a direct vote invalidates the voter's pending vote-signatures across all open proposals, not just the one voted on. The per-proposal keyed-nonce alternative (GovernorNoncesKeyed) was rejected for this PR — it needs OZ's un-keyed fallback killed and the ENS relayer's signing scheme migrated off-chain. First core change of the milestone; amends the "core untouched" property. Tests pin the fix (outstanding sig cannot override a direct vote), the accepted cross-proposal invalidation, and the still-foreclosed used-signature replay. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Audit-panel finding (guidelines HIGH): the "tallies are non-monotonic; do not arm one-shot state on a tally crossing" constraint lived only on the concrete base's natspec, but a Nexus 3 anti-snipe consumer binds against IRuleset — the interface was silent. Adds the note to IRuleset.quorumReached/voteSucceeded, phrased as "MAY be non-monotonic" (an immutable-vote ruleset is monotonic, so it is not an interface-wide guarantee either way). No ABI change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…den tests Audit-panel cleanups (all low, no behavior/ABI change): - Split the public validated tally(id, support) from an internal unchecked _tally(id, support). quorumReached/voteSucceeded/proposalVotes pass constant, known-valid supports, so they now use _tally and skip the redundant _isValidSupport dispatch on the hot outcome-evaluation path (queue/execute). Three tools converged on this. - Declare _isValidSupport `pure` (was `view`): an override now physically cannot read storage, so it can't make tally/countVote state-dependent or break the unknown-id no-revert contract. Its natspec also states the consumption obligation — every accepted support must be read by the ruleset's quorumReached/voteSucceeded, or its weight is silently dropped from outcomes (the Bond No+Slash footgun). - Add test_countVote_rejectedRevote_leavesStandingVoteIntact: a re-vote that reverts (invalid support / weight overflow) must not partially mutate the standing vote. - Rewrite the conservation fuzz oracle to reconstruct expected tallies from the INPUT sequence instead of the contract's own receipts, making it an independent oracle rather than an internal-consistency check. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…(D21) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The integrator notes were written in imperative second person (before
…it (D22-D26) Max N concurrently live (Pending|Active) proposals per proposer, lazily pruned inside _proposeWithType — the single ProposalCore-writing chokepoint, so every creation door present or future runs the check and records the id (D22). Liveness is a positive whitelist: Queued does not occupy a slot and Canceled/Defeated/Executed free theirs immediately — a concurrency cap, not a rate limit (D23). The cap is governance-settable within 1..MAX_ACTIVE_PROPOSALS_CEILING (10): zero would revert every propose including the fix proposal (the config-self-brick class), and the ceiling makes the prune's O(cap) bound explicit (D24). Deploy value 2 per the RFC (ENSParams). Liveness never dispatches to a ruleset: past-deadline ids settle on proposalDeadline alone and state() is consulted only within the deadline, where it resolves purely from core storage — a ruleset with poisoned views cannot brick its proposer's next propose, preserving the Nexus 1 adversarial containment property (D26). Prior art: Bravo/Nouns latestProposalIds (cap=1 induction breaks at N>1), Moonwell maxUserLiveProposals (EnumerableSet desync brick risk), Snapshot-X ActiveProposalsLimiter (rate window, no liveness) — see docs/research + spec 2026-07-16-nexus4-spam-limit (local docs). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…(D27, D29) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A castVoteBatch loops the internal _castVote, bypassing the public castVote* overrides where D21 spends the nonce — without this a batch left outstanding relayer ballots alive, reopening the Nexus 2 audit-panel Medium. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ilures (D29) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…D27) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…spend Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ents Comments should document the code, not index an external spec. Removed "(spec D22-D26)"/"§5.x" tags from the spam-limit comments added in the prior commit — the reasoning each comment states already stands on its own without pointing at a document that isn't in this repo. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…h; guard-order natspec Findings 1-3 (Minor) from the milestone's final whole-branch review. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The `_activeProposals` comment stated length can never exceed the live `_maxActiveProposals` — false the instant governance lowers the cap while a proposer holds more live proposals than the new value. The always-true bound is the immutable `MAX_ACTIVE_PROPOSALS_CEILING`; reworded to state that precisely and note the cap-lowering behavior doesn't retroactively prune. Also adds a README section for the Nexus 4 spam-limit mechanism (semantics, cap range, Sybil caveat) and lists its test suite in the Layout table, since it had zero README coverage. Found by the audit panel (ToB code-maturity, ToB guidelines-advisor, code-review all independently converged on the invariant-comment gap). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comments keep the why (all-or-nothing, nonce spend, last-wins) but no longer point at milestone names, ClickUp ids, or decision-record codes that live outside the repo (docs/ is gitignored). The ENS RFC reference stays — it is a public external document. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…amsBatch OZ's cast* names announce the full parameter surface (castVoteWithReason, castVoteWithReasonAndParams, ...BySig); the batch entry point takes reasons and params, so its name should say so. Also keeps the short castVoteBatch name free for a lean convenience overload if one is ever wanted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RFC §2.6: a proposal observed failing at any point inside the final 24h that would pass at the original deadline gets voting extended once, by 48h anchored at the original deadline (never at flip time — closes F4). The trigger is a window low-water mark, not a one-shot slot: both stored bits are protection-monotone, so mutable-vote oscillation cannot burn the extension (closes F2 per the D16 boundary contract — no state armed on a tally crossing). Observation rides the cast path (pre-count in _castVote, post-count in _tallyUpdated, sig paths included); the extension materializes lazily on the first post-deadline cast, emitting the OZ GovernorPreventLateQuorum ProposalExtended ABI. Reads dispatch to the pinned ruleset's quorumReached && voteSucceeded, so every proposal type gets the mechanism under its own semantics (D36). Params are constructor immutables (24h/48h in blocks for the ENS deploy); registerType now requires votingPeriod > extensionWindow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The live ENS governor decides at the original deadline no matter when the outcome flipped; GovernorNexus extends by 48h from that deadline. Asserted side by side on the mainnet fork, same flip block on both. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
README gains the Nexus 3 section (mechanism, integrator notes on the lazy deadline view and the materialization-time event) and layout rows; the IRuleset non-monotonicity note now points at the late-flip extension as the reference D16-compliant consumer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Strip milestone, decision-record and internal-spec references (D-numbers, spec section pointers) from comments introduced by the late-flip work — provenance lives in the spec docs, comments explain only the code they sit on. No behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
refactor: quality pass — quorum-fraction mixin, bond struct/tuple cleanup, sweep leftovers (QUAL-15/16/18/21)
refactor(validator): pass the governor-computed proposalId through IProposalValidator (QUAL-17 + QUAL-19)
fix(cancel): bar cancel in the propose block, reject zero votingDelay (LEAD-10)
feat(registry): registration-time guards — ruleset binding + zero quorum numerator
fix(bond): apply the EP 5.15 ratified slash predicate verbatim (raw tallies, no per-address exclusion)
fix(governor): keep _isLive ruleset-free so a poisoned ruleset can't brick propose (SEC-1)
The bond is an anti-spam instrument — surviving the vote fulfills its purpose, so the refund no longer waits for execution. The timelock-veto forfeit becomes best-effort: it reaches only bonds still unsettled when the veto lands. Product decision 2026-08-03 (spec D64, amends D61/D57). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Succeeded, Queued, and Executed all map to SlashReason.None; one condition states the doctrine (refund from Succeeded onward) instead of two branches returning the same value. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
feat: bond refunds in Succeeded/Queued (D64)
…alidators, ERC-1271 coverage Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per-proposal vote-signature nonce
- regenerate the gas table from GasBench at the pinned block (propose 138,778 / castVote 135,842; queue and execute unchanged) - validateProposal signature: the hook receives the governor-computed proposalId first; the layout row carried descriptionHash from the pre-proposalId interface - self-cancel: 'always' was false — cancel is barred in the propose block; document the bar and why it exists - layout table: add RulesetQuorumFraction, the batch and voteNonce suites, and the FeeOnTransferToken mock; retitle the page Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Source files already carried MIT SPDX headers with no license text at the root. SECURITY.md gives researchers a private disclosure path (shared@blockful.io) ahead of the external audit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
No+Slash predates the option's final name; also reword one bond-suite pin comment that referenced an internal review id. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BOND_AMOUNT (1,000 ENS, EP 5.15) and VETO_THRESHOLD (500k ENS) were quoted in the README but existed nowhere in src; ENSParams is the single source of truth, so they live here. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Internal-process wiring, and it triggered on a branch that is being retired. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
chore: pre-announcement cleanup — stale README claims, license, security policy
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.
No description provided.