Skip to content

Dev - #25

Merged
LeonardoVieira1630 merged 147 commits into
mainfrom
dev
Aug 19, 2026
Merged

Dev#25
LeonardoVieira1630 merged 147 commits into
mainfrom
dev

Conversation

@LeonardoVieira1630

Copy link
Copy Markdown
Member

No description provided.

LeonardoVieira1630 and others added 30 commits July 14, 2026 10:56
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>
LeonardoVieira1630 and others added 29 commits July 28, 2026 16:12
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>
- 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
@LeonardoVieira1630
LeonardoVieira1630 merged commit bd38238 into main Aug 19, 2026
8 checks passed
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.

3 participants