dfinity / dfinity/public-multidex
OhShii Labs review, round 18 — part 2 · 4 findings (#55.1–#55.4): the verifier takes its origin from the host, the Bridge wipe never fires where it is deployed, and the instruments that certified both
Nobody has claimed this yet.
- Dominant language
- Motoko
- Stars
- 14
- Forks
- 6
- PR merge metrics
- No merged PRs in 30d
Description
Scope. The second half of round 18 against the 1.60 hardening drop (f233d18 … 1fff1d7). Part 1 (#54) read three bands of main.mo and the four value-path modules; this part read the rest of the main.mo diff hunk by hunk, the oracle and both ledger verifiers end to end, src/arb, src/bridge, the deploy scripts, and the shell and Motoko test infrastructure. Every line number below is at 1fff1d7; for everything under src/, scripts/ and tests/ that tree is byte-identical to f233d18. Nothing here repeats a #54 item; three things this pass re-found (#54.1, #54.4, #54.6) are not repeated, and where a finding is the sibling of a #54 item the number is in its first sentence.
Filed as a public issue per the revised SECURITY.md (aac2da9). Nothing in this report is a working exploit against an end user's sign-in, delegation or custody; every reproduction below runs against a stub, a scratch copy or a local PocketIC instance, never against the live venue.
What was run. The four numbered findings are reproduced — #55.2 on PocketIC, the other three offline against an unmodified verifier or module with the runnable scripts published: #55.1 (https://gist.github.com/rvnt9999/ed2ee1b3c945d979ffe12e4e8784f883), #55.3 (same gist), #55.4 (the justification predicate, same gist as #55.1), and in the appendix oracle split and Float.toInt trap (https://gist.github.com/rvnt9999/65fd598cede33eee5f1755a0101cef6d) and rebuild loop (https://gist.github.com/rvnt9999/547a988ccd8a5202641e80127ae05923). no-await gate scope and seed gate on a nat were exercised by planting a mutation in a scratch copy and watching the gate. #55.2 is reproduced on PocketIC against the backend and bridge wasm compiled from 1fff1d7, unmodified, with the steps in its section. exits still say refused is reproduced the same way (a real shed floor on the unmodified wasm, no dev hook). sweep fills an expired order is reproduced on PocketIC with the XRC mock as the price source. flat-rate sizing and silent "complete" are reproduced on PocketIC in one probe. phantom pending is reproduced on PocketIC with the DEX stopped, and its upgrade trigger is refuted the same way. Everything else is a source read with the numbers derived from the code's own arithmetic, and each finding says which it is. No tests/test_*.sh, no scripts/*.sh, no icp call, no contact with multidex.ai.
Four concessions first. (a) Our #25.2 remedy (W4-12) and our #26.4 remedy (W4-16) shipped in the shape we asked for; the clauses missing in #55.2 and in orphan slot overwritten are ones our text did not ask for. (b) Our #46 noted that whyAmIRefused().admittedNow re-derives the gate instead of sharing its predicate, for the controller branch, and held it; exits still say refused is the same shape on the new exit branch. (c) Our own GHSA-6qpg remedy — "export a resetAssistant() and call it from logout()" — was adopted verbatim; in-flight turn survives sign-out is the clause it lacked. (d) Two of our earlier findings, #26.4 and #27.2, and the W3-04 and W4-16 fixes built on them, rest on "an upgrade lands during an await and the continuation is lost"; measured on this build, the Motoko runtime refuses that upgrade outright — see phantom pending and the verified-correct list. The defects we filed are real for other triggers; the narrative was not.
#55.1 — both ledger verifiers take the chain origin from the host they are verifying: chainStartSeq is uncertified, and it is the active segment's own per-canister anchor, so a host that omits every earlier archive verifies clean [play: MEDIUM / prod: HIGH]
The invariant, in the code's words
W2-04's own root-cause sentence (docs/tasks/done/W2-04-verifier-trust-anchors.md:39-40): "every structural input to the verifier — archive list, gap list, chain start — is an uncertified self-report." Its Done-when ticks "Genesis pinned: coverage starting after the origin (active chainStartSeq, or the out-of-band --expect-start…)". The fix pins the origin to the one structural input its own sentence names as a self-report.
What the code does
scripts/verify_ledger.mjs:466-468:
const claimedStart = head.chainStartSeq.length ? Number(head.chainStartSeq[0]) : 0;
const esFlag = flag("expect-start");
const ORIGIN = esFlag !== null && esFlag !== true ? parseInt(esFlag, 10) : claimedStart;
:469 then tests Number(archives[0].firstSeq) > ORIGIN. verifySlice (:401, if (expectPrev !== null)) never checks the first event's prevHash when seeded null, so nothing else observes a missing predecessor. The browser twin is the same comparison — src/frontend/src/ledger.js:224 const chainStart = head.chainStartSeq.length ? Number(head.chainStartSeq[0]) : Number(active.firstSeq); and :275 const origin = … opts.expectStart : chainStart; — and its only caller passes {} for a full walk (:549), so the page has no override at all.
Two facts make the default worse than "uncertified". src/backend/ArchiveCanister.mo:226 puts only the 32-byte head hash into certified_data; getCertifiedHead returns chainStartSeq beside it, unsigned. And :218 sets it as if (chainHead == null) { chainStartSeq := ?e.seq } — the first chained sequence stored in that canister (:52-56 describes it as a per-canister anchor). So on any chain with a sealed predecessor, ORIGIN equals the active segment's own start and archives[0].firstSeq > ORIGIN is false by construction. The backend keeps a true chain origin (main.mo:8512, :8519, _chainStartSeq) and exposes it nowhere.
Reproduced — verifier byte-identical, only the stub SDK mutated (https://gist.github.com/rvnt9999/ed2ee1b3c945d979ffe12e4e8784f883)
Wired exactly as tests/test_verify_ledger_gate.sh:38-56, plus one scenario obtained by changing two lines of the stub copy: the active head claims chainStartSeq = 2 and the sealed archive is omitted from getArchives.
| run | result |
|---|---|
honest good-cert, default flags |
exit 0 — ✓ 2 chain links verified; 1 segment certificate(s) subnet-validated |
shipped truncated-list (the inconsistent liar: list truncated, head still claims 0) |
exit 1 — unjustified discontinuity: history starts at seq 2 but the chain origin is 0 |
consistent-liar, default flags |
exit 0 — ✓ 0 chain links verified; 1 segment certificate(s) subnet-validated / ✓ IC certificate VALID — the subnet vouches for the ACTIVE archive's head |
consistent-liar --expect-start 0 |
exit 1 — the same refusal as row 2 |
The verifier copy was cmp-identical to scripts/verify_ledger.mjs before and after. No output line says the origin was self-reported. The same run with a canister-faithful stub (a 40-event tape, archive 0 = [0..19], active = [20..39], active head chainStartSeq = 20) exits 0 with ✓ 19 chain links verified while the whole first archive is missing from the list.
The gate cannot see this. the stub SDK (tests/fixtures/verify-ledger-stub, line 134) hard-codes chainStartSeq: [0n] on the active head — a value a second segment never holds under ArchiveCanister.mo:218 — so tests/test_verify_ledger_gate.sh:94-98 ("genesis pinned, omitted archives refused") pins the check against a head shape the canister does not produce. Neither documented invocation (docs.js:1375-1380, docs/pre-mainnet-checklist.md:105-107) passes --expect-start.
What bounds it
The certificate still binds the ACTIVE head, so the surviving segment is genuinely verified; what a consistent liar can hide is everything before archives[0].firstSeq — on this venue a whole sealed season, whose SeasonRecord the docs anchor to the archived tape (docs.js:1775-1778). It needs a hostile gateway (the verifier's own stated adversary, header :5-8) or a controller reinstall. Integrity, not value; uncapped by #play.
Nearest filed items, and the discriminator
#37.1 (tail concession) and #37.2 (certificate absence) are ours; #4.3 is the browser check that never ran. None touches the origin. W2-04 is their fix, and the residual is the third trust input the fix reads from the party under audit.
Remedy — shape and clause
Shape: default ORIGIN to 0 in both verifiers (or to a backend-exposed, certified chain origin — _chainStartSeq exists and is not exported), keep --expect-start as the override, mirror it into ledger.js:275, and add consistent-liar to the stub as a seventh gate scenario (the two-line mutation in the gist is the fixture). Clause: the default origin must not be a value the party under audit chose; any default read from getCertifiedHead, getArchives or getLedgerGaps re-opens this. Not established: whether a season reset leaves archives[0].firstSeq == 0 on the live venue (main.mo:16932 clears _chainStartSeq); we did not query it.
#55.2 — resetSeason switches on the setter-only _bridgePrincipal, so on every venue the shipped scripts produce the W4-12 Bridge wipe (our #25.2) never fires — and the one-token fix opens #54.1's window on every venue [play: HIGH / prod: N/A]
prod is N/A: resetSeason returns #err on #production at :17040-17042.
What the code does
main.mo:17081-17093:
switch (_bridgePrincipal) {
case (?bp) { let br = actor (Principal.toText(bp)) : actor { adminSeasonWipe : () -> async { #ok; #err : Text } };
try { switch (await br.adminSeasonWipe()) { … } } catch (e) { return #err(…) };
};
case null {}; // unwired (pure dev sandbox) — nothing to keep coherent
};
_bridgePrincipal (:5622) has exactly one writer, setBridge at :5675. The resolver the rest of the file uses, effectiveBridge<system>() at :5653-5658, falls back to the PUBLIC_CANISTER_ID:bridge environment variable and caches the result in _envBridgeCache — never into _bridgePrincipal. The five sibling sites that need the Bridge (:6337, :6735, :6791, :7927, :7955) all switch on effectiveBridge<system>(); resetSeason is the one that switches on the raw stable var. The design comment at :5623-5633 calls the env fallback "the DEFAULT wiring" and the setters "a break-glass OVERRIDE".
No shipped script runs the setter. command grep -n setBridge scripts/*.sh returns only comments: cold_start.sh:281 ("no setBridge/setDex leg needed"), deploy.sh:775-777 ("No setBridge/setDex leg: both canisters discover each other from the PUBLIC_CANISTER_ID:* env vars … the setters remain only as a break-glass override"), play_start.sh:135. The only calls in the tree are two tests, tests/test_bridge_deposit_claim.sh:38 and tests/test_play_deposit_cap.sh:76.
So on every venue deploy.sh, cold_start.sh or play_start.sh produce, resetSeason takes case null {}: the DEX clears playReservedUnits, creditedSeq and playAdmitSeq (:16883-16891) while the Bridge keeps every confirmed − claimed balance and its admittedUnits counter — the #25.2 divergence, verbatim, on the fix that closed it. docs/bridge-and-cks-design.md:188-199 and docs/deployment-modes.md:89-91 describe the two-phase wipe as unconditional.
Why the suite is green
tests/run_all.sh:50 runs ls test_*.sh | sort. test_bridge_deposit_claim.sh (position 19) and test_play_deposit_cap.sh (63) call setBridge and the stable var survives across suites on one deployment; test_w4_batch2.sh (90) never calls it, and its §4 assertion (:133-136, "Bridge half wiped with the DEX half") is green only under that ordering. Run §4 alone on a cold_start.sh venue and it takes case null.
The interaction with #54.1, which decides the remedy
#54.1 reproduced the post-gate await window on PocketIC and reported a control row "Bridge NOT wired (no await)" in which the record was correct. That control row is the shipped wiring. So today the function is atomic and wrong on every scripted venue, and two-phase and racy only where someone ran setBridge by hand. Replacing :17081 with switch (effectiveBridge<system>()) — the in-tree pattern — makes :17085 execute everywhere and turns #54.1's fixture-only window into a window at every real season boundary.
Remedy — shape and clause
Shape: switch (effectiveBridge<system>()) at :17081, keeping the #err → abort arm and keeping case null only for setter-and-env absent. Clause, and it is #54.1's: the two must land together — re-verify the seal gates after the await (or capture-and-compare nextEventSeq/_chainHead) before performWorldWipe, or the sealing latch #54.1 proposes. Shipping the one-token fix alone is a regression on the record. We concede that our own dedup note on W4-12 had already observed "resetSeason is the only consumer of _bridgePrincipal" without following it to this consequence.
Reproduced — PocketIC, backend and bridge wasm built from 1fff1d7, unmodified
Three worlds on one subnet, same two wasm files (sha256 61984c66… / 62089ada…), same deposit on the Bridge, same controller resetSeason; the only difference is which variable holds the bridge id. Predictions were written before the first run; two runs, 35 of 35 assertions each, identical except the two chain-head hashes (they hash timestamps).
| world | wiring at reset time | resetSeason |
Bridge getMyDeposits after the reset |
|---|---|---|---|
E — env-wired: backend PUBLIC_CANISTER_ID:bridge, bridge PUBLIC_CANISTER_ID:backend, no setter ever called (the shipped wiring) |
#ok, finalEventSeq 1 |
confirmed 5 000 000 000, claimable 5 000 000 000 — not wiped | |
S — the same env vars plus setDex + setBridge |
#ok, finalEventSeq 2 (the setBridge #config event) |
confirmed 0, claimable 0 — wiped, W4-12 ran | |
| N — control, backend without the env var | — | the Bridge's every DEX call is refused with "Only the Bridge canister may …" |
That the env wiring is live in every other direction is pinned by message discrimination: in E the Bridge's playDepositReserve and creditAndRegister calls get past the "Only the Bridge canister" gate (:6792, :6338) and are refused one step later on the email binding (:6495), where N is refused at the gate; getBridge() reads opt <bridgeId> in E and null in N. So the DEX resolves the Bridge from the environment on every update path that uses effectiveBridge<system>() — and resetSeason is the one Bridge-facing site that does not. What the fixture cannot show, and we do not claim: the DEX-side half of the #25.2 divergence (playReservedUnits cleared while the Bridge keeps its counter) needs an admitted deposit, which on the #play wasm requires a Google-linked binding that only a #dev hook can plant; the Bridge ledger in E was created through the Bridge's own unwired branch and the venue env-wired afterwards. The half that decides the finding — whether adminSeasonWipe is called at all — is measured.
#55.3 — the Ledger page's W2-04 sealed-segment loop swallows a THROWN certificate failure: ok === null is neither fatal nor counted, and the page paints VALID on the active head alone [play: MEDIUM / prod: MEDIUM]
What the code does
src/frontend/src/ledger.js:169-174 — the helper's catch arm, with its own contract:
} catch (err) {
// … `null` means UNKNOWN, never OK: the caller must not paint a verified state on it. …
return { ok: null, why: (err && err.message) || String(err) };
}
The helper returns ok:false only on a lookup_path miss or a hash mismatch (:163-166). A genuine verification failure — @icp-sdk/core@5.4.0's Certificate.create verifies and throws (certificate.js:105-109 in the SDK; TrustError for a bad BLS signature at :186/:190, a stale or future time at :173/:180, an unauthorised delegation at :220) — lands in that catch. The loop, :319-327:
if (!aHead.certificate.length) {
if (!isLocalReplicaOrigin()) throw new Error(`archive ${a.canisterId}: no certificate for this segment — …`);
} else {
const c = await validateCertifiedHead(aHead.certificate[0], a.canisterId, new Uint8Array(aHead.headHash[0]), rootKey);
if (c.ok === false) throw new Error(`archive ${a.canisterId}: segment certificate FAILED: ` + c.why);
if (c.ok === true) segCertsOk++;
}
heads++;
Three outcomes, two arms. The verdict at :364 is certOk: certCheck.ok === true — the ACTIVE head only — and :559/:578 paint lg-ok on it; the copy at :358 renders segCertsOk − 1 and only when segCertsOk > 1, so a sealed set whose certificates all throw renders as a clean "IC certificate VALID" with no count at all. The CLI twin (scripts/verify_ledger.mjs:248-251) throws on the same input off a local host.
Reproduced — offline, the loop copied verbatim with the helper stubbed (https://gist.github.com/rvnt9999/ed2ee1b3c945d979ffe12e4e8784f883, sealed_segment_null.mjs)
Two sealed segments returning { ok: null, why: "Signature verification failed" } and an active head { ok: true }: {"segCertsOk":1,"heads":3}, no throw, className = lg-verify-out lg-ok, note without a sealed suffix. Controls: ok:false throws segment certificate FAILED; certificate = [] on a remote origin throws no certificate for this segment. The null arm is the only one of the three that does nothing.
What bounds it
With no #gap in the chain, the carried hash (:191, :309, :327) still binds every sealed segment to the certified active head, so the missed class is display-only. Across a gap it is not: reanchored seeds null (:302), and the #gap event commits the sequence range only (canonicalEvent, :93), so sealed segments before the last re-anchor are bound to the certified head by nothing except their own certificate — the check whose thrown failure this loop swallows. W2-04:63 calls that per-segment certificate "the load-bearing change"; :107-108 records the browser side as done. It was implemented for an ABSENT certificate, not an INVALID one. No pin: tests/frontend_security.test.mjs §A never names segCertsOk.
Nearest filed item, and the discriminator
Our #4.3: the active-head check threw on every call and the page painted green. Its fix (certOk as a hard boolean, the three-outcome contract) is in 585814e. This is the new W2-04 loop honouring two of the contract's three outcomes on a different archive — and certOk cannot see it because it reads the active head only.
Remedy — shape and clause
Shape: in the loop, if (c.ok !== true && !isLocalReplicaOrigin()) throw … — the same origin gate the empty-certificate arm already uses at :320, and the rule the CLI's validateArchiveCert applies. Clause: the gate belongs in the LOOP, not the helper — the active-head path on a local replica still needs null (:361's "DID NOT RUN" prose), and moving the throw into the helper re-breaks #4's local state. Pin: assert the loop body carries an ok !== true arm gated on isLocalReplicaOrigin, as an exact count.
#55.4 — two archive sheds with no acked event between them leave ONE jump in the archive list and TWO per-shed #gap declarations in the chain; both verifiers demand equality with a single declaration, so an honest tape reads "unjustified discontinuity", and the Ledger page has no waiver [play: MEDIUM / prod: MEDIUM]
What the code does
shedOldestEvents emits one #gap event per shed (main.mo:8829, emitEventRaw(…, #gap { fromSeq = gapFrom; toSeq = gapTo })), each carrying that shed's range. sealActiveArchiveAtAcked (:8699-8715) adds a sealed entry only case (?p); on a second shed with archive0 == null — the successor spawn blocked — the case null {} arm adds nothing to the list. So after two sheds the archive list carries one jump [a,c) and the chain two declarations [a,b), [b,c).
Both verifiers build their pending entry from the archive list — ledger.js:292-297 (if (from !== prevLast + 1) pendingJustify.push({ from: prevLast + 1, to: from … })), verify_ledger.mjs:489-494 — and justify it only by exact equality: ledger.js:331 const inChain = chainGaps.some(([f, t]) => f === p.from && t === p.to);, verify_ledger.mjs:551. Neither file merges contiguous declarations (no reduce/merge over chainGaps in either). The page passes {} (:549), so it has no acceptGaps; the CLI's --accept-gap a:c prints "accepted by FLAG (legacy, not chain-declared)" (:556) — the waiver of exactly the proof W2-04 introduced. (recordGap's coalescing at :8720-8728 is consistent with the archive list and is not the cause.)
Both #gap events survive: shed 1's declaration sits in the surviving tail at b + shedTo, and shed 2 drops less than that from b under every default cap (:8440-8449), while the baseline swallow (:8797-8803) is strict on both sides. The verifier collects both (ledger.js:201, verify_ledger.mjs:405) and still throws: it holds a complete proof of [a,c) and refuses it. (Predicate re-implemented and run: https://gist.github.com/rvnt9999/ed2ee1b3c945d979ffe12e4e8784f883, double_shed_justify.mjs — a single shed justifies; a double shed throws whether or not the tuples are coalesced.)
Reachability — traced, not executed
tickShipEvents runs the L2/L3 checks (:8896-8901) before the archive0 == null spawn branch, which returns on the 600 s backoff (:8919) armed by a spawn whose cycles are below ARCHIVE_INITIAL_CYCLES + ARCHIVE_FEEDER_MIN_HEADROOM (:8397-8403, :8921-8927) or by a spawn throw (:8931-8933); the L3 shear (:8896) is size-only. So a second shed needs the shear cap of new events inside one backoff window with the spawn still blocked — the "spawn failure plus refill" case. The shipped suite has no contiguous double-shed fixture: tests/test_archive_failover.sh §C and §F both shed with successful ships between, so their tuples are non-contiguous and a sealed archive sits between the jumps.
Nearest filed items, and the discriminator
Our #9.4 (the shed firing on depth alone — a backend trigger, fixed in 1.60) and #37 (pre-W2-04 verifier). This is the justification predicate W2-04 added: one archive-list jump versus N chain declarations, exact equality between them, no union anywhere.
Remedy — shape and clause
Shape: accept a pending range iff it is exactly covered by a chain of contiguous chain-collected #gap events — sort, merge adjacent [f,t) where t_i === f_{i+1}, then exact-match the merged list — in both verifiers. Clause: merge only #gap events collected from the verified walk, never getLedgerGaps tuples, and require the merged range to equal the jump exactly; a superset or a hole re-admits the fabricated-tuple attack the fabricated-gap stub pins.
Further items, verified and not counted — one paragraph each, citable by the slug in bold
The numbered findings above are the ones we reproduced against the pair #54 opened. Everything below is verified with the same standard (every line opened, default verdict refuted, a concrete input for each) and is a source read unless a run is named; we list them rather than number them because the rubric counts findings, not titles, and a queue of twenty numbered items the morning after #54 would say more about us than about the code. Each opens with a stable slug so it can be cited by name.
-
oracle split —
[play: MEDIUM / prod: MEDIUM], the residual of @andreij6's#17item 3 (W3-05 is its fix); we are not reopening the source floor (our ownPRICE_MIN_SOURCESproposal was withdrawn after#17).PRICE_SOURCES(main.mo:14941-14999) tags two venues#usdand six#usdt;PriceFeed.mo:275never trims a group of fewer than three, and the diverged branch (:433-435) returns the USD group with its ownsourceCount, which the floor at:15882(three) then refuses — no branch relaxes it. Measured withmocon the shipped module against585814e's, same eight readings (https://gist.github.com/rvnt9999/65fd598cede33eee5f1755a0101cef6d,usd_usdt_split.mo): coinbase 100.00 and coingecko 97.50 with six USDT venues at 100 ± 0.1 → 1.60 reports diverged,sourceCount 2, stddev 179 bps, primary refused;585814etrims 97.5 and applies seven sources. The threshold is a 2.0% lag with coinbase present, 1.01% alone; a real depeg (USD group 110, USDT 100) also yieldssourceCount 2, so "the mark follows the USD group" is unreachable with this fleet, and on a cloud-engine play deployment_xrcPrincipalis null and the fallback the design relies on is, in the code's words (:15736-15737), inert — the mark freezes with a "possible USDT depeg" warn every 30 s.docs/oracle-xrc-fallback-design.md:181-183says the USD count may drop below the floor; with this fleet may is always, which is the part nobody wrote down.tests/PriceFeedQuotes.test.mo:48pinssourceCount == 2as correct and never composes it with the floor. Remedy shape: make the divergence decision robust before it may discard six venues; clause: the pooled trim stays primary whenever the USD group is not. -
Float.toInt trap —
[play: LOW–MEDIUM / prod: MEDIUM].PriceFeed.mo:430-431divides by the USD group's price andFloat.toInts the quotient;core@2.5.0Float.mo:446-454traps forinfandNaN;parseLeadingFloathas no lower bound. Measured withmoc 1.9.0(https://gist.github.com/rvnt9999/65fd598cede33eee5f1755a0101cef6d):Float.toInt(1.0/0.0)→bigint_of_double: argument is NaN or inf; a coingecko body with 300 zeros (323 bytes, under the cap) parses to 9.99e-302 and passes> 0.0; through the shippedaggregateByQuotewith the shipped fleet shape — coinbase absent, that body, six USDT venues at 100 000 — the call traps, while the pooled path on the same readings applies six sources. The trap sits in the post-await continuation (main.mo:15970):lastAggregatesis not written,applyFreshAggregateis never reached so the XRC fallback is bypassed, and the tick'strywraps thefor, so every later pool is skipped that tick; thefinallyreleases the flag, so nothing wedges, and it repeats every 30 s. Preconditions: a hostile USD upstream (the class the tree's own< 1e15gate at:369-376exists for) plus the routine absence of the other USD source. Remedy: a finiteness gate beforeFloat.toInt; clause: an infinite divergence must be reported as diverged, not zeroed. -
rebuild loop —
[play: MEDIUM / prod: MEDIUM], bounded.main.mo:18196-18199, an actor-body statement that runs on every start, rebuilds the release-link reverse index withfor ((k, v) in Map.entries(stagedReleasedAs)) { revAppend(v, k) };revAppend(:386-394) deletes already-yielded keys from the same map once a target id carries more than 32 handles, against the rule the file states forty lines up (:452-453, "nothing here iterates a map it is mutating"). Reproduced withmoc -roncore@2.5.0with the two functions copied verbatim (https://gist.github.com/rvnt9999/547a988ccd8a5202641e80127ae05923): one shape traps inside the iterator (UNREACHABLE_ERROR … Map.internalEntries(), internal kvIndex out of bounds— in the actor body that is a refusedinstall_code), the others silently skip 29% of live links, after whichcancelMyOrder/cancelOwnSpotOrderresolve the skipped handles to the cancelled old id. Reachability, honestly:585814eproduces at most one handle per order id andresetExchangekeeps the map, so the over-32 state is the W4-07 intermediate (d8f4c58, after the public585814e) whose repoint had no cap — whether the live venue ran it long enough is not visible from the mirror, and only the first 1.60 start is exposed. Remedy:Iter.toArray(Map.entries(…))before the loop; clause: no delete on the map while anentries()cursor is live. -
no-await gate scope —
[play: INFO / prod: MEDIUM], the maintainer's own rating for the class (W5-05:5).tests/test_deploy_hygiene.sh:1093-1108asserts zeroawaitin four lib files and citesdocs/security-review.md:143("noawaitin any user value path"); the thirteen value-path entry points (withdrawLp,closePosition,swap,placeMarketOrder,depositLp, …) aremain.mofunctions the gate never opens, andmain.molegitimately holds 63 awaits in 41 functions — none of those thirteen today. Mutation on scratch copies: anawaitplanted as the first line ofclosePositionleaves §8 green (fourok, rc 0); one planted inlib/Accounts.mogoes red. W5-05's doc scopes the four files deliberately ("where the property lives") — conceded — and triage:633records our#41.3as fixed by it; this gate is that fix, and its scope is narrower than its subject.geptorFetchAndSweep's continuation is not a:143violation under its own definition, and we do not file it as one. Remedy: a per-function assertion overmain.movia the file's ownmo_func, floored at 13; clause: slice the function, never the file. -
exits still say refused —
[play: LOW / prod: MEDIUM], the same shape our#46held for the controller branch, now on the branch W1-05 (our#27.1) added.inspect(main.mo:9670-9673) admits eight exit methods at any shed floor;whyAmIRefused().admittedNow(:10976-10985, byte-identical to585814e, a singleBool) still reportsfalse; the banner (main.js:3797-3798) still says "orders will be refused";docs.js:1559-1560advertises the field as the way "a shed bot can always self-diagnose"; andtests/test_load_shed_exits.sh:66-68andtests/test_ship_backpressure.sh:73pinadmittedNow = falsein the same run that proves the exits pass — the failure text "diagnostic disagrees with the gate" is attached to the assertion enforcing the disagreement. A bot that trusts the field holds itscancelAllMyOrdersuntil the floor drops; a caller who simply tries succeeds. Reproduced on PocketIC against the unmodified#playwasm (setTestShedFlooris a#devhook and traps on this build, so the floor was raised with real load: 63 owners × 32 post-only staged orders,execLive = 2019 ≥ SHED_SOFT_STAGED, one heartbeat recompute →shedFloor 1): in that one state the registered rank-0 user readswhyAmIRefused = {shedFloor: 1, admittedNow: false}, itsplaceLimitOrderis refused pre-consensus (canister_inspect_message explicitly refused message, the same code pair as an anonymous caller's refusal — so PocketIC does runinspecton ingress, pinned by a control), and in the same state itscancelAllMyOrders(null)returns3andwithdrawreturns#ok;admittedNowstill readsfalseafterwards. Two runs, 22 of 22 assertions, RAW lines identical. Two of the eight exits were driven; the other six rest on the source and the shipped test's §3. Remedy: oneisExitMethodpredicate consumed byinspectandwhyAmIRefused(anexitsAdmittedfield), the banner completed, both tests flipped; clause: one predicate, not a second list. -
orphan slot overwritten —
[play: LOW / prod: LOW], downgraded by the same measurement, on the record our#26.4asked for (W4-16)._pendingSpawn(main.mo:8159) is set aftercreate_canister(:8163), yields on the install (:8164), and no spawn site checks it (:8741,:8919,:9047gate only on the transient_spawnRetryAfterNs,:8150); the reconciler reads it only past a 300 s age gate (:8171) and clears unconditionally (:8177, contrast the compare-and-clear at:3691-3692). Our first draft, like W4-16's own doc and the comment at:7506-7509, ran the timeline through "an upgrade landing on the open install": on this build that cannot happen (previous item), so the record survives every upgrade the toolchain allows. What remains isperformWorldWipe, which zeroes the cooldown (:16933) and wipeseventLog(:16705) while an install may be in flight across its own awaits; then the first beat re-spawns and:8163overwrites the record with no log line — a canister main controls, holding up to 3T cycles, listed nowhere. Narrow, and a source read. Remedy: refuse to spawn while the slot is occupied, beforecreate_canister; compare-and-clear at:8177; log the principal on install failure. We concede our#26.4asked for a slot and a reconciler, not a busy guard — and that its "lost continuation" premise was wrong for this toolchain. -
phantom pending —
[play: LOW–MEDIUM / prod: N/A], follow-on of our#27.2(W3-04), with the trigger corrected by execution.src/bridge/main.mo:387-389writesadmittedUnitsandpendingbefore the await; the#errrollback (:392-401) runs only in the continuation, thecatchkeeps state (:404-406),postupgradeclears onlyclaiming/admitting(:486-489), anddevConfirmDepositsskips only whileadmittingholds (:426-433). We first wrote this up as "the DEX refuses and a Bridge upgrade lands before the reply" — and that trigger does not exist on this build: the Motoko runtime compiled into the pristinebridge.wasmrefusescanister_pre_upgradewhile a callback is outstanding ("attempted with outstanding message callbacks (try stopping the canister before upgrade)", measured), stop-before-upgrade drains the call so the rollback runs, andskip_pre_upgradedoes not drop the continuation but wipes the whole Bridge state. The trigger that does exist is thecatcharm — the DEX rejecting the call — and it is routine: the same runtime check forces every DEX upgrade through a stop. Reproduced on PocketIC with the DEX stopped:devSimulateDepositkeepspending Xwhile the DEX used nothing;devConfirmDepositsmoves it —confirmed 5 000 000 000 000, claimable 5 000 000 000 000for an amount the DEX never reserved;claimis refused bycreditAndRegister's excess branch withreserved == 0, identically on retry; the same refusal with the DEX running rolls back topending 0. Two runs, 35 of 35, RAW lines byte-identical. What the fixture cannot reach on the#playwasm: an allowance bucket (the email binding is a#devhook), so the refusal exercised is the identity gate, not "allowance exceeded", and the "later legitimate deposit on the same asset is poisoned too" aggravation (claim is all-or-nothing,:323) rests on the source read.:353-354("A refused deposit is never created") andmain.js:1754-1756are false on the catch path. Remedy: drop a pending with no matching DEX reservation before confirm; clause: only with no reservation — which needs a reservation query the Bridge does not have today. -
seed gate on a nat —
[play: LOW / prod: INFO].deploy.sh:442-444mdx_call_okgrepsvariant { ok; W5-22 applied it at:363-364toseedInsuranceFund, which returnsasync Nat(main.mo:12886,backend.did:1578), so every successful seed prints "Insurance fund seed failed" — fed the real definition,(144_000_000_000_000 : nat)is false. The previous exit-status gate was correct for this method;play_start.sh:303grepsnat.tests/test_deploy_hygiene.sh:635requires the wrong form,:636-638bans the right one, and the §6m self-test (:645-648) has no bare-natfixture. A manual re-seed mints twice (no idempotence guard at:12894-12901) — on#playan accounting oddity of unbacked play money, not a value defect. Remedy: gate on the returnednat; clause::635and anatfixture in the same commit. -
fallback clock rewinds —
[play: LOW / prod: LOW], follow-on of our#27.4/#27.3. The XRC fallback writer (main.mo:15909-15923) stamps the anchor's honest time but never compares it with the standing mark; the only monotonic guard (:15878-15881) is on the aggregate. One fan-out below the floor a second after an eight-source apply moves the mark to a single-source older value and its clock back ~90 s; the direction is conservative for anti-snipe, but a straggling primary sampled atT − 30 sthen passes the guard — the overwrite W3-06 refuses.tests/test_oracle_time_bases.sh:44-56§2 executes the rewind and asserts it, so this is the clause not applied to the fallback writer, not a missed case. Sibling:fetchedAtNsis stamped after the outcall (:14407-14408), an arrival clock (@andreij6#17.2, our#9.2). Remedy:if (xrcAnchorEffectiveNs(a) <= p.refPriceUpdatedNs) return #rejected; clause: the anchor's observation clock, and §2 of that test changes with it. -
sweep fills an expired order —
[play: LOW / prod: LOW], the union of @andreij6's#14findings 4 and 17 (W4-07 carries the expiry, W4-06 checks it at staged release). The contract (main.mo:185-187,MatchingEngine.mo:59-61) says "never filled"; everyctx.isExpiredsite tests the maker (:317,:602,:780);ammSweepRestingcollects a crossed resting order with anti-snipe only (:2394,:2415), cancels it (:2461) and re-submits it as the aggressor (:2462-2466) without readingorderExpiry[it.id];repointOrderIdentitycarries the expiry after the trades. Window: until the finaliser beat (:1657-1665, 0.5 s); the real one is a GEPTOR requote landing after its outcall. Price improved, contract broken. Reproduced on PocketIC against the unmodified wasm (mark via the XRC mock, sincesetAmmRefPriceis a#devhook on this build): a bid placed withplaceLimitOrderExp(…, ?150)rests; with maintenance timers paused the clock is moved past its expiry; the mark moves down through the bid and the controllerrequoteAmmruns the sweep — one trade, buyer = the user,trade.timestamp = E + 29 s, statusfilled, base credited. Two controls on the same fixture:adminSweepExpiredOrdersfirst, or timers unpaused, and the order iscancelledwith no trade. The canister's own expiry is bracketed by its own actions (the W4-06 release gate did not kill the order atE − 91 s; the expiry sweep did cancel it atE + 29 s), so the claim does not rest on our derivation ofE. Two runs, 20 of 20 assertions. The production window (a GEPTOR requote landing after its outcall) was not measured; this adds the reproduction, not a wider window. Remedy:if (orderExpired(it.id, Time.now())) { cancelRestingOrderInternal(it.id); continue }before the cancel; clause: test the order's current id. -
flat-rate sizing —
[play: LOW / prod: LOW].main.mo:13351-13355sizes a budgeted market buy atTAKER_FEE_BPS(10) whilequoteSwap(:13160-13163) previews at the caller's ladder rate and release charges it (MatchingEngine.mo:826-833;TAKER_TENTH_BPS = [100, 90, 80, 70, 60]). With the repo'sFixedrounding, 10 000 ICPUSD on a flat book at 100: L4 preview 99.94003597 base, execution 99.90009990,quoteSpent9 996.00,rem = 0so thespentAllblock (:3219-3262) never runs and 3.996 ICPUSD stays unconverted with no record; siblings at:3587and:13439. The residue stays with the caller.tests/test_swap_direct_budget.sh:120-126pins over-delivery only, with an L0 identity; the sizing comment at:13334-13335is false for L1–L4. Not a regression against585814e(which over-spent — our#48.5); a new claim that overstates by the ladder discount. Reproduced on PocketIC against the unmodified wasm, with the level earned the way a live trader earns it (one $12 000 maker fill and a tier tick → L3, 7 bps), on a flat users-only book at $100: the L0 control receives exactly its preview (9 990 009 990 units both ways); the L3 caller is previewed 99.93004896 ICP and receives 99.90009990 — the L0-sized quantity — with 2.997 ICPUSD left unconverted,getMyRecentSwapnull and no rejection record. Two runs, 26 of 26 assertions, every integer equal to the prediction written first. L4 was not executed (the scorecard hook is#dev-only); its 4 bps follow from the same formula. Remedy: size withquoteFeeFor(caller, 10_000, #takerDebit); clause: the reservation may stay at the ceiling. -
silent "complete" —
[play: LOW / prod: LOW], three inputs into one branch.main.mo:3228-3234judgesspentAll = b <= quoteSpent + max(1, b / 10_000)and takes "nothing to re-park, nothing to record". (1)biseff = min(bud, avail)(:2570-2572, deliberate) whileswappre-checks the raw balance (:13244-13245), the staging reply saysfromAmount = 0(:13363),quoteSwapnever clamps, the UI toast names the full amount (main.js:9089) and the direct path writes noSwapOutcome: a caller with 600 reserved who asks for 1 000 converts 400 and is told nothing. Reproduced on PocketIC (same runs): a caller holding 1 000 ICPUSD with a staged bid reserving exactly 600 is previewed 9.99 ICP for a 1 000 swap,swapaccepts withfromAmount 0, toAmount 0, the release debits 399.99999939 ICPUSD and credits 3.996 ICP,getMyRecentSwapis null andgetMyReleaseRejectionsis empty. (2) The engine's tuple carries no stop reason, so a book stop within 1 bps of the budget (10 ICPUSD on 100 000) is silent while one unit further out re-parks (:3241) or records (:3249); the true budget residue the crumb was built for is underprice/SCALE. (3) is the flat-rate sizing above. Remedy: a stop reason from the engine,effreported at staging, aSwapOutcomeon the direct path; clause: the crumb shrinks to the real residue. -
two order budgets —
[play: LOW / prod: LOW]. The three reject sites are our#52.1follow-up landing, which#54already recorded as correct, and:1827-1830states the wallet path was deliberately left on evict (pinned bytests/test_order_caps_margin.sh:133-136) — conceded. Unstated:evictOverCap(:1761-1768) counts the wallet key only, pool orders rest under the pool principal, so 100 pool-path entries then 100 wallet orders reach 200 resting under one owner (the reverse order stops at 100);myOpenOrderCountreports 200 besideopenOrderCap = 100, and the newgetApiDocsentence (:10896, "counts your account PLUS your margin pools — placeLimitOrder evicts…") is false on the wallet path. Both checks also run at staging while counting resting orders, a +32 transient the tree documents for the level cap (Types.mo:288-291) and not for this one. Not liveness. Remedy: loop the evict on the owner count, or correct:10896; clause: the disclosure surfaces describe the same rule. -
pool cancel door —
[play: LOW / prod: LOW], follow-on of @andreij6's#14finding 16 (W4-05 claimscancelMyOrderonly).cancelPoolOrderInternal(main.mo:11643→:11664→:11679) has nostagedReleasedAshop; the three wallet doors resolve (:10536-10539,:10805-10810,:13818-13829);getPoolOrdersexposes the staged id (:11612-11626) andlinkStagedReleaseruns for every release (:3274).cancelPoolOrder(pool, S)says "Order not found for this pool" whilecancelMyOrder(S)cancels;getApiDoc:10917offers them as equivalents anddocs.js:1525-1527says ids resolve "automatically"; the frontend does not re-poll before cancelling (main.js:4899-4902). Remedy: the same two-line resolve; clause: the ownership check on the resolved order. -
in-flight turn survives sign-out —
[play: LOW / prod: LOW], the clause our ownGHSA-6qpgremedy lacked.assistant.js:745-752resets the transcript and says "an in-flight turn's writes land in a cleared, ownerless chat at worst"; there is no epoch or abort, the loop re-reads module state after every await (:512,:546-547,:580-581,:685) and renders into the current list. A turn parked in an outcall when A signs out writes A's result into the fresh transcript; sign-in never resets, so the next identity on the same browser reads it and relays it over its ownaiComplete. "Ownerless" is false the moment someone signs in. Remedy: a module epoch checked after every await; clause: before every push and render, not only at the loop top. -
config row blank —
[play: LOW / prod: LOW], follow-on of our#47.4.ledger.js:381-398kindCellgainedgapand noconfig; six setters emit#configonto the same tape (main.mo:8551-8562);main.js:7656-7659handles it; the Ledger page shows the row with kindeventand an empty detail — exactlysetter/value. Remedy: aconfigcase; clause:escHon both fields. -
arb clip equals the cap —
[play: LOW / prod: N/A], follow-on of our#24.1(W4-08).src/arb/main.mo:61setsTRADE_CAP_USDequal toARB_MAX_SWAP_USD;capBaseis floored at a mark read once per tick (:273-276,:297) and the DEX re-prices with a ceiling at its current mark after the 50-bps bound (main.mo:5926-5931). With the repo'sFixed: no self-refusal at a still mark; the tolerated upward move ismark²/(cap·SCALE)— $0.0072 at $60k, one unit on ICP — so a +$0.01 tick refuses a cap-sized import, flatten or in-tick unwind; on a rising mark every flatten of last tick's full clip is exposed; a one-tick delay, not a wedge. W4-08 sizes at the cap with no headroom sentence;test_deploy_hygiene.sh:505-511pins-le. Remedy: a few bps of headroom and a strict pin; clause: headroom above the largest applied oracle step. -
epoch guard on one caller —
[play: LOW / prod: N/A], follow-on of our#25.4(W4-13)._oracleEpochis captured and compared only intickPriceRefresh(main.mo:16098,:16115);refreshMultiSourcePricewriteslastAggregatesand_lastOracleSrc(:15978,:15986) before the check runs, and its other callers (geptorFetchAndSweep :3673-3686,fetchAndSetRefPrice :16021) have none, against W4-13's Done-when "abandons itself … rather than writing a result"._geptorInFlightsurvives the wipe by design,emptyPoolstampsrefPriceUpdatedNs = 0(AMM.mo:64-76) and the monotonic guard passes any real sample against 0, so betweencreateAmmPooland the seed script's own fetch a pre-wipe observation can be the new pool's first mark. Genuine data, seconds old; the finding is the Done-when sentence. Remedy: capture in the callee; clause: side-map writes after the compare. -
empty successor as a season segment —
[play: LOW / prod: N/A], adjacent to#54.5and our#25.5/W4-15.performWorldWipe(true)(:16962-16964) registers_archiveNextin_seasonArchiveswithfirstSeq = 0, then nulls the pointer (:16967); the next season spawns fresh (:8930-8948), so the registered canister never receives a batch: it renders as aseasonrow reading "0 – growing" (main.js:6555-6557), is refilled forever (:7745-7770), and costs one ofarchiveExecute's six hops on every deep read (:17565,:17574). Precondition: a successor pre-spawned at the wipe. -
round-trip verdict on the whole balance —
[play: LOW / prod: N/A], new in W4-25.src/arb/main.mo:289reads the arb's whole available balance and:164judgesavailBase * 2 > qty; carried inventory (export capped at:298, refused at:306) makes a fully filled hedge read as a spoof and parks the rich leg for 10 s per event (the 1 280 s cap needs eight in a row); the memo is deleted at:163before the export, so a refused export never bumps the backoff. -
"side" inon a primitive —[play: LOW / prod: LOW].assistant.js:669-670appliesintoobj.args, which is rawJSON.parseoutput with no shape check; a primitive throwsTypeError,assistantRunhas nocatchandassistantSubmitfires it un-awaited (:726): an unhandled rejection and a silent turn. New with theGHSA-6qpgfail-closed check. -
quote accepts 0, swap refuses it —
[play: LOW / prod: LOW], the residual of @andreij6's#13item 3 (W4-04, which deliberately keptquoteSwap's 5% default,:13129-13132) and our#48.5class.swapwithmaxSlippage = 0now refuses (:13237-13238); the UI, the assistant and the arb never send 0, andgetApiDocdocuments neither the default nor the range, so only a bot reading the API doc meets a preview that succeeds and an execution that refuses — in the safe direction. -
silent abort under
set -e—[play: LOW / prod: LOW].deploy.sh:78sets-euo pipefailwith notrap; the two W5-22 sites are barex="$(icp … 2>&1)"in then-bodies (:363,:806), not theif icp …; thenforms they replaced, so a non-zero CLI exit ends the script with the diagnostic unprinted; at:806that skipssetEnabled, the assistant-key injection and the frontend build. Reachable trigger on shipped postures: transport or decode failure (the posture and controller traps are excluded earlier in the same function). The class predates W5-22 (:349); the in-tree fix iscold_start.sh:459's|| trueinside the substitution. -
merge-lock race — tooling, outside
SECURITY.md's scope, noted becausefactory.sh:952-953states a guarantee the fresh-lock case breaks: the lock directory exists (:947) two commands before itstsfile (:963); a concurrent acquirer reads a missingtsas 0, breaks the fresh lock and takes it; two holders, and the latetslands in the wrong owner's directory. Reproduced with two subshells; the natural window is milliseconds. -
three small instrument notes —
--max-tail-skewand--expect-startgiven bare or non-numeric parse toNaNand silently disable their bound (verify_ledger.mjs:68,:468; the zero-links rule still catches total withholding); the header-gate loopsbreakafter the first CSV token (deploy.sh:979-985,:1067-1073) so a non-https://first origin runs no check — every documented value ishttps://-first, so latent;check_headers.sh:12has no-L, a false red on a redirect.
Informational — comments and docs the drop left behind, one line each
main.mo:12444-12448: thecreated_at_timecomment promises ledger deduplication on retry; the stamp isTime.now()per attempt, so a retry is a new key. The surrounding W3-02 interlock makes the promise unnecessary, and the text is what an operator reads during an ambiguous transfer.main.mo:5993"expired after 30s" —ARB_UNWIND_MEMO_TTL_NSat:5748is 60 s and:5998uses the constant.lib/Types.mo:181andmain.mo:4302assert an ACTIVEmigration.mo;main.mo:65says it was applied and removed, and no such file exists atf233d18or585814e.docs.js:1850"apiVersion is now 2.0.0";MM_API_VERSIONis"5.0.0"(:10742).docs/tasks/README.mdis load-bearing in the software-factory skill file (SKILL.md:87/244/279),docs/security-review.md:167and W6-12 and is absent from the tree.main.mo:15352-15354"Withdrawals need no gate: withdrawLp redeems a pro-rata basket IN KIND, which is price-neutral by construction" — since 1.60holdingsUsd(:16437-16441) marks four legs atpoolRefPriceand the payout factor iskeepBps − W/H; the sentence was true at585814e. (The zero-basket case itself is#54.6.)docs/security-review.md:137-138marksgetMyPendingMatches"✔ Fixed. Per-user secondary indexes…"; atf233d18it still walksMap.entries(pendingMatches)(:13940-13946). The walk is bounded (matches live ≤ 30 s), so this is the#36.1class, not a cost item — and we concede that W5-21's "all five sites" is accurate for the population it names (deferredExecs), which is not this one.docs/tasks/done/W5-19-remaining-uncapped-sweeps.md:58"All six walks bounded, indexed or cached" overstates F6: of the threetickTierwalks the maintainer's own triage §4.3 named, the volume-badge one is sharded (:7277-7280) and the uptime sampler (:7239-7252) and the five-map level recompute (:7257-7263) are still full walks — pruned at source, in their own message, and far from any ceiling; the item is the maintainer's and @andreij6's#19.6, credited as such.scripts/play_start.sh:32says the script "re-wires setBridge/setDex";:135says it does not.main.mo:13334-13335"the mostamountcan convert on a flat book" — see flat-rate sizing.
What we verified and found correct, so it is on the record
-
An upgrade cannot land inside an
awaiton this toolchain. Measured on PocketIC with the pristinebridge.wasm:install_codein upgrade mode traps incanister_pre_upgradewith "attempted with outstanding message callbacks (try stopping the canister before upgrade)" while a call is outstanding; a stop drains the call before the upgrade;skip_pre_upgradewipes the state rather than dropping the continuation. Every "lost continuation on upgrade" narrative in the tree (W3-04's, W4-16's, the comment atmain.mo:7506-7509, and our own#26.4/#27.2) describes a state this build does not produce; the#54.1race is unaffected (it is a message interleave, not an upgrade). -
The W4-23 half-spread clamp cannot be picked off during a >50% pend. A pend does not advance
refPriceUpdatedNs(:15907returns#pended), the sweep takes only orders that predate the last accepted fetch (:2394,:2415), and the 5% marketable band (:10343,:10370) refuses a bid at 1.5× the mark while any ask sits below it; the ladder geometry (asks at 1.5× ref above a ~50% gap) is real and unreachable. -
check_origins.sh'ssettings showparser matches the shipped CLI.icp-cli 1.3.0's format-string pool renders environment variables askey: valuebeside the nine settings labels;frontend_originsis an environment variable and environment variables are a canister setting. Fail-closed by design. -
The four
getMy*queries that still walk global maps (:11698,:11739,:11776,:13940) are pre-existing and not a ceiling risk:marginPoolsneeds ~8–16M entries for the 5B query ceiling, an order of magnitude where the heap wall your own comment at:11105-11109documents arrives first;pendingMatcheslives ≤ 30 s. -
The 1.60 budget arithmetic in
MatchingEngine.moholds for every taker level (cost + fee ≤ capfor exact bps;quoteSpent= Σ slices;filled + remaining == original); the one latent gap is the maker ladder's half-bps rungs (4.5, 3.5, 2.5) truncated by the integer-bps probe at:827— ≤ 0.5 bps, unreachable today because the only maker-rated context passes no budget (:2466); and the#marketbranch (:975-993) materialises the aggressor with a fresh id rather than the reservedaggressorId, unreachable because the onlyaggressorIsMakercontext passes#limit(:2465-2470). -
MarginPools.liqPricerounding directions,Accountsprefix seek, the Bridge'sclaiming/admittingclears on both post-await paths and inpostupgrade, the recomputed CSP hash forai-connect.html, the W4-22 fee-role half pinned end to end bytest_volume_credit.sh§3, every newNatsubtraction guarded in the same construct,#gap/#configexhaustive in everyswitch, theinspectenumeration complete, no stable type changed. -
The Motoko unit tests added in the drop pin properties, not prerequisites, with two shapes worth naming:
OqlRegistry.test.mo:1145andEventChain.test.mo:287are reach-the-line pins by design and say so;PriceFeedQuotes.test.mois exact and blind to the class in#55.4by fixture choice, not by construction.
Rejects from this round, with the line that closes each
- The AMM clamp pick-off (above) —
:15907,:2394/:2415,:10343/:10370. - A browser full-verify hard-failing on a first archive whose
chainStartSeq > firstSeq(ledger.js:288-291, no override) — unreachable atf233d18:chainStartSeqandfirstSeqare set on the same first stored event (ArchiveCanister.mo:218-220), the chain predates the first public commit, and W4-14 confirms the Season I chain was detached. The reachable sibling — the same no-override throw at:301-305for agetLedgerGapsre-anchor from an L2 shed that happened before 1.60 (the "legacy gap" W2-04's Done-when names with a CLI-only override) — is plausible, unknowable from the mirror, and only fires on an explicit full walk. - "The archive-list walker picks the empty successor for seq 0" — no walker resolves a sequence through
_seasonArchives(archiveForSeq :9186-9192,getArchives :8325-8332). - "A refused Bridge deposit becomes a free credit" —
creditAndRegister :6392-6414charges the allowance or refuses. - "
sourceCount > 0implies the quotient is finite" — two of our own readers made this argument against Float.toInt trap;Float.toInt(inf)traps and a positive operand below 1e-299 makes the quotient infinite. We say so because the argument is persuasive and wrong. - "21 of the 26 new shell suites bypass
assert_invariants" — the counts are exact (21 define private helpers, 5 source_lib.sh;finish_testis the only caller;run_all.shdoes not run it), and the convention was already the majority at585814e(37 of 67), so it is an addendum to our#38.2/#51.5, not a finding. - A
_liqInFlightleft raised by the pause gate (:4003-4006early-returns withoutcompleteLiquidationPass) — a one-interval phantom ingetLiquidationSweepHealth, self-healing on the first completed pass, below the streak threshold; the residual of our#52.4on the fixed shape.
Coverage — what this round did not read
main.mo 10000–15999 outside the functions named above; MatchingEngine.mo outside the W4-22 windows; AMM.mo, ArchiveCanister.mo and Liquidator.mo outside their diff hunks; OrderBook.createOrderWithId beyond :400; main.js outside its diff hunks (getDerivationOrigin, the inputs of renderVaultHaircut); tests/eventchain_equivalence.test.mjs. Live sizes on multidex.ai are unknown to us and were not queried. Everything not listed as reproduced above is a source read, and we say so rather than imply a run.
— Ravenith, OhShii Labs
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading the four finding sections and the cited entry points: main.mo resetSeason, ArchiveCanister.mo, scripts/verify_ledger.mjs, src/frontend/src/ledger.js, and tests/test_verify_ledger_gate.sh. Reproduce the verifier and bridge cases with the published scripts and PocketIC steps. Done means each reported behavior is corrected in both relevant implementations and regression coverage exercises the listed missing scenarios and deployment ordering.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, shell
- Domain
- backend, security, testing-qa, tooling
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 25/100