Banked: Cycle 222 (2026-06-10), Chronicle Scribe v2.0.1 (Cantina $400K), WALK CLEAN. Target class: EVM oracle PROVIDER using ecrecover-based Schnorr multi-sig aggregation. This is the COMPLEMENT to our usual CONSUMER-side lens (CAPO price-caps). Reusable for any Chronicle-style / Schnorr-aggregate / optimistic-oracle provider.
What is bound into the SIGNED message? Grep the message constructor.
- Scribe: H(wat, val, age). Bound: feed-tag + value + timestamp. NOT bound: the signer set.
- Ask: can a valid sig be replayed for a different (val, age, chain, contract)? If wat/chainid/
contract-addr is NOT in the message, suspect cross-instance or cross-chain replay. (Scribe binds
wat per-instance -> per-feed isolation; no chainid but each deploy is a distinct contract with
distinct wat, and age-monotonicity blocks same-contract replay.)
- TEST: sign for (val,age1), assert verify==false for (val,age2) and (val2,age1).
How is the aggregate public key derived? Is the signer SET signed, or RE-DERIVED by the
verifier from caller-supplied ids?
- Scribe: verifier recomputes aggPubKey = sum(pubKeys[feedId]) from the supplied feedIds.
A wrong set just fails verification. Substituting a set that sums to the same key = rogue-key /
linear-private-key-relationship = the FENCED trust assumption (mitigated by proof-of-possession
ECDSA at lift). Confirm the lift-time PoP exists; if it does, rogue-key is OOS.
Double-sign / threshold-fake guard. Can one signer be listed bar times to fake the
threshold? Look for a bloom filter / sorted-strictly-increasing-id check.
- Scribe: bloom bitset, first id seeds it, loop rejects repeats; requires len == bar exactly.
- TEST: list the same id twice, assert verify==false.
Signature malleability / degenerate inputs. Check the verify primitive rejects:
s == 0, commitment/R == address(0), s >= Q (low-s / order bound), off-curve pubkey,
zero challenge. For the ecrecover-as-ecmul trick (Vitalik's), r = Px must be a valid nonzero
field element < Q. Scribe guards all of these (LibSchnorr 28-50). On a multi-audit codebase these
are present -> 0 signal; do NOT re-grind without a NEW angle.
Optimistic challenge SYMMETRY (the single most informative test). For opPoke/opChallenge:
- VALID opPoke must NOT be challengeable (no griefing the honest poker; feed stays, no reward).
- INVALID opPoke MUST be challengeable (feed dropped + reward paid).
Both directions must hold. The challenge must re-verify the EXACT committed schnorrData
(_schnorrDataCommitment = keccak(sig,commitment,feedIds)) reconstructing the ORIGINAL signed
message (Scribe stores _originalOpPokeDataAge to rebuild H(wat,val,originalAge)).
Watch for: calldata-encoding ambiguity letting a valid opPoke be challenged with a re-encoded
schnorrData (Scribe FIXED this in v2.0.0 - "non-default calldata encoding" bypass). On a fresh
fork, re-test this with a manually re-encoded bytes feedIds.
- TEST: both H2a (valid not challengeable) and H2b (invalid challengeable).
Finalization read-gate + staleness. Confirm the read path serves the PRIOR finalized value
during the challenge window (a bad unchallenged opPoke only becomes readable after the period),
and that age is strictly monotonic (no rewind) and <= block.timestamp (no future).
On a target shipping >=3 audits (Scribe ships ABDK + Cantina x2 + ChainSecurity), grinding crypto- primitive edge cases (point-at-infinity, zero challenge, malleability bound) yields ~0 signal - the guards are present. Spend budget on: (a) calldata-encoding ambiguity in the challenge path, (b) cross-instance/cross-chain message replay if chainid/contract not bound, (c) economic griefing of the challenge-reward mechanism. Frame as LEARN-MODE; walk-with-lessons is the good outcome.
script/libs/LibFeed.sol (newFeed, signSchnorr, signECDSA) to mint
valid signatures - do not hand-roll secp256k1.uint8(uint160(addr) >> 152); pick privkeys until you get distinct ids before lifting.StaleMessage if pokeData.age <= block.timestamp at genesis fork time -
vm.warp(block.timestamp + 1 hours) before constructing opPoke pokeData.vm.deal) so challengeReward() is non-zero to assert reward payment.