Order-book depth the protocol itself will not let you withdraw.
A resting order on Somnia Shannon (chain 50312), built on DreamDEX's
binary event-contract pools, whose owner is a contract with no cancel path β so the pool refuses
the very wallet that paid for it. The centrepiece is a transaction that
failed, permanently on a public chain:
0x959b4770β¦6ddb
(status 0x0). 93 tests, 0 failures, src/ at 100% coverage.
Reproduce it with no wallet and no gas β see Live Deployment.
Judges β start here: Repo Β· Typed-book viewer Β· Demo video (2:44) Β· Evidence trail Β· SDK & docs feedback (12 findings) Β· BUIDL #48111
The reason column is the product; the badge is only its summary.
One command, offline and deterministic: the same answer on any machine.
Anyone can replay the refusal β no wallet, no gas. The pool names both parties in its own revert data.
rampart.edycu.dev/viewer β or open it locally, no server required:
open site/viewer/index.html # no server, no build step, no network requiredA self-contained page that renders a real Somnia book with every level typed. It ships a snapshot
pinned to block 468201000 β 11 resting orders, 2 FIRM, 0.1% of displayed depth cannot be
withdrawn β the same numbers node script/firmness.mjs --block 468201000 prints, from the same
engine. Every row carries the reason for its class (DELEGATECALL present (1),
forbidden selector setOperatorApprovalForPool(β¦), attested + locked), not just a badge.
UNVERIFIED is drawn as a no-claim state β dashed outline, hollow glyph, and the words "Claim:
none" β deliberately not as a negative one. Class is never carried by colour alone.
A Live check button re-reads the pool at latest from your browser and reclassifies. Run it today
and it will honestly report an empty book: the testnet pool has cycled, and 0% over zero orders is
vacuous, not a finding. The page says exactly that rather than showing you a zero.
Regenerate the snapshot at any block: node site/viewer/make-snapshot.mjs --block <n>.
On every exchange on earth, displayed depth is a promise. The market maker showing you a bid can pull it the moment you need it most, and you have no way to tell in advance which levels will hold.
Typing a level as firm because EXTCODESIZE(owner) > 0 does not fix this β it is forgeable. A
contract can hide a cancel, sit behind an upgradeable proxy, DELEGATECALL to an attacker target, or
grant an operator after resting. All four display as FIRM under a naive check and are fully pullable,
which is worse than no signal: it launders unreliable liquidity through your own metric.
A FirmQuote contract holds collateral, approves a Somnia BinaryPool, and calls
placeBinaryOrder β so the resulting Order.owner is the contract, not a wallet.
The contract exposes no cancel path, no reduce path, and never grants an operator. Every route that could withdraw the resting order is therefore closed:
| Withdrawal path | Who may call it | Source |
|---|---|---|
cancelOrder(uint128) |
the order owner only β a non-owner reverts IncorrectSender 0xf5e39c1f |
errors |
cancelOrderFor(owner, id) |
an operator the owner approved. "Unlike placeOrderFor, the system-contract allowlist does not admit callers here β only the owner's per-user approval does." |
functions |
reduceOrderFor(owner, id, qty) |
"Per-user approval only (no system allowlist)." | functions |
The quote stands until a taker fills it or its mandatory expireTimestampNs lapses. Not even the
wallet that paid for it can take it back.
Order.owner is readable on-chain, so every price level can be typed:
| condition | claim | |
|---|---|---|
| FIRM | owner is a contract whose EXTCODEHASH is attested, still inside its lock window |
this depth cannot be withdrawn |
| PULLABLE | owner is a wallet | this depth can vanish in one block |
| UNVERIFIED | owner is a contract we have not attested | no claim is made |
"Percent of book that cannot be withdrawn" is a liquidity-quality observable that exists on no other exchange. Off-chain books β Polymarket, Kalshi, every CEX β cannot expose it: their resting orders are signed messages inside a private matching engine, with no on-chain owner to inspect.
UNVERIFIEDis load-bearing. Under our policy the four forgeries above mint UNVERIFIED depth instead of FIRM, and UNVERIFIED makes no claim.What that is not: a proof of irrevocability.
FIRMmeans attested and inside its lock window, and attestation is a human-reviewed transparency list βanalyze()is the pre-filter that rejects the obvious escapes, never the sole gate. A selector built arithmetically at runtime evades any static scan, so a greenanalyze()is a precondition for review, not a substitute for it. See Honest limits.
Same diagram as mermaid source
flowchart LR
subgraph CHAIN["Somnia Shannon Β· chain 50312 Β· 100 ms blocks"]
EOA["Funder EOA<br/>pays for the order"]
FQ["<b>FirmQuote</b><br/>no cancel Β· no reduce<br/>no operator grant"]
POOL["<b>BinaryPool</b><br/>holds all escrow<br/>enforces Order.owner"]
ADV["Adversarial corpus S1βS6<br/>each looks firm,<br/>each has a real escape"]
end
subgraph ENGINE["script/ β off-chain engine, zero runtime deps"]
BOOK["read the resting book<br/><i>getAllOpenOrdersOffChain</i>"]
CODE["EXTCODEHASH per<br/>distinct owner"]
POLICY["<b>analyze()</b> static policy<br/>no DELEGATECALL Β· no SELFDESTRUCT<br/>no forbidden selector Β· not a proxy"]
HUMAN{{"human review<br/>β the policy is a filter,<br/>not a prover β"}}
SET["attested code-hash set"]
CLS["<b>classify</b><br/>FIRM Β· PULLABLE Β· UNVERIFIED"]
PCT["<b>Ξ£ firm Γ· Ξ£ displayed</b><br/>% of book that<br/>cannot be withdrawn"]
end
EOA -->|"funds, then CANNOT cancel<br/>IncorrectSender 0xf5e39c1f"| FQ
FQ -->|placeBinaryOrder| POOL
ADV -->|placeBinaryOrder| POOL
POOL --> BOOK --> CODE --> CLS
CODE --> POLICY --> HUMAN --> SET --> CLS
CLS --> PCT
style FQ fill:#0b3d3d,stroke:#22d3ee,color:#e6fbff
style POOL fill:#1e1b4b,stroke:#818cf8,color:#eef2ff
style ADV fill:#3f1d2e,stroke:#fb7185,color:#ffe4e6
style PCT fill:#0b3d3d,stroke:#22d3ee,color:#e6fbff
style HUMAN fill:#3d2f0b,stroke:#fbbf24,color:#fffbeb
The left half runs on-chain and is what the proof rests on: the pool refuses the funder because
Order.owner is a contract with no code path to ask. The right half is off-chain and read-only β
it never sends a transaction, so anyone can re-derive every classification from public chain state.
The amber node is the honest seam: analyze() rejects the known escapes, a human decides what gets
attested. See Honest limits.
| Layer | Technology | Why |
|---|---|---|
| Contracts | Solidity 0.8.28 Β· Foundry | cancun target, so EIP-6780 closes the SELFDESTRUCT escape at the EVM level |
| Chain | Somnia Shannon (50312) | 100 ms blocks are what make live full-book retyping plausible |
| Protocol | DreamDEX BinaryPool Β· ERC-6909 |
the pool holds all escrow and enforces Order.owner β that enforcement IS the product |
| Off-chain engine | Node β₯ 20, zero runtime deps | hand-rolled keccak-256 + raw JSON-RPC, so anyone can re-derive a classification |
| Sponsor SDK | @somnia-chain/markets-sdk (dev dep) |
CI-only differential against the hand-transcribed ABI β see below |
| Verification | halmos Β· Foundry invariants Β· Slither Β· CodeQL | the security property is an absence, so it is attacked three ways |
The off-chain engine has no runtime dependencies: it hand-rolls keccak-256 and speaks raw JSON-RPC, so anyone can re-derive a classification with nothing but Node and a public endpoint.
That buys verifiability and costs a risk: a hand-transcribed ABI rots silently. A uint256 where the
pool wants a uint96 produces a different function selector, and the call reverts with nothing
decodable. We hit exactly that during the build.
So @somnia-chain/markets-sdk is a dev dependency, and script/sdk-verify.mjs is the bridge β
the shipped engine stays dependency-free while CI proves the hand-written surface still agrees with
the sponsor's own source of truth:
npm run sdk-verify # 35/35 β included in `npm run verify`| It checks | Against |
|---|---|
placeBinaryOrder's exact 9-arg signature, and that the uint256 variant does not exist |
binaryPoolWriteAbi |
all 7 FORBIDDEN_SELECTORS are the true keccak of the signatures we claim |
our own keccak, re-derived |
CANCEL_ORDER_FOR_SELECTOR = 0xe37b444b |
the SDK constant β also the constant in test/Adversarial.t.sol |
the buy-side-only invariant kind β {0, 2} |
the SDK's ORDER_KIND.BUY_YES / BUY_NO |
the pool really exposes cancelOrder / reduceOrder / approveBuilder |
binaryPoolWriteAbi |
the enum values FirmQuote hardcodes: orderType 0 still rests, and selfMatchingOption 0 still cancels the taker (if it meant maker, the depositor could erase its own "irrevocable" quote by self-crossing) |
ORDER_TYPE / SELF_MATCHING_OPTION |
IncorrectSender = 0xf5e39c1f, and that (sender, expected) is the word order the proof decodes |
the SDK's shipped error ABI |
the book reader's selector and the exact 8-field Order layout it decodes by word offset |
the shipped readsAbi.ts |
the ERC-6909 setOperator our policy bans is the sponsor's own |
erc6909Abi |
A passing run also re-verifies our keccak against the sponsor's published selector constants β the hash implementation and the ABI transcription check each other.
It reports two things it cannot verify rather than passing them silently: four forbidden selectors
absent from the SDK's curated write ABI (reached directly on-chain β S6 does exactly that), and
PLACE_ORDER_FOR_SELECTOR, which the package exports without any ABI entry that would let a
consumer derive it. That last one is filed as finding 9 in our SDK feedback report.
Two things cost real time and neither is in the prose docs, which document SpotPool:
- A binary pool has
placeBinaryOrder, notplaceOrder. The generic entry point revertsUseBinaryPlacement. The YES/NO side is an explicitkindparam andpriceis always quoted in YES terms. builderFeeBpsTimes1kmust beuint96. It is selector-critical β auint256there produces a different function selector and the call reverts with nothing decodable.
Both were read out of markets-sdk/src/tradeAbi.ts, not the documentation.
The engine is packaged β 23.2 kB, 11 files, zero runtime dependencies:
npm install github:edycutjong/rampartimport { analyze, attestedClassify, readBook, selectorOf } from 'rampart-firm-book';Subpath exports (rampart-firm-book/analyzer, /keccak, /classify, /book, /rpc) are available
if you want one piece. Runnable examples and the honest limits are in
script/lib/README.md. Not published to npm β install from the repo.
A BinaryPool accepts a contract as Order.owner, and the funder cannot take the order back.
Order β¦9685 rested with real escrow while its own funder's cancel reverted:
0xf5e39c1f IncorrectSender(
caller = 0xFbc73Ce1β¦3595 β the wallet that paid for the order
expected = 0x2a09b4c4β¦191a β FirmQuote
)
The explorer, unedited. Failed Β· f5e39c1f Β· and the pool names both parties itself.
The failed transaction is public and permanent:
0x959b4770β¦6ddb (status 0x0).
Control: the same call --from the contract returns 0x β the pool would allow its owner to
cancel; the owner simply has no code path to ask.
Full evidence, including why the first attempt was invalid and was re-run: DEMO.md.
Verify it yourself with no wallet, no funds, no gas:
cast call 0x1b8ed5380a4741df019acf5faa0ce6ecbf6167ee "cancelOrder(uint128)" \
129127208515966879685 --from 0xFbc73Ce1C0B43f87cD065f82df24697dEc653595 \
--rpc-url https://api.infra.testnet.somnia.network
# β execution reverted, data: 0xf5e39c1fβ¦ β IncorrectSender. The pool refuses the funder.Read the returned data carefully β we would rather you did.
0xf5e39c1fisIncorrectSender(address,address). Run without a block pin today and the decodedexpectedfield is0x00β¦00: the right selector, but the weak proof ("no such order"), because that order has since expired out of the book.Add
--block 465697720for the strong decode β Somnia's public RPC is archival, so this works against the same endpoint:cast call 0x1B8eD5380a4741df019acf5FAa0Ce6eCbf6167Ee "cancelOrder(uint128)" \ 129127208515966879685 --from 0xFbc73Ce1C0B43f87cD065f82df24697dEc653595 \ --block 465697720 --rpc-url https://api.infra.testnet.somnia.network # β reverted 0xf5e39c1f # caller = 0xfbc73ce1c0b43f87cd065f82df24697dec653595 # expected = 0x2a09b4c474828e6895af273e51ba8c181c91191a β the FirmQuote contractThat is the claim in its strong form: the pool named an owner different from the caller, and that owner was the contract. The permanent proof is the mined transaction above β transaction history cannot expire β and the pinned call reproduces the same state on demand.
firmness.mjs --block 468201000 β pinned, so it prints these same numbers forever.
The ternary classifier is not only an off-chain script β it is live on Shannon, and its answers are a public call away. No wallet, no gas:
REG=0x04aDbfC40dD10215Ee7b7D14B0aD74074a83f8C1
RPC=https://api.infra.testnet.somnia.network
cast call $REG "attester()(address)" --rpc-url $RPC
# 0xFbc73Ce1C0B43f87cD065f82df24697dEc653595 β immutable; there is no setter
cast call $REG "classify(address)(uint8,bytes32,uint64)" \
0x8116c3a4DE042D4A215B532B7C4054F36e074B68 --rpc-url $RPC
# 1 Β· 0xc60110e0β¦58b5 Β· 1787443200 [0 PULLABLE Β· 1 UNVERIFIED Β· 2 FIRM]| Registry | 0x04aDbfC4β¦f8C1 Β· deploy 0xedddeb8fβ¦9e1d |
| Attestation | 0xe5f061f6β¦e69b β binds the LIVE FirmQuote runtime hash 0xc60110e0β¦58b5 |
| Record hash | 0xcecd1f5cβ¦18b0 β keccak of analyze()'s full JSON, so the published reasoning cannot be swapped later |
| Full record | script/registry.deployed.json |
Read that 1 carefully β it says UNVERIFIED, and that is the point. S0's unlockAt was
2026-08-23 00:00 UTC and has lapsed, so a lock that no longer binds is not firm. The on-chain
classifier reaches exactly the same verdict as the off-chain engine, for exactly the same reason.
A registry that returned FIRM here would be the broken one.
The attester is an immutable constructor argument β no setter exists. The registry's honesty is
auditable rather than governed, and attestation remains a human-reviewed transparency list: a green
analyze() is a necessary pre-filter, never a proof of irrevocability.
node script/headline.mjs # attested classifier 8/8 Β· naive EXTCODESIZE 2/8The corpus is the real FirmQuote, six attacker contracts that each look firm to a naive check
(hidden cancel, EIP-1967 proxy, DELEGATECALL, late operator grant, quiet reduceOrder, and cancel
via an alternate selector), and a plain wallet. The attested-EXTCODEHASH classifier types all eight
correctly; the naive EXTCODESIZE > 0 check is fooled by all six contract attacks. The whole corpus
is deployed on Shannon, and five of the six escapes are executed as real transactions β the
sixth (late operator grant) is rested on-chain with its exact blocked state documented. See
DEMO.md and script/corpus.deployed.json.
| Metric | Value |
|---|---|
| Foundry tests | 93 passing, 0 failures |
src/ coverage |
100% line / statement / branch / function (168/168 lines) |
| Symbolic proofs | 5 (halmos) β quantify over every caller and every timestamp |
| Invariant campaigns | 3, 128k call sequences over the bytecode's real dispatch surface |
| Off-chain checks | 17 (node script/test.mjs) |
| Sponsor-SDK differential | 35/35 (npm run sdk-verify) |
| Full-book retype | p95 0.13 ms on a deterministic 2,000-order book, inside a 100 ms block |
| Escapes executed on-chain | 5 of 6 |
forge test Β· npm run sdk-verify β neither number is transcribed by hand.
src/FirmQuote.solβ a resting quote the pool will not let its funder withdraw. Buy-side only by design: a sell escrows outcome tokens, which needs an ERC-6909setOperatorgrant, and granting no operator is what keeps the lock airtight. (42 unit tests incl. seven asserting the absence of every withdrawal selector, 3 invariant campaigns over 128k call sequences, and 5 halmos symbolic proofs β the security property is an absence, so it is attacked three different ways.)src/FirmnessRegistry.solβ the ternary classifier (FIRM / PULLABLE / UNVERIFIED) expressed as a Solidity contract: attested-EXTCODEHASHset +classify/classifyBatchwith the lock-window horizon. (22 tests.) Deployed and seeded on Shannon 2026-08-26 β see Verify the registry yourself.src/adversarial/*.solβ six attacker contracts, each with a real working escape proven against a faithful mock pool. (26 tests; every adversarial source file is at 100% line, statement, branch and function coverage.)script/β the off-chain engine: a dependency-freekeccak256, an EVM disassembler + static bytecode policy (analyze), the FIRM/PULLABLE/UNVERIFIED classifier, the headline comparison, the firmness % over a live market, and the bench.node script/test.mjsβ 17 checks, including four that pin the analyzer's known evasions; the analyzer's hash matches on-chainEXTCODEHASH.src/IBinaryPool.solβ the pool surface, transcribed from@somnia-chain/markets-sdk, and checked against it in CI byscript/sdk-verify.mjs(35/35).gate.shβ the day-1 go/no-go against Somnia Shannon testnet. Run 2026-08-19: PASSED.
Honest edges (detailed in DEMO.md β Honest limits): five of the six attacker escapes
execute a full on-chain withdrawal; the sixth β the operator-grant β has its grant executed and
verifiable in the registry, but the binary pool rejects cancelOrderFor from any operator, so
that route cannot withdraw here. That is a finding about the pool, not a gap we papered over, and the
mechanism is proven in unit tests. The 8/8-vs-2/8 classification is computed from live on-chain
EXTCODEHASH and does not depend on the escapes running.
| Foundry | forge β₯ 0.2 |
contracts + tests |
| Node.js | β₯ 20 | the off-chain engine (uses global fetch) |
git |
any | submodules β forge-std is vendored as one |
# 1. Clone WITH submodules (lib/forge-std is a submodule β a plain clone will not build)
git clone --recurse-submodules https://github.com/edycutjong/rampart.git
cd rampart
# already cloned without them? git submodule update --init --recursive
# 2. Install Foundry, if you do not have it
curl -L https://foundry.paradigm.xyz | bash && foundryup
# 3. Contracts: build + test β 93 passing, 0 failures
forge build
forge test
# 4. Off-chain engine: dev deps only, the engine itself has ZERO runtime dependencies
npm ciNo API key, no wallet, and no funds are needed for any of the above β every command is offline or read-only. Optional, only if you want to re-run the on-chain gate yourself:
export SOMNIA_TESTNET_RPC=https://api.infra.testnet.somnia.network # the default; override to use your own
export PRIVATE_KEY=0xβ¦ # a funded Shannon key β ONLY for ./gate.sh| Symptom | Cause | Fix |
|---|---|---|
Source "forge-std/Test.sol" not found |
cloned without submodules | git submodule update --init --recursive |
headline.mjs --live prints 7/8 and exits 1 |
S0's lock lapsed 2026-08-23 β correct behaviour | add --block 468201000 |
firmness.mjs exits 1 with "0 resting orders" |
the testnet pool has cycled | --block 468201000, or node script/find-pool.mjs for a live one |
| pinned run reports "block PREDATES its deployment" | pinned earlier than the corpus deploy | pin β₯ 468201000 |
forge test # 93 passing
npm run prove # 5 symbolic proofs (halmos)
npm run verify # syntax + lint + typecheck + 17 off-chain checks + sdk 35/35 + headline 8/8
# The live classifier, pinned so it reproduces exactly (Somnia's public RPC is archival):
node script/headline.mjs --live --block 468201000 # 8/8 attested vs 2/8 naive, off-chain EXTCODEHASH
node script/firmness.mjs --block 468201000 # 11 orders, 2 FIRM β 0.1% of the book is firm
PRIVATE_KEY=0xβ¦ POOL=0xβ¦ ./gate.sh # the day-1 gate β steps 4 and 5 SUCCEED BY REVERTINGgate.sh step 4 has the funding wallet attempt pool.cancelOrder on the contract's own order. That
transaction is supposed to fail, and the failed transaction on the explorer is the proof β an
artifact that cannot be mocked.
CI runs six stages on every push: contracts, a β₯80% coverage gate, the classifier self-test, lint + typecheck + the sponsor-SDK differential, the 100 ms bench gate, and a pinned live on-chain proof that asserts 8/8 against Shannon. Slither, CodeQL, gitleaks over the full history, and Dependabot run alongside.
| π¬ Demo video (2:44) | youtu.be/DhxuWFHOsyM |
| π Live site | rampart.edycu.dev |
| π Typed-book viewer | rampart.edycu.dev/viewer |
| π Pitch deck | rampart.edycu.dev/pitch |
| π Full evidence trail | DEMO.md |
Every command in the video was really run and captured raw; the terminal scenes are frame-stepped replays of those captures, labelled on screen as replays. Nothing is sped up, and no result is staged.
MIT β see LICENSE.


