Skip to content

V12 audit round 2: assess all findings, fix the 32 confirmed - #647

Merged
n13 merged 9 commits into
mainfrom
v12-audit-round-2
Aug 8, 2026
Merged

V12 audit round 2: assess all findings, fix the 32 confirmed#647
n13 merged 9 commits into
mainfrom
v12-audit-round-2

Conversation

@n13

@n13 n13 commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

V12 Audit Round 2 — assessment & fixes

Second V12 autonomous audit run (2026-08-06) produced 226 findings. Of these, 147 were triaged invalid by V12 itself (listed collapsed at the bottom). We independently re-assessed the remaining 79 against main at HEAD — not trusting the tool's triage — and reached our own verdict on each.

Our verdicts on the 79 tool-"valid" findings

Verdict Count Meaning
✅ Fixed 32 Genuine defect confirmed in current code; fixed in this PR
❌ Invalid 42 Not a real/material defect on our code — reasoning below
♻️ Obsolete 5 Referenced code was removed/changed after the audit (mainly the wormhole-turnstile removal in #645)

Weight-metering fixes below adjust benchmark code and hand-tune weights.rs conservatively in the existing style. Official weights must be regenerated on the reference benchmark machine before release.

Fixed (32)

# Sev Title What we changed
181308 critical Root quorum collapses when collective membership is undersized Grew all tech-collective seeds to 5 members (the size the 60/61% thresholds assume) and added a fail-early genesis floor rejecting a non-empty seed < 5; documented the removal-path convention in definitions.rs. (e37cd824)
180560 high Approval Can Lose Enactment schedule_enactment now returns a Result; a referendum is committed Approved only when enactment scheduling succeeds, otherwise it stays Ongoing with a retry alarm so the call/origin are never discarded. (af122ee3)
181329 high Failed queue admission permanently removes referendum wake-up When the bounded track-queue insert fails, ready_for_deciding now returns a NotQueued branch that arms the undeciding-timeout alarm instead of leaving the referendum with no wake-up. (af122ee3)
181346 high Stale tallies are evaluated after all eligible voters are removed Tally::support returns Perbill::zero() when the electorate is empty (was 100%) and clamps ayes to the member count so a shrunken electorate can't exceed 100%. (5e74e221)
181347 high Failed alarm replacement can strand ongoing referenda ensure_alarm_at now schedules the replacement alarm before cancelling the old one and keeps the existing alarm if scheduling fails, so a vote/service can't destroy a working alarm. (af122ee3)
181395 high Alarm scheduling failure can strand deciding referenda Added rollback paths: if a deciding referendum can't be re-armed after agenda exhaustion, its deciding slot is freed and a best-effort timeout alarm is set instead of holding the only slot forever. (af122ee3)
181418 high Queue eviction leaves referenda permanently marked as queued insert_sorted_by_key now returns the evicted entry; both eviction sites clear the displaced referendum's in_queue and re-arm its timeout alarm, with the extra read/write charged in the queued/requeued weights. (af122ee3)
181299 high Terminal scheduler removal can permanently freeze scheduled funds cancel_transfer makes the scheduler cancel_named call best-effort (logs on failure, no longer propagates), so a already-removed task can no longer roll back the funds release and freeze held funds. (ccbbc7a8)
180135 high Stale Concurrent Fork Choice Added a shared async import lock so the read-best-work → fork-choice → store → import section is atomic across the concurrent import-queue and mining-worker clones. (a48bc7ff)
181313 high Genesis difficulty is not validated before first-block consensus genesis_build now asserts initial_difficulty is within [min, max) (rejecting zero) using the existing bound helpers. (a48bc7ff)
181437 high Fail-open cumulative-work reads corrupt fork-choice baseline The two get_chain_work reads in import_block now propagate errors (fail closed) instead of substituting a zero fork-choice baseline. (a48bc7ff)
181438 high Timestamp-derived difficulty retargeting lets proposers lag time and manipulate difficulty Floors the block-time delta used in the difficulty retarget (MIN_RETARGET_BLOCK_TIME_MS), chosen below the production divisor so it's a no-op for legitimate targeting and cannot stall fast blocks. (a48bc7ff)
181332 high Root origin rewriting bypasses the high-security whitelist dispatch_as/dispatch_as_fallible now enforce HighSecurity::is_call_allowed when the rewritten origin is Signed, matching as_derivative; Root/None/other origins unchanged. (e37cd824)
181327 medium Failed timeout scheduling creates unserviceable referenda submit returns a new AlarmSchedulingFailed error when the timeout alarm can't be scheduled, so the transactional extrinsic rolls back the reserve/counter/scheduler writes instead of creating an unserviceable referendum. (af122ee3)
181394 medium Terminal queue entries retain bounded governance capacity cancel and kill now remove the referendum from TrackQueue when it was still queued, freeing bounded governance capacity; the extra read/write is charged. (af122ee3)
181396 medium Manual nudges orphan scheduled referendum alarms nudge_referendum clears the stored alarm only when it matches the current block, otherwise cancels the orphaned scheduler task instead of leaking it. (af122ee3)
181227 medium Lookup Tasks Omit Retry Storage Weight The lookup branch of MarginalWeightInfo::service_task now composes service_task_base, charging the unconditional Retries read+write that was only counted on the base path. (b23089e3)
181353 medium Missing proposal migration strands pre-upgrade state Added a VersionedMigration (v0→v1) that translates each Proposals record to the new layout (dropping call_weight), registered it in the runtime, and updated the README. (d61aedba)
181294 medium Depth-dependent proof hashing is missing from transfer weight execute_transfer_weight now adds the depth-dependent Poseidon ref-time (insert_leaf_hash_ref_time_at_depth) from the live tree-depth snapshot, mirroring mining-rewards. (ccbbc7a8)
181295 medium Transfer proof weight omits depth-dependent tree hashing per_transfer_weight takes one tree-depth snapshot and adds the depth-dependent Poseidon ref-time alongside the DB ops so post-dispatch reconciliation charges the full unit. (ccbbc7a8)
181386 medium Reject self-guardians in genesis enrollment GenesisConfig::build now rejects a self-guardian high-security row (guardian == who), matching the signed set_high_security invariant. (ccbbc7a8)
181268 medium Planck treasury signers under-funded for multisig operation Raised the Planck treasury-signer genesis seed from 1 to 3 UNIT so a signer can cover the ~2.63 UNIT create+propose cost. (e37cd824)
180119 medium Major Sync Can Leave Mining Permanently Idle Add a sync-completion transition that re-triggers a build (e.g. detect is_major_syncing going false and push a build request), or have mining_loop request a build when metadata is None and not syncing. No audit patch supplied. (a48bc7ff)
181245 medium Mining seal submission not gated by major-sync oracle Thread the SyncOracle into MiningHandle and have submit return false (skip) when is_major_syncing(). Audit patch (hasPatch) doing this is usable but low value given existing mitigations and negligible impact. (a48bc7ff)
181258 medium Finalization Violates Maximum Reorg Window Use finalize_depth = max_reorg_depth (finalize best - max_reorg_depth, keeping exactly max_reorg_depth blocks reorganizable) to align enforcement with the configured value. Audit patch (hasPatch) removing the -1 is usable if the intended window is exactly MaxReorgDepth. (a48bc7ff)
181374 medium Suppressing finalization failures disables reorg-depth protection Escalate the failure (log at error + metric) and consider gating further imports only on repeated/persistent failure; do NOT blindly propagate/halt on the first error as the audit patch (hasPatch) implies, since halting all imports on a transient finalization hiccup harms liveness. The patch is therefore questionable as-is. (a48bc7ff)
181403 medium Failed block imports leave the miner without a candidate On import failure in submit, either restore the taken build or explicitly request a rebuild (push the current best into the pending_build channel) so a fresh candidate is produced without waiting for an external trigger. No audit patch supplied. (a48bc7ff)
181339 low Benchmark development genesis leaves technical collective unseeded The runtime-benchmarks genesis branch now re-inserts the stripped tech_collective_seed_members key so the benchmark dev chain actually seeds the collective. (e37cd824)
181393 low Scheduler exhaustion leaves freed deciding slots unserviced Factored one_fewer_deciding into a helper and call it from the exhaustion fallback so a freed deciding slot promotes a queued referendum instead of only decrementing the count. (af122ee3)
181231 low Unavailable Work Causes Permanent Task Loss Terminal Unavailable/PermanentlyOverweight outcomes now count as serviced work so a following task isn't misclassified and permanently deleted on the same cycle. (b23089e3)
180118 low Version Check Does Not Bind Mining Snapshot Read version() before snapshotting metadata in handle_local_mining so a concurrent rebuild invalidates the stale post-search version check. (a48bc7ff)
181263 low Active Pallets Missing From Benchmark Registry Registered the six active-but-missing pallets (TransactionPayment, Preimage, Utility, TechCollective, TechReferenda, Recovery) in define_benchmarks! and added the missing pallet-utility/runtime-benchmarks feature. (e37cd824)

Assessed invalid (42)

# Sev Title Why we judged it invalid
181341 high Unbounded submissions can monopolize the governance queue Queue entry requires the 1000 UNIT decision deposit plus the 100 UNIT submission deposit per referendum (place_decision_deposit gates ready_for_deciding), so filling 100 slots costs ~110k UNIT locked by an already-trusted collective member, and the queue is keyed by ayes with force_insert_keep_right evicting the LOWEST key — a legitimately supported proposal (2 ayes vs the attacker's max 1) always displaces attacker entries and reaches the queue head. The genuine defect in this area is the stranding on failed/evicting insertion (181329/181418), not the absence of a per-submitter quota.
181349 high Register the referenda v0-to-v1 storage migration The referenda pallet in this repo (and the upstream crate it was forked from) has always declared STORAGE_VERSION 1 and always used the v1 Option layout in ReferendumInfo (types.rs 366-375), so no v0-encoded terminal records can exist on this chain. Registering MigrateV0ToV1 would be a no-op at best, and actively harmful if the on-chain version marker were 0 while the data is v1 — the migration would iterate v0::ReferendumInfoFor and mis-decode/skip live v1 terminal records.
181358 high Omitted upgrade migration strands legacy preimages No V0 preimage state can exist on this chain: the runtime's first-ever preimage dependency was pallet-preimage 39.0.0 (commit 682319c), which already uses the tuple-keyed PreimageFor/RequestStatusFor layout, and the inlined fork (commit 3fc401f) kept STORAGE_VERSION=1; nothing in this repo's history ever wrote hash-keyed v0 PreimageFor or StatusFor entries (only migration.rs:146 and test mocks write StatusFor). Registering migration::v1::Migration in the Migrations tuple (runtime/src/lib.rs:169-174) would drain empty maps and strand nothing.
181439 high Lazy legacy-status conversion fails non-atomically and reports success on ticket failure The non-atomic take/unreserve-before-Consideration::new pattern does exist in do_ensure_updated (pallets/preimage/src/lib.rs:283-328, inherited verbatim from upstream), but it is unreachable: the deprecated StatusFor map has never had a writer in this chain's history (this fork started on pallet-preimage 39.0.0; only the never-registered migration and test mocks insert into it), so StatusFor::take(h) always returns None and the function returns false before any destructive step. The fee-free ensure_updated credit likewise can never be earned.
181355 high Initialize treasury multisigs at genesis Factually true that genesis derives treasury addresses without inserting Multisigs entries (genesis_config_presets.rs), but this is deliberate and harmless: create_multisig is permissionless and the address deterministically binds signers/threshold/nonce, so no one can hijack it, treasury funds accrue regardless, and the Planck preset explicitly seeds each treasury signer 1 UNIT (signer_fee_seed) to pay the 0.6 UNIT MultisigFee for post-genesis registration. One routine follow-up transaction is not a high-severity custody issue.
181281 high Producer-Controlled Clock Breaks Transfer Delays check_inherent (pallets/timestamp/src/lib.rs:302-327) is the unmodified upstream Substrate model: lower bound prev+MinimumPeriod, upper bound local+30s, and create_inherent uses max(local, prev+min), so any honest block snaps the clock back to wall time — sustained slow-clock deferral requires winning essentially every block (majority hashpower), and forward manipulation is hard-capped at 30s beyond every validator's local clock regardless of block count, immaterial against the default 1-day delay (DefaultDelay = DAYS, runtime/src/configs/mod.rs:491). MinimumPeriod=100ms (configs/mod.rs:171) is a deliberate PoW choice; the audit's fix (min advance = 12s target) would push consensus time ahead of wall time during fast PoW block streaks and risk TooFarInFuture rejections/stalls.
181361 high Default genesis permits missing treasury state and halts finalization The behavior exists at HEAD exactly as described (all-None treasury genesis is a no-op at pallets/treasury/src/lib.rs:105-127, account_id()/portion() expect at :195-204, and mining-rewards on_finalize unconditionally calls T::Treasury::portion() at pallets/mining-rewards/src/lib.rs:160), but it is a deliberate, documented design (commit 4f25885 'stop defaulting the treasury account to the keyless minting sentinel'; in-code comments state the all-None default writes nothing so a spec that forgot the treasury 'fails loudly' on first use, because FRAME/tooling requires RuntimeGenesisConfig::default() to build). Half-configured treasury JSON is already rejected at genesis build, all three shipped presets set treasury, and the only pass-through case (raw no-preset default genesis) halts deterministically at the very first block of a value-less chain with a self-explanatory panic — an operator-misconfiguration footgun that fails loudly as intended, not a high-severity vulnerability; the audit's patch (reject the all-None default at build) would break the default-genesis buildability the design deliberately preserves.
181440 high Balances genesis builder uses unchecked aggregate issuance The unchecked fold exists verbatim at HEAD (pallets/balances/src/lib.rs:585) and Cargo.toml release/production profiles lack overflow-checks, but this is stock upstream pallet_balances behavior over fully trusted input: the genesis author already controls every balance and storage item on the chain, so 'crafting' endowments summing past u128::MAX (~3.4e26 tokens at 12 decimals) is self-sabotage, not an attack. Downstream consumers handle supply above MaxSupply gracefully (mining-rewards uses saturating_sub yielding zero rewards, pallets/mining-rewards/src/lib.rs:146), and the cited Wormhole propagation path is gone at HEAD — the migration seeding PotentialWormholeBalance from total_issuance was replaced by MigrateV1ToV2 which deletes the counters (commit 29cc9b4).
181375 high Unsafe provisional work updates corrupt fork-choice metadata The described corruption modes are not reachable. Upstream sc-consensus calls check_block before import_block, and PowBlockImport::check_block (lib.rs:219) delegates to the client which returns AlreadyInChain (client.rs:1783-1785) for known blocks, so replayed/known blocks never reach the pre-import store at lib.rs:348. When AlreadyInChain is returned from inside import_block it is Ok (no rollback runs), and that only happens in a rare race where the pre-import write used correct (non-deleted) parent work. For genuinely new blocks there is no prior entry to overwrite, so delete-on-error correctly removes only the just-written value. Sibling finding 181437 itself retracts the replay concern.
181401 high Accept non-canonical QPoW digest structures Digest processing is deterministic and identical on every node. The block hash commits to the whole digest and the DIGEST_LOGS_SIZE bound is enforced (lib.rs:237-240), and extract_author_from_digest (primitives/wormhole/src/lib.rs:122-151) deterministically takes the first 32-byte POW PreRuntime item (or None). When None, mining-rewards mint_reward(None,...) sends the reward to treasury (pallets/mining-rewards/src/lib.rs:165,193-199,220-228). A non-canonical digest therefore only causes the block's own miner to forfeit/misattribute their own reward to treasury; there is no cross-node divergence and no attack on others. The 'incompatible acceptance' premise assumes a stricter external implementation that does not exist.
181392 high Stale recovery attempts can restore revoked proxy authority The code description is accurate (claim_recovery at pallets/recovery/src/lib.rs:650-683 leaves ActiveRecoveries in place; cancel_recovered at :786-798 removes only Proxy), but this is unmodified upstream pallet_recovery semantics and grants no authority the rescuer did not already legitimately hold: re-claiming only restores a proxy the rescuer voluntarily gave up, the lost account can invalidate the attempt at any time via close_recovery (:698-748), and set_recovered (:468-494) is documented as a Root grant primitive, not a revocation one — Root has no revoke path here either way and can already reach the same state via System::set_storage. Additionally, the implied fix (consuming ActiveRecoveries on claim) would orphan the rescuer's reserved RecoveryDeposit, since close_recovery is the only path that repatriates/unreserves it (:706-742, reserved at :574-576).
180587 medium Stale Votes Control Queue Promotion A vote calls try_access_poll, which schedules the referendum's own alarm at now+1 (lib.rs 817), so the queue key is refreshed within one block via the RequeuedSlide branch; the worst case is that a promotion executed in that same block uses a key one block stale. That is a best-effort ordering property (identical upstream) affecting only which of two live referenda starts deciding first — each still has to pass on its own tally — not a safety or authorization defect.
181340 medium Unbounded genesis member list exhausts builder resources The tech_collective_seed_members array comes from the operator's own chain spec passed to the GenesisBuilder runtime API, not from any on-chain or attacker-controlled input, and serde_json::from_slice in prepare_genesis_build_input has already materialized the whole array before parse_tech_collective_members_array allocates — so the Vec::with_capacity(arr.len()) adds no amplification and there is no DoS surface. MaxMemberCount is still enforced by do_add_member before any state is committed.
181348 medium Rank queue by weighted approval instead of bare ayes In this runtime PromoteOrigin/DemoteOrigin are NeverEnsureOrigin and MinRankOfClassConverter always returns 0 (configs/mod.rs 245-254, definitions.rs 169-174), so every member is permanently rank 0 and Linear vote weight is 1 — weighted ayes and bare_ayes are identical, as docs/TECH_COLLECTIVE_GOVERNANCE_TUNING.md states. Switching the queue key to approval() would also fold nays into an ordering key that upstream deliberately keeps as an aye count, changing behaviour with no benefit here.
181397 medium Cancellation weights do not model deciding-state work The unbenchmarked work on the deciding branch of cancel/kill is note_one_fewer_deciding's single scheduler agenda read plus one write (or one DecidingCount write on the fallback), while the flat surcharge alarm_retry_weight (branch.rs 34-38) already adds 16 DB reads — several times more ref_time than the missing component. Charging non-deciding branches the same surcharge is over-, not under-metering, so there is no material weight gap.
180231 medium Corrupt records are destructively discarded The destructive drain-before-validate behavior exists in pallets/preimage/src/migration.rs:105-151, but it is stock upstream code that is not registered in the runtime's Migrations tuple (runtime/src/lib.rs:169-174) and can never encounter data: this chain has no V0 preimage records (it never ran pre-V1 preimage code) and the migration self-skips unless the on-chain storage version is 0. Hardening never-run dead code against corruption of nonexistent state is not a material defect.
181324 medium Reject zero timestamp bucket configurations TimestampBucketSize is a compile-time constant set to 24000ms (runtime/src/configs/mod.rs:149) with no on-chain path to change it; a zero value requires a developer misconfiguration shipped in a runtime build. The zero-precision behavior is already documented as a caller invariant in normalize (primitives/scheduler/src/lib.rs:64-66), and the finding itself concedes it is a latent-configuration concern, not reachable through the current constant. Adding an integrity_test is defensive hardening, not a defect fix.
181359 medium Lazy preimage conversion is not charged by direct dispatches The lazy-conversion branch of do_ensure_updated can never execute on this chain (the legacy StatusFor map has no writer in the repo's entire history, so the take at pallets/preimage/src/lib.rs:285 always returns None), and the one cost that IS reachable — the single StatusFor read — is already included in the note_preimage benchmark ('Preimage::StatusFor (r:1 w:0)', pallets/preimage/src/weights.rs:93-94). No caller can make direct preimage dispatches exceed their declared weight via this path.
181434 medium Trait scheduling helpers mishandle bounded-call preimage ownership The unconditional request in the trait shims is a documented, deliberate V12 #162453 decision (pallets/scheduler/src/lib.rs:791-795, 924-926), and for every actual trait caller in this runtime the accounting balances: reversible-transfers always bounds a 34-byte inline call (no lookup hash), and referenda passes either inline nudge calls or user-noted proposals whose status is owner-ticketed — for those, request+terminal-drop is symmetric and a failure-path drop degrades losslessly (unrequest at count==1 with an owner ticket reverts to Unrequested keeping the payload; on an Unrequested status it is a no-op). The harmful combinations (a system-noted bound Lookup double-requested, or destroying an ownerless Requested count) require a ManagerOrigin/root-noted proposal shared across consumers plus a placement failure — unreachable through any realistic path in this runtime.
180106 medium Oversized approvals are under-metered approve pre-charges approve(MaxCallSize) at inclusion, so block packing uses the max; the post-dispatch refund based on proposal.call.len() (lib.rs ~line 773) omits only the caller-supplied input bytes, whose cost is an O(n) BoundedVec decode covered by the transaction length fee, plus an O(1) length-mismatch compare - at the measured 332 ps/byte the entire omission is ~3.4us at 10KB. Not a materially exploitable under-metering.
180526 medium Threshold-one proposal path is unbenchmarked True that both propose benchmarks hard-code threshold=2 (benchmarking.rs lines 203, 231) and the threshold-1 path additionally emits ProposalReadyToExecute (lib.rs ~line 697), but the unmeasured extra work is a single event deposit (~1us) against a 34us+ base with 2 reads/2 writes - immaterial to weight calibration and not a viable resource-exhaustion vector.
181010 medium Runtime Upgrade Can Reinterpret Approved Calls Accurately describes the code (ProposalData stores raw SCALE bytes, execute re-decodes against the current RuntimeCall, lib.rs ~lines 104-118, 1122), but this is the standard Substrate pattern (scheduler/preimage store encoded calls identically); reinterpretation requires a privileged runtime upgrade that reorders call indices - controlled by the chain's own governance - and execute already re-validates weight and HS policy at dispatch. A design/operational note (drain or cancel proposals across index-changing upgrades), not a vulnerability.
181331 medium Undercharge proposal-byte work during batch cleanup FRAME benchmarking pins non-varying components at their maximum, so the c coefficient in claim_deposits (14,255 ps/byte, weights.rs line ~203) was measured with i=200 proposals each carrying c bytes - it already embeds the aggregate per-proposal byte work (~71 ps/byte/proposal x 200), meaning the refund claim_deposits(i, r, avg) matches actual cost at i=200 and overcharges for i<200; pre-dispatch also reserves the absolute worst case (200, 200, 10240). No undercharged i*c interaction exists in the reachable domain.
181253 medium Scheduling Worst Cases Are Underweighted schedule_transfer's charged weight already includes the Agenda and PendingTransfersBySender reads/writes and full worst-case PoV (Estimated 13483 including the 10018-byte 50-slot Agenda MaxEncodedLen, pallets/reversible-transfers/src/weights.rs:117-125); the only omission is the ref-time of push_to_agenda's occupancy scan and re-encode of a <=40-entry (~8KB) agenda plus extending a <=15-entry pending vec (pallets/scheduler/src/lib.rs:736-767), tens of microseconds against ~800us total charged — a real but immaterial benchmark-coverage gap.
181255 medium Cancellation Ignores Bounded Vector Worst Cases cancel() already charges 7 reads/7 writes plus the full-agenda worst-case PoV (Estimated 13483 including the 10018-byte Agenda, pallets/reversible-transfers/src/weights.rs:142-150); the uncovered work is retain over <=16 hashes plus decode/re-encode of a <=50-entry agenda in do_cancel/cleanup_agenda (pallets/scheduler/src/lib.rs:850-877, 771-783) — tens of microseconds vs ~950us charged. The proposed conservative-multiplier fix would materially overcharge honest guardians for an immaterial delta.
180142 medium Peer Announcements Bypass Inherent Checks skip_execution_checks() correctly gates the inherent check because a skip_execution block is imported without applying state. chain_sync.rs:1450 sets skip_execution:true with state:None, which upstream maps to StateAction::Skip: the client applies no storage changes, so there is no execution and no state to diverge — skipping the inherent check (which only validates execution-time inherents) is consistent. The other skip_execution_checks() case (state-sync ApplyChanges::Import) verifies the state root against the header (client.rs:653-658). Inherents are re-validated whenever the block is later imported with execution (StateAction::Execute, skip_execution_checks()==false → check runs). Header-only imports likewise apply no state. This is standard, safe Substrate behavior.
180145 medium Configurable Height Skips Inherent Validation Production wiring passes check_inherents_after = 0 (node/service.rs:641), and check_inherents (lib.rs:169) only bypasses when block.number() < threshold; for an unsigned block number, number < 0 is never true, so no block is ever skipped in the shipped config. The parameter mirrors upstream sc-consensus-pow's check_inherents_after knob; the described gap requires an operator to deliberately configure a nonzero threshold and is not a defect in the shipped code.
181380 medium Allowing startup without the genesis work baseline Startup init is non-fatal (service.rs:604-606) and a missing genesis baseline makes the first child record achieved_difficulty instead of one+achieved_difficulty (lib.rs:293-300), but the resulting offset is a uniform -1 applied to EVERY block's cumulative work on the node. Fork choice (is_heavier) only compares competing branches locally, all of which descend from the same genesis baseline, so the uniform offset cancels and does not change which chain is heaviest. initialize_genesis_achieved_work is idempotent (chain_management.rs:114-123), so no mixed 0/1 state can arise. The claimed fork-choice inconsistency does not materialize; the underlying fail-open read is already covered by 181437.
180484 medium Unbounded Benchmark Repeat Count BenchmarkConfig.internal_repeats (frame/benchmarking/src/utils.rs:210-224) is only reachable through impl frame_benchmarking::Benchmark<Block> for Runtime, which is gated by #[cfg(feature = "runtime-benchmarks")] (runtime/src/apis.rs:257-299), and the only caller is the node's own Subcommand::Benchmark CLI, itself gated by the same feature (node/src/cli.rs:69-72, node/src/command.rs:499-548). There is no production/RPC attack surface: the value is supplied by the operator running their own benchmarking build, so an operator DoS'ing their own local benchmark run is not a vulnerability; a runtime-benchmarks build is never a production artifact.
180300 low Corruption is silently worsened during removal remove_from_rank (lib.rs 766-782) is only reachable through do_demote_member/do_remove_member_from_rank, both of which run ensure_member first, and it does return Error::Corruption when IdToIndex or the swap target is missing. The residual concern (not re-verifying IndexToId(rank, last_index) == who when index == last_index) only misbehaves on storage that is already corrupt, which no code path can produce; this matches upstream and is a defensive nitpick.
180590 low Unvalidated Queue Callback Breaks Count one_fewer_deciding (lib.rs 687-710) is ensure_root, and Root on this chain is only reachable through a passed tech referendum — an origin that can already set storage arbitrarily. Within the pallet's own flow the callback is only ever scheduled by note_one_fewer_deciding for a referendum that just left deciding, next_for_deciding drains non-ongoing entries, and a deciding referendum is never left in TrackQueue (its entry is popped on promotion), so the described count corruption requires deliberate Root misuse.
181328 low Report and handle partial referendum deposit slashes slash_deposit (lib.rs 1417-1422) can only under-slash if the account's reserved balance is below the deposit the pallet itself reserved in take_deposit, which cannot happen under the runtime's invariants without external tampering (Root force_unreserve or a balances bug); the imbalance actually handed to T::Slash is always the real amount, so only the event's amount field could be cosmetically wrong in an already-corrupt state. Matches upstream behaviour.
181419 low Reserve shortfalls permanently erase refundable deposit markers refund_deposit (lib.rs 1410-1414) discards unreserve's residual, but a nonzero residual requires the account's reserved balance to be below what this pallet reserved for that exact deposit — impossible without external tampering, since take_deposit reserves it and only these refund/slash paths release it. Same class as 181328: at most a cosmetic event amount in an already-corrupt state, identical to upstream.
181228 low Scheduling Weights Omit Preimage Work The scheduling weights indeed omit Preimages::bound work (pallets/scheduler/src/weights.rs:170-221 list only Timestamp/Agenda/Lookup), but the concern is immaterial: the runtime excludes all scheduler dispatchables from RuntimeCall via #[runtime::disable_call] (runtime/src/lib.rs:229-231) and additionally gates ScheduleOrigin as EnsureRoot, so no caller can ever dispatch schedule*/cancel* and be metered by these weights; the internal trait-based schedulers (reversible-transfers, referenda) bound only small inline calls or carry their own weights.
181389 low Periodic rejection leaves bounded-call preimages orphaned The early PeriodicNotSupported returns (pallets/scheduler/src/lib.rs:1541-1543 and 1583-1585) do skip T::Preimages::drop, but the path is dead: the only v3 trait callers in the runtime are pallet_referenda, which always passes maybe_periodic=None (pallets/referenda/src/lib.rs:957 and 1011), and reversible-transfers uses the ScheduleNamed trait which has no periodic parameter at all. No dispatchable reaches these shims, so no bounded-call preimage can be orphaned through them; this is a consistency nitpick on unreachable code.
181326 low Proposal weights omit signer and proposal-map workload The propose benchmarks do fix 3 signers while production allows 100, but the unmeasured delta is decoding/encoding a Multisigs record bounded at 6892 bytes plus O(100) AccountId compares and map ops - a few microseconds against propose's 34us base plus 2 reads/2 writes of DB weight (~hundreds of us on RocksDB), with PoV already charged at MaxEncodedLen. Not a material under-metering; adding an s-component would be a polish item only.
181254 low Recovery Cancellation Slope Omits Full Agendas recover_funds already charges full-agenda worst-case PoV per transfer (12493 = 50-slot Agenda MaxEncodedLen, pallets/reversible-transfers/src/weights.rs:216) plus 3 reads/4 writes per n, and pre-dispatch charges recover_funds(MaxPendingPerAccount) (lib.rs:525); the only uncovered component is the ref-time to decode/re-encode a fuller (<=40 low-priority entries, ~10KB max) agenda inside cancel_named/cleanup_agenda — tens of microseconds against the ~540us charged per-transfer slope (64_986_364 ps + DB ops). Real but immaterial benchmark-coverage nitpick, not a viable liveness attack.
181256 low Execution Omits Full Pending List Cost The under-benchmarked work is list.retain(/id/ id != tx_id) over a BoundedVec of at most 16 32-byte hashes (pallets/reversible-transfers/src/lib.rs:714-716) whose <=561-byte storage read/write is already charged in execute_transfer's 5r/5w base (weights.rs:63-64); scanning 16 hashes is nanosecond-to-microsecond scale and cannot meaningfully exceed the charged weight.
180301 info Integrity checker panics and misses index corruption try_state_index (ranked-collective lib.rs 949-976) is behind #[cfg(any(feature = "try-runtime", test))] and never runs in a production block; an unwrap panic there fails try-runtime validation exactly as an Err would, so the 'panic instead of Corruption error' point has no on-chain consequence, and the missing reciprocal/density checks are a completeness wish for a debug-only checker, not a defect.
181399 info Benchmark fixtures omit active-proposal count state Real but immaterial fixture-fidelity note: insert_multisig/insert_proposal in benchmarking.rs leave proposals_per_signer empty, yet remove_proposal_and_return_deposit still performs the same Multisigs read+write (mutate) regardless, PoV is metered in MaxEncodedLen mode, and the skipped work is a BTreeMap get/decrement measured in nanoseconds against 20-30us bases plus DB constants. Produced weights are not materially affected.
181400 info Benchmark fixtures omit proposal deposit reservations insert_proposal fixtures for execute/remove_expired/claim_deposits do record deposit without reserving, but Currency::unreserve performs the identical account read/mutate whether reserved is zero or not, so DB ops and timing are unchanged; the cancel benchmark already reserves. No effect on produced weights, benchmark-only fidelity note.
181414 info Benchmark proposal fixtures omit active-count bookkeeping Duplicate of 181399: insert_multisig (benchmarking.rs line ~113, proposals_per_signer: BoundedBTreeMap::new()) and insert_proposal bypass the per-signer count bookkeeping, but the affected extrinsics still execute the same storage read/write pattern and PoV is charged at MaxEncodedLen; the compute delta (empty vs populated map ops) is negligible relative to the measured bases.

Assessed obsolete (5)

# Sev Title What changed since the audit
181321 high Treasury multisig registration can undercount Wormhole exit backing The wormhole turnstile was removed at HEAD (commit 29cc9b4): PotentialWormholeBalance, reveal_address, and NonWormholeAccounts no longer exist except as a v2 removal migration (pallets/wormhole/src/migrations.rs kills the counters), and create_multisig in pallets/multisig/src/lib.rs no longer invokes any reveal hook. The entire accounting mechanism the finding attacks is gone.
181388 high Mutable treasury exclusion undercounts Wormhole exit backing Commit 29cc9b4 removed the entire wormhole turnstile after the audit: PotentialWormholeBalance, TotalWormholeExits, the SoundnessInvariantViolation exit check, NonWormholeAccounts, and is_ambiguous_account no longer exist at HEAD (the only remaining references are in pallets/wormhole/src/migrations.rs, whose MigrateV1ToV2 deletes the counters from storage). process_exit_bundle (pallets/wormhole/src/lib.rs:758) mints exits with no backing/soundness comparison, and record_transfer (lib.rs:1248-1298) does no ambiguity classification, so a treasury reconfiguration can no longer undercount any pool or block exits.
180427 medium Proof Recording Work Is Under-Metered Commit 29cc9b4 removed PotentialWormholeBalance and the ambiguity classification entirely; at HEAD record_transfer (pallets/wormhole/src/lib.rs:1248-1298) only reads/writes TransferCount and inserts the ZK-tree leaf, and per_transfer_weight (runtime/src/transaction_extensions.rs:114-118) now charges 1+tree_reads/1+tree_writes covering exactly those ops, so the finding's cited missing pool read no longer exists. The residual claim (System event-storage writes) is contrary to FRAME-wide convention of not weighting deposit_event and is immaterial.
181296 medium Fresh-recipient proof recording is not fully weight-accounted The fresh-recipient path this finding prices no longer exists: commit 29cc9b4 removed nonce/NonWormholeAccounts classification and PotentialWormholeBalance from pallet-wormhole; at HEAD the recorder path (pallets/wormhole/src/lib.rs:1248-1298, 1309-1346) only reads/writes TransferCount and inserts the tree leaf, which execute_transfer's 5r/5w base plus depth-scaled tree ops (reversible-transfers/src/weights.rs:60-79) already covers.
181257 medium Reward finalization undercounts storage reads The reads the finding says are unmetered no longer happen: commit 29cc9b4 removed is_ambiguous_account, NonWormholeAccounts, and the PotentialWormholeBalance mutation from record_transfer (pallets/wormhole/src/lib.rs:1248-1298 now only touches TransferCount and the ZK tree), and rewrote the weight model in the same commit — pallets/mining-rewards/src/weights.rs now uses BASE_READS=12/BASE_WRITES=7 (fixed: CollectedFees r/w + TreasuryPortion r + TreasuryAccount r; per transfer: failed miner mint r1 + treasury mint r1w1 + TransferCount r1w1), which matches the current on_finalize path. The cited BASE_READS=15 model and the nonce/multisig/derivative/treasury classifier lookups do not exist at HEAD.
V12-triaged-invalid findings (147) — not independently reviewed (V12's own triage reasons are in the export JSON)
# Sev Title
180095 low Wrapped Counters Bypass Proposal Capacity
180109 low Unbounded transaction notification drain
180110 low Major sync does not invalidate candidate production
180111 low Best-hash check races candidate publication
180116 low Proposal parent is not bound to metadata
180117 low Oversized proposal digest reaches miners
180122 low Sync Clear Does Not Invalidate In-Flight Build
180126 low Unauthenticated Miner Listener Lacks Admission Limits
180157 low Duplicate replay claim is unconfirmed
180234 low Provider Unnote Deletes Requested Preimages
180235 low Provider Notes Under-Count Shared Requests
180245 low Partial Unreserve Is Silently Consumed
180393 low Zero Genesis Multiplier Removes Compute Fees
180426 low Stale First-Signature Balance Enables Excess Exits
180485 low Unbounded Benchmark Component Values
180496 low Benchmark silently accepts failed setup
180512 low Cleanup benchmarks use unreachable approvals
180513 low Cleanup benchmarks use unreachable approvals
180525 low Threshold-one proposal path is unbenchmarked
180527 low Create validation boundaries are unbenchmarked
180528 low Create validation boundaries are unbenchmarked
180543 low Inherent checker omits duplicate guard
180571 low Referendum Index Counter Wraps
180601 low Terminal referenda retain stale metadata
180614 low Queue-Eviction Alarm Placement Is Undercharged
180615 low Deferred Slot Release Fallback Is Undercharged
180618 low Missing-Track Failure Uses Timeout Weight
180620 low Retry Weight Arithmetic Is Bounded
180629 low Invalid Genesis Input Traps Builder
180673 low Recovery trusts stale sender index
180683 low Recovery weight bound matches processing
180684 low Recovery weight bound matches processing
180685 low Recovery weight bound matches processing
180686 low Recovery weight bound matches processing
180958 low Recovery Delay Error Describes Wrong Action
180959 low No confirmed recovery-flow finding
180960 low No confirmed recovery-flow finding
180963 low Partial Settlement Has No Reconciliation Path
180989 low Lifecycle documentation claims automatic execution
181004 low Nonce exhaustion strands proposal deposits
181041 low Unusable Accounts Can Fill Collective
181086 low Unset Timestamp Causes Difficulty Drop
181092 low Batched proofs undercharge tree growth
181104 low Unbounded genesis proof recording
181139 low Misrouted Benchmark Candidates
181140 low Misrouted Benchmark Candidates
181141 low No End-to-End Vote Tally Finding
181142 low No End-to-End Vote Tally Finding
181143 low No End-to-End Vote Tally Finding
181144 low Failed Induction Leaves Partial Membership
181145 low Unchecked Rank Leaves Orphaned Indexes
181198 low Recovery Reports Success With Armed Transfers
181237 low Failed-mint retries are redistributed to the next block's miner
181243 low Unchecked underflow in benchmark call-size loop
181246 low Stale cumulative-work aux entry can persist after failed import
181249 low Unbounded migration can halt chain
181252 low Guardian Index Worst Case Is Unbenchmarked
181262 low Non-injective block hash on state and extrinsics roots
181266 low Scheduled dispatch actual weight silently ignored
181269 low Treasury genesis state initialization omits lifecycle events
181271 low Timestamp Contract Omits Extra Bucket
181273 low Scheduler weight benchmark ranges exclude configured max agenda length
181274 low Minted event not statically counted causing extra weight registration
181276 low Outer-result gating skips proof recording for successful inner transfers in failed txs
181285 low Non-atomic scheduling leaves unrecoverable pending transfers
181286 low Failed scheduled transfers discard their recovery state
181287 low Zero-value transfers can exhaust reversible scheduling capacity
181288 low Saturating transaction nonce reuses transfer and task identifiers
181289 low Recheck high-security policy for every nested dispatch
181290 low Recovery can orphan scheduled tasks after cancellation failure
181291 low Recovery benchmark omits timestamp agenda worst case
181292 low Benchmarks omit coupled transfer and scheduler cleanup invariants
181293 low Timestamp agendas use the prior block's clock
181297 low Saturated timestamp targets create unreachable scheduled tasks
181298 low Cancellation finalizes transfers without confirming scheduler cleanup
181300 low Reversible transfers create duplicate proof records
181301 low Reject invalid high-security enrollment during genesis
181302 low Small agenda limits eliminate priority reservation
181303 low Recovery closure can transfer unrelated reserved funds
181304 low Recovery removal can release unrelated reserved collateral
181305 low Recovery removal deletes deposit accounting after reserve shortfall
181306 low Preimage deposits ignore configured rates and overcount footprint items
181307 low Benchmark submitter does not satisfy collective membership
181309 low Maximum rank can overflow into a zero-weight aye vote
181310 low Reject overflowing timestamp bounds
181311 low Unchecked target-time scaling corrupts difficulty adjustment
181312 low Unchecked cast can invert slow-block difficulty adjustment
181316 low Technical referendum Root submission path is unavailable
181317 low Live membership changes can lower referendum quorum
181318 low Removed members retain voting weight in active referenda
181319 low Failed genesis builds retain partially seeded governance state
181320 low Accepting an uncontrollable treasury reward destination
181322 low Guard scheduler benchmarks against zero capacity underflow
181323 low Cleanup weight model omits reachable maximum removal range
181325 low Maximum accepted multisig call size is omitted from benchmarks
181330 low Unbounded referendum migrations can block runtime upgrades
181333 low Release builds can include two-block Root governance timing
181334 low Unprotected fast-governance build changes runtime semantics
181335 low Rejected proposal paths undercharge collection processing
181336 low Proposal policy lookup is under-metered
181337 low Mismatched approvals receive excessive refunds
181338 low Failed proposal creation can burn the proposal fee
181342 low Reject zero target times in difficulty calculation
181343 low Saturated pending-reward bucket silently discards accrued value
181344 low Membership churn corrupts active referendum voter sets
181345 low Untracked privileged credits can freeze Wormhole exits
181350 low Block-provider transition leaves referendum timing state stale
181351 low Retain preimages referenced by referendum metadata
181352 low Ghost-queue repair paths use weights benchmarked for Queued
181354 low Accepting noncanonical call payloads causes execution-event mismatch
181356 low Reconcile QPoW runtime API arity with client calls
181357 low Ambiguous runtime API work metric can diverge fork choice
181360 low Signed-origin validation blocks unsigned exits and inherents
181362 low Unvalidated genesis proof endowments permit unbacked exits
181363 low Validate collective indexes before genesis member seeding
181364 low Invalid benchmark selections succeed without running a provider
181365 low Difficulty benchmark does not validate timing or adjustment results
181366 low Benchmarking omits the reward mint-fallback path
181367 low Benchmark clamp-warning paths in finalization weight
181370 low Align timestamp inherent precheck with execution constraints
181371 low Align zero-nonce handling between mining and verification
181372 low Accepting blocks without an author digest redirects miner rewards
181373 low Malformed inherent data can panic block import
181377 low Failed finalization cleanup permanently retains work records
181378 low Unbounded auxiliary work records for abandoned forks
181379 low Saturated cumulative work causes incorrect fork choice
181382 low Try-state fails to detect dangling metadata preimages
181384 low Initial technical-collective membership is absent from events
181385 low Normal transfer completion leaves empty sender-index keys
181387 low Genesis enrollment can desynchronize guardian reverse index
181391 low Delegated calls discard post-dispatch fee and weight metadata
181398 low Cleanup deletes proposals despite incomplete deposit refunds
181402 low Failed proposal builds permanently consume rebuild requests
181404 low Import stream closure permanently disables candidate rebuilding
181405 low Benchmark API permits unbounded execution workload
181406 low Saturating request count loses preimage references
181407 low Mismatched legacy keys are silently dropped during migration
181408 low Migration omits drain deletions and preimage processing from weight
181409 low Genesis can seed an unusable technical collective
181410 low Reject non-Quantus SS58 formats for governance seeds
181411 low Validate seeded collective members are usable signers
181415 low Benchmark proposal fixtures omit deposit reservations
181416 low Timestamp-backed named tasks cannot be queried
181417 low Mining guide advertises the wrong block-time target
181420 low Invalid unsigned proofs trigger unpaid ZK verification
181421 low Unbounded endowments can stall block-one initialization
181436 low cleanup_poll zero-removal path discards cursor and returns fee-bearing success

🤖 Generated with Claude Code

n13 and others added 7 commits August 7, 2026 16:38
Confirmed findings 180560, 181329, 181347, 181395, 181418, 181327, 181394,
181396, 181393. ensure_alarm_at now schedules before cancelling; failed queue
admission and evictions keep a timeout wake-up; submit/schedule_enactment no
longer strand referenda or discard the approved call; cancel/kill free the
track queue; nudges no longer orphan alarms.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tally::support returns zero (not 100%) when the electorate is empty and clamps
ayes to the member count, so a shrunken/emptied collective can no longer push a
live referendum to 100% support.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… 181227)

Terminal Unavailable/PermanentlyOverweight outcomes count as serviced work so a
following task is not wrongly deleted; the lookup branch of service_task now
charges the unconditional Retries read+write.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dian (181299, 181294, 181295, 181386)

cancel_transfer makes the scheduler cancel best-effort so a removed task cannot
freeze held funds; execute_transfer and per_transfer weights add depth-dependent
Poseidon ref-time; genesis rejects a self-guardian high-security row.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A prior commit dropped ProposalData::call_weight and bumped STORAGE_VERSION to 1
without a migration, stranding v0 records. Adds a VersionedMigration that
translates stored Proposals to the new layout, registers it in the runtime, and
updates the README.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…mark registry (181308, 181339, 181268, 181332, 181263)

Grow the tech collective to the 5-member size the 60/61% curves assume and reject
undersized genesis seeds (critical quorum-collapse); re-seed the benchmark dev
collective; fund planck treasury signers for multisig operation; enforce the
high-security whitelist on Utility::dispatch_as{,_fallible}; register the six
active pallets missing from define_benchmarks! (with the utility bench feature).
Updates the tech-collective tests to the 5-member invariant.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…180135, 181313, 181437, 181438, 181258, 181374, 181403, 180119, 180118, 181245)

Serialize the fork-choice decision under a shared import lock; validate genesis
difficulty; fail closed on cumulative-work read errors; floor the retarget block
time; use the full reorg window; error-log (not swallow) finalization failures;
rebuild the mining candidate after a failed import and after major sync; read the
build version before snapshotting; gate seal submission on the sync oracle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@illuzen

illuzen commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review of PR #647 — V12 audit round 2
Verdict: approve. I read the full diff (7 commits, 33 files) against main, traced the risky state-machine changes line by line, ran the test suites of every changed pallet (all pass), compile-checked the runtime with runtime-benchmarks (the new define_benchmarks! entries build), and CI on the head commit is green. I found no bugs. The fixes match what the PR description claims, every fix ships with a targeted regression test, and the two audit-fix patterns that most often go wrong here — bounded-queue eviction repair and "schedule the replacement before cancelling the old alarm" — are implemented correctly. A few things are worth raising before merge, none blocking.

Things worth a comment or a decision

  1. Confirmation is no longer final (semantic change, referenda #160560). When enactment scheduling fails, the referendum stays Ongoing/confirming and retries on a future alarm — but at the retry, is_passing is re-evaluated. So a referendum that fully passed its confirm period can subsequently lose passage if votes/membership change before the retry succeeds. That's a defensible trade-off (better than silently discarding the enactment call, which is what Approved did), but it changes governance semantics in the pathological case and deserves a sentence in the PR description or code comment.

  2. Confirm key custody for the two new Planck collective members. planck_tech_collective_seed() hardcodes qzmTAz3UUw... and qzm5QCox8.... With a 5-member collective and the 61%/60% thresholds, any 2 members can veto every tech referendum and any 3 can authorize Root. Whoever holds those two keys holds a permanent veto on Planck governance — worth an explicit sign-off that the team controls them. (The [3u8;32]/[4u8;32] public-seed padding for dev/heisenberg is fine since those collectives are already all well-known Alice/Bob/Charlie keys.)

  3. Self-cancel of an in-flight scheduler task (referenda #181396). The new nudge_referendum logic cancels the stored alarm whenever when != now. That case is reachable not only by manual Root nudges but also when the scheduler postpones an alarm task (overweight agenda) and executes it at a later block — the nudge then cancels its own currently-executing task address. I traced this through service_agenda: it's benign, because the executing slot is written back as None regardless and alarms are anonymous tasks, but it silently relies on the agenda write-back ordering at the end of service_agenda. A short comment there (or a test) would protect against someone later changing the write-back semantics.

Minor / non-blocking
Scheduler lookup weight (#181227): base.saturating_add(service_task_fetched(l)) double-charges everything in service_task_base except the missing Retries read/write (upstream treats service_task_fetched as base-inclusive). Overcharging is the safe direction and the PR already says weights get regenerated before release, but the precise fix is a benchmark rerun, not composition.
Uncharged work on catastrophe paths: the failed-enactment path performs up to 17 schedule_named attempts, and note_one_fewer_deciding's new inline-promotion fallback does a full queue promotion with nested alarm retries — both charged only against the generic alarm_retry_weight cushion. Only reachable when agendas are full for 17+ consecutive blocks; acceptable.
Rolled-back promotion emits events: in one_fewer_deciding_now, begin_deciding deposits DecisionStarted/ConfirmStarted before the arm-failure rollback, so indexers can see a decision start that never happened. Cosmetic.
Import serialization: the new shared futures::lock::Mutex in PowBlockImport fully serializes verification-passed imports (including the inner client import and finalization). Correct — it's shared across clones via Arc, and I found no re-entrant import path that could deadlock — just be aware it's a throughput ceiling on import parallelism, which is fine for this chain.
What I verified specifically
Referenda (the bulk of the risk): eviction repair can never alias the referendum currently being serviced (eviction is always the position-0 entry, insertion always at position > 0); the Err arm of insert_sorted_by_key correctly arms the timeout alarm mirroring NotQueued; schedule_enactment's retry_at is always ≥ now + 1; DecidingCount accounting stays balanced through the new rollback paths (promotion transfers the slot without touching the count, rollback decrements exactly once); cancel/kill queue removal is charged in the weight annotations.
Reversible-transfers: the best-effort cancel_named cannot cause a double-pay — a stale scheduled task hits PendingTxNotFound in do_execute_transfer because the pending record is removed before the hold is released.
Multisig migration: the v0 layout matches the documented version history (call_weight positioned between call and expiry), it's wrapped in VersionedMigration (no-op unless on-chain version is 0), registered in the runtime Migrations tuple, and tested end-to-end including the version bump.
QPoW: get_chain_work fail-closed only applies to real backend/decode errors — a merely-missing aux entry still returns zero, so syncing old blocks isn't broken; the 500 ms retarget floor sits below the production divisor (8 000 ms) so it's a no-op for legitimate timing; the genesis difficulty assert reuses the operational bounds.
Utility high-security guard: matches the existing as_derivative enforcement, applied to both dispatch_as and dispatch_as_fallible, only for signed effective origins.
Ranked-collective: zero-electorate returns Perbill::zero() and ayes clamp to the member count; the shrunken-electorate test covers both.

@v12-auditor

v12-auditor Bot commented Aug 7, 2026

Copy link
Copy Markdown

Warning

V12 could not find a connected workspace for this repository. Make sure the repository owner has signed in to V12 and connected their GitHub account (or that the V12 GitHub App is installed for the owning organization), then comment again.

illuzen added 2 commits August 8, 2026 14:25
Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	pallets/reversible-transfers/src/weights.rs
#	runtime/src/genesis_config_presets.rs
#	runtime/src/transaction_extensions.rs
Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	runtime/Cargo.toml
#	runtime/src/genesis_config_presets.rs
@n13
n13 merged commit dbe640b into main Aug 8, 2026
5 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.

2 participants