- Somnia Shannon · chain 50312
- mined 2026-08-19 · permanent
- status 0x0 — reverted on purpose
The wallet that paid for this order tried to cancel it.
The chain said no.
rampart makes order-book depth the protocol itself will not let you withdraw. A FirmQuote contract owns the resting order and has no cancel function — so the pool refuses everyone, including its own funder.
classifier accuracy
8/8
naive EXTCODESIZE check: 2/8
forge test
70
0 failures · 87.97% line coverage
full-book retype p95
0.25 ms
against a 100 ms block budget
adversarial corpus
6
deployed attacker contracts · 4 escapes executed on-chain
A failed transaction is the only proof that cannot be staged
It failed, it was mined, and it is permanently public. Nothing here is mocked, replayed, or reconstructed.
$ cast call 0x1b8ed538…67ee "cancelOrder(uint128)" 129127208515966879685 \
--from 0xFbc73Ce1…3595 # the funder's own wallet
execution reverted, data:
0xf5e39c1f IncorrectSender(
caller 0xFbc73Ce1C0B43f87cD065f82df24697dEc653595 ← the wallet that PAID for the order
expected 0x2a09b4c474828e6895af273e51Ba8C181c91191a ← FirmQuote — a contract with no cancel function
)
The pool names both parties in its own revert data. It knows exactly who is asking — the wallet that funded the order — and it refuses, because the order's owner is the contract. That error message is the product. Open the transaction on the explorer ↗
from the funder wallet
0xFbc73Ce1…3595 (EOA)
execution reverted · 0xf5e39c1f IncorrectSender
from FirmQuote itself
0x2a09b4c4…191a (contract)
0x — no revert. The pool would let its owner cancel.
Same pool, same order, same calldata. So this is not "orders can't be cancelled here" — the pool would honour a cancel from the owner. The owner is a contract with no code path to ask. That distinction is the entire mechanism.
Three moves close every exit
The trick is not what the contract does — it is what it cannot do.
Hold & approve
FirmQuote holds the collateral and approves the Somnia BinaryPool. The funder's wallet only ever funds the contract.
Rest as the owner
The contract calls placeBinaryOrder, so the order's owner is the contract — not any wallet.
No way to ask
No cancel path, no reduce path, no operator ever granted. The pool will only take instructions from an owner that cannot give them.
cancelOrder(uint128)
Owner-only. Any other caller — including the funder — reverts IncorrectSender 0xf5e39c1f. Proven above, on-chain.
cancelOrderFor(owner, id)
Needs an operator the owner approved. FirmQuote never grants one — there is no code path that could.
reduceOrderFor(owner, id, qty)
Per-user approval only, no system allowlist. Same closed door.
The quote stands until a taker fills it or its mandatory expireTimestampNs lapses. Two exits — neither of them is "the maker changed their mind." Seven of the 70 tests assert the absence of every withdrawal selector from FirmQuote's ABI: the lock is tested as a missing feature, because it is one.
A book where every level tells you if it can run away
Order.owner is readable on-chain, so every price level can be typed by who owns it.
FIRM
Owner is a contract whose EXTCODEHASH is attested, still inside its lock window.
Claim: this depth cannot be withdrawn.
PULLABLE
Owner is a wallet.
Claim: this depth can vanish in one block.
UNVERIFIED
Owner is a contract we have not attested.
Claim: none. No claim is made — and that refusal is the point.
can vanish in one blockthe pool will not release it
UNVERIFIED sits off this axis on purpose. Every escape in our corpus mints UNVERIFIED depth, not FIRM — it renders as exactly what it is. FIRM means attested and locked, and attestation is a human-reviewed list: the static filter rejects the known escapes, it does not prove irrevocability.
Why UNVERIFIED is load-bearing
A naive "owner is a contract → firm" check is forgeable: a hidden cancel, an upgradeable proxy, a DELEGATECALL, or an operator granted after resting would all fake FIRM. Attestation binds to exact runtime bytecode, so every one of those mints depth that carries no claim. The honest limit: a static scan cannot see a selector built at runtime, so attestation is human-reviewed rather than automatic.
An observable that exists nowhere else
Percent of the book that cannot be withdrawn is a liquidity-quality signal no other exchange can publish. Off-chain books — Polymarket, Kalshi, every CEX — keep resting orders as signed messages inside a private matching engine, with no on-chain owner to inspect.
The viewer renders pinned chain state from block 468,201,000 — 11 resting orders, 2 FIRM, 0.1% of the book unwithdrawable — with the reason behind every label, and a live re-check that runs the same classification in your browser.
Six contracts that all look firm. One classifier catches every one.
The corpus is FirmQuote, six deployed attacker contracts that each look firm to a naive check, and a plain wallet. Four of the six escapes were executed on-chain, not just unit-tested.
attested EXTCODEHASH
8/8
Every owner typed correctly — including all six contracts engineered to look firm.
naive EXTCODESIZE > 0
2/8
Fooled by all six contract attacks. It only gets the two trivial ends right.
| # | Owner | The escape it hides | Attested | Naive |
|---|---|---|---|---|
| S0 | FirmQuote | none — no cancel path exists | FIRM ✓ | FIRM ✓ |
| S1 | HiddenCancel | a private cancelOrder behind an innocuous method | UNVERIFIED ✓ | FIRM ✗ |
| S2 | Erc1967Proxy | upgrade the implementation after resting | UNVERIFIED ✓ | FIRM ✗ |
| S3 | DelegateEscape | DELEGATECALL into attacker code | UNVERIFIED ✓ | FIRM ✗ |
| S4 | OperatorGranter | grant an operator after the order rests | UNVERIFIED ✓ | FIRM ✗ |
| S5 | QuietReduce | shrink the depth with reduceOrder | UNVERIFIED ✓ | FIRM ✗ |
| S6 | BatchCancel | cancel through an alternate batch selector | UNVERIFIED ✓ | FIRM ✗ |
| S7 | plain wallet | cancels trivially, any time | PULLABLE ✓ | PULLABLE ✓ |
The naive column is computed the same way, from the same chain state, in the same run — so the comparison is arithmetic, not rhetoric. Reproduce it with node script/headline.mjs.
0.13 ms
p95 to retype the full book, against a 100 ms block budget — three orders of magnitude of headroom.
70 / 0
forge test — 70 tests passing, 0 failures, at 87.97% line coverage.
5 of 6
attacker escapes executed as real transactions on Somnia Shannon. The other two are documented, not hidden — see honest status.
One read-only call. No wallet, no funds, no gas.
The RPC answers anyone. Ask the pool to cancel the resting order as the wallet that paid for it, and watch the chain refuse.
cast call 0x1b8ed5380a4741df019acf5faa0ce6ecbf6167ee "cancelOrder(uint128)" \
129127208515966879685 \
--from 0xFbc73Ce1C0B43f87cD065f82df24697dEc653595 \
--rpc-url https://api.infra.testnet.somnia.network
execution reverted, data:
0xf5e39c1f
000000000000000000000000fbc73ce1c0b43f87cd065f82df24697dec653595 ← caller (the funder)
0000000000000000000000002a09b4c474828e6895af273e51ba8c181c91191a ← expected (FirmQuote)
$ cast 4byte 0xf5e39c1f
IncorrectSender(address,address)
The control
Swap --from to the contract's own address — 0x2a09b4c4…191a — and the same call returns 0x. The pool would let its owner cancel; the owner just has no way to ask.
Replay the whole thing
./gate.sh runs deploy → rest → refuse end-to-end with your own key. forge test runs all 70 tests. Every command and its expected output is in DEMO.md.
What is here is real; what is not here is named
Calibrated honesty is part of the engineering, not a disclaimer bolted to the end of it.
Real today
- FirmQuote — deployed to Shannon at 0x2a09b4c4…191a, chain 50312
- The on-chain refusal — the funder's cancel reverted 0xf5e39c1f, mined and permanent
- The control — the same call from the contract's address returns 0x
- The classifier — attested 8/8 vs naive 2/8 over the deployed corpus
- 6 attacker contracts deployed — 5 of the 6 escapes executed on-chain
- 70 tests, 0 failures · 87.97% line coverage · retype p95 0.13 ms
- DEMO.md — every transaction, the control, and exact reproduce steps
Named, not claimed
- Testnet only — nothing is on Somnia mainnet, and nothing has been audited
- No firmness percentage on this page. It is a live measurement over a live market; a static page cannot honestly freeze one. script/firmness.mjs computes it on demand
- Sell-side quoting — deliberately out: it needs an ERC-6909 operator grant, and granting none is what keeps the lock airtight
- 2 of the 6 escapes did not execute — one is blocked by the pool's own unwired operator path (a genuine SDK finding), one by a dry native faucet. Both are proven in unit tests and written up, not hidden
- The registry is a transparency list, not a trustless oracle — anyone can re-derive it from the same bytes
- Demo video · external users — none yet
The whole story is on the explorer
All on Somnia Shannon (chain 50312), all mined 2026-08-19.