Book head-clog of expired orders can force hybrid takers into the scan-step cap #289
Labels
No labels
agent:fix_bugfix
agent:fix_conflicts
agent:fix_security
agent:gap_analysis
agent:implement
agent:implement
agent:implement
agent:open_issues
agent:ready
agent:research
agent:security_audit
agent:verify
architecture
backend
blocker:hybrid
blocker:launch
blocker:limit-orders
blocker:v2
block:log_only
block:security
bug
ci
contracts
correctness
deploy
dev
devops
docs
documentation
duplicate
e2e
enhancement
epic
feature
frontend
functional-completion
gas
good first issue
governance
help wanted
high-risk
hooks
hybrid
indexer
infra
infrastructure
integrators
invalid
launch-blocker
limit-orders
localnet
localterra
low priority
missing-implementation
needs-design
ops
performance
priority
high
priority
medium
product
qa
QA
question
ready
ready
research
scripts
security
security-hardening
smartcontracts
tech-debt
testing
ux
UX
v2
verification
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
code/cl8y-dex-terraclassic#289
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Severity: Low
Reachability: Attacker places many short-expiry orders at the book head; cost is escrow + gas, reclaimable after expiry.
Affected: matcher scan cap interaction (
smartcontracts/contracts/pair/src/orderbook.rs,MAX_SCAN_STEPS/MAX_EXPIRED_PARKS_PER_SWAPindex-common/src/pair.rs).Summary
(Re-examined after an earlier pass leaned toward refuting this — I think there's a real, if minor, griefing vector.)
The matcher walks at most
MAX_SCAN_STEPS = 500nodes per swap, and parks at mostMAX_EXPIRED_PARKS_PER_SWAP = 15expired orders per swap; beyond 15 it just skips-and-advances (still burning a scan step). So if an attacker clogs the head with more than ~500 expired (or otherwise skipped) orders, a hybrid taker hits the 500-step cap before reaching real liquidity, while only 15 of the clog get cleared per swap. The book leg then under-fills — the taker degrades to pool-only or reverts on slippage — and it takes many swaps (≈ clog size / 15) to grind the clog down.No funds are stolen (the taker falls back or reverts), and the auto-park (#264) plus permissionless clean do eventually clear it — but the clear is slow, and
clean_limit_bookhas its own traversal issue (see the clean unbounded-scan report), so "just clean it" isn't free either. Net: a capital+gas griefing vector that degrades hybrid fills for a while. Filing it Low.Current codebase
dex-common/src/pair.rs:MAX_SCAN_STEPS = 500,MAX_EXPIRED_PARKS_PER_SWAP = 15.orderbook.rsmatcher:book_walk_stepbreaks atMAX_SCAN_STEPS; expired orders past the 15-park cap are skipped (expired_parks_skipped) but still advance and consume a step.Recommended direction
book_start_hintis side-validated (the cross-escrow report), a legit taker can use it to start past a clogged head — but that depends on that fix landing first.Acceptance criteria
Test plan (attack / abuse)
1 & 2 approved for now. Document that if this attack does occur in the future, we may need to add a small LUNC fee per order (say 10 LUNC) that is transferred to treasury. But at this time not likely to be needed.
Implementation plan (your approved dirs 1+2). Medium, contract-only. Gated on #274.
MAX_EXPIRED_PARKS_PER_SWAP. Apply symmetrically to BOTHmatch_bidsandmatch_asks(structurally identical), and keep execute/simulatescan_steps_cappedparity. Files: orderbook.rs (expired branch, both legs), dex-common/pair.rs (budget const + gas-envelope justification like #254).book_start_hintis already side-validated (#272/dd70ea3), so a legit taker can already start past a clogged head; it's just a matter of the frontend/indexer supplying a live-side hint.Precedent:
book_walk_stepscan-cap +match_bids_scan_steps_cap_bounds_expired_prefix_walk(orderbook.rs:3557) + simulate parity (3602);unlink_order/detach_limit_order_from_booksplice for the bulk-unlink; theplace_expired_bidsharness (limit_order_tests.rs:1742). Gated on #274 — full closure ("clearing a clog doesn't depend on an unbounded clean traversal") needs #274's bounded clean to land first. Tests: >MAX_SCAN_STEPS expired head + one live order past it → single swap reaches+fills the live order. @PlasticDigitsPicking this back up now that #274 landed (it was the gate). Where #289 actually stands after reading the matcher park path closely:
Dir 2 (no code) is the real mitigation and it's already in place. A hybrid taker skips a head clog by passing a live-side
book_start_hint— the matcher seeds its walk fromresolve_match_start_hint(orderbook.rs:1376/1545/1701), which since #272/dd70ea3 only accepts a SAME-side hint. So a taker can start past the expired clog: no parks, no scan-step exhaustion. As long as the frontend/indexer hands a live-side hint, the taker isn't stuck behind the clog.#274 (MR !753) now covers the keeper path. A keeper clears an arbitrarily long expired clog in bounded, resumable chunks (
max_steps+resume_cursor), without the old unbounded-traversal risk.Dir 1 (matcher change) — honest assessment. The per-expired-order cost is dominated by the
EXPIRED_LIMIT_CLAIMS.save(the maker's refund row) + event, NOT the DLL pointer rewrites. So "bulk-unlink the contiguous expired prefix in one splice" only saves the pointer cost, which isn't the expensive part — it wouldn't letMAX_EXPIRED_PARKS_PER_SWAPgo meaningfully higher. And raising that cap (15 -> N) is a gas-envelope tradeoff I don't want to size by guessing; it needs a live gas benchmark like #254/#262, on a redeploy.So between dir-2 (taker skips the clog) and #274 (keeper clears it, bounded), the clog is mitigated from both sides without touching the hot matcher path. My recommendation: treat #289 as covered by dir-2 + #274; if you still want a cap raise, I'll size it against a fresh gas benchmark on the next redeploy rather than guess. Dir-3 (per-order ~10 LUNC fee) stays future-only per your note. @PlasticDigits
mentioned in issue #309
marked as related to #309
Yes should check for cap raise on redeploy
mentioned in merge request !794
mentioned in commit
4c4c26846bVerification summary (#289)
Threat model: CONFIRMED (low).
MAX_SCAN_STEPS(500) +MAX_EXPIRED_PARKS_PER_SWAP(15) allow a capital+gas griefing vector: a deep expired head prefix can exhaust the scan budget before live liquidity on a head-only hybrid swap (scan_steps_capped=true; book leg under-fills; pool spillover or slippage revert). No fund theft.Accepted mitigations (per issue discussion + PlasticDigits approval): no matcher change in this pass; document operational defenses.
Acceptance criteria
book_start_hintseeds walk past expired prefix —hybrid_same_side_book_start_hint_still_matches. Without hint:hybrid_walk_scan_steps_cap_bounds_expired_prefix_and_spills_to_pool(degraded book, pool spillover). Small clog:hybrid_walk_three_expired_bids_all_parked_then_fills_live_bid.MAX_CLEAN_SCAN_STEPS(500) +resume_cursor—clean_limit_book_scan_cap_resume_reaches_tail;dex-common/limit_clean.rs.Test plan (issue table)
cargo test -p cl8y-dex-tests hybrid_walk_scan_steps_cap;cargo test -p cl8y-dex-tests hybrid_same_side_book_start_hint;cargo test -p cl8y-dex-pair match_bids_scan_steps_capcargo test -p cl8y-dex-tests expired_parks_benchmark;cargo test -p cl8y-dex-tests clean_limit_book_scan_capRepo changes (docs/guardrails)
MR documents mitigations in
docs/limit-orders.md,docs/contracts-security-audit.md(L5/L15/L17),skills/AGENTS_BOOK_MATCH_HINT_SECURITY.md,skills/AGENTS_TERRACLASSIC_GAS.md.Follow-ups (not blocking)
book_start_hint: null(hybrid_route_opt.rs) — product hardening to pass first live-side id.MAX_EXPIRED_PARKS_PER_SWAPon next pair wasm redeploy (PlasticDigits note).mentioned in merge request !795
mentioned in commit
c179fa92fbVerification summary (#289)
Agent:
verifyautomation (2026-06-05)Branch verified:
main@9d7331cThreat model: CONFIRMED (low severity). A deep expired head prefix (>
MAX_SCAN_STEPS= 500) can exhaust the hybrid book walk on a head-only swap (book_start_hint: null), settingscan_steps_capped=trueand under-filling the book leg (pool spillover or slippage revert). No fund theft.Accepted mitigations (per issue discussion): no matcher change in this pass; operational defenses documented.
Acceptance criteria
book_start_hintseeds walk past expired prefix —hybrid_same_side_book_start_hint_still_matches. Without hint: degraded but bounded —hybrid_walk_scan_steps_cap_bounds_expired_prefix_and_spills_to_pool. Small clog (≤15):hybrid_walk_three_expired_bids_all_parked_then_fills_live_bid.#274:MAX_CLEAN_SCAN_STEPS(500) +resume_cursor—clean_limit_book_scan_cap_resume_reaches_tail;dex-common/limit_clean.rs.Test plan (issue table)
cargo test -p cl8y-dex-tests scan_steps_cap→ 1 passed;cargo test -p cl8y-dex-tests hybrid_same_side_book_start_hint→ 1 passed;cargo test -p cl8y-dex-pair scan_steps_cap→ 2 passedcargo test -p cl8y-dex-tests expired_parks→ 1 passed;cargo test -p cl8y-dex-tests clean_limit_book_scan_cap→ 1 passedDocs / guardrails (no repo changes this run)
Mitigations already documented in
docs/limit-orders.md§ Expiry (head-clog griefing),docs/contracts-security-audit.md(L5/L15/L17),skills/AGENTS_BOOK_MATCH_HINT_SECURITY.md,skills/AGENTS_TERRACLASSIC_GAS.md.Follow-ups (not blocking closure)
book_start_hint: null(hybrid_route_opt.rs) — product hardening to pass first live-side id.MAX_EXPIRED_PARKS_PER_SWAPon next pair wasm redeploy (PlasticDigits note).Closing: all acceptance criteria met via accepted operational mitigations + bounded clean path; no code/docs changes required in this verification pass.
mentioned in issue #332
marked as related to #332
mentioned in merge request !816
mentioned in issue #424
mentioned in merge request !1004
mentioned in merge request !1009
mentioned in issue #707
mentioned in issue #708