No price band on limit orders: a 1e-18 dust ask at the book head overflows match math and reverts any crossing swap #467
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#467
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?
Came out of the #381 security sweep, poking at the limit-order path. There's no lower/upper bound on the price a limit order can be placed at, and the match loop divides by that price. A single dust-priced order parked at the best-price head of the book lets anyone brick book matching for every crossing swap.
What it is / where
validate_placement_iteminsmartcontracts/contracts/pair/src/limit_placement.rs:316-333is the only gate on a placed order's price, and all it does is:amount == 0(ZeroAmount)price == 0("limit price must be positive")expires_atNo minimum, no maximum.
grep -rniE "MIN_LIMIT_PRICE|MAX_LIMIT_PRICE|min_price|max_price|price_band"overcontracts/pair/srccomes back empty — the band doesn't exist anywhere, andexpand_limit_ladderdoesn't add one either.So the smallest positive price you can place is
Decimal::raw(1)= 1e-18.Why it blows up (the mechanism)
The match loop inverts the price to size a fill. Ask side,
orderbook.rs:1588-1600:Decimal::one() / Decimal::raw(1)= 1e18. Thentoken1_left.checked_mul_floor(1e18)internally doestoken1_left * 1e18inUint256/Uint128space and floors back toUint128.Uint128::MAXis ~3.4e38, so oncetoken1_left(the taker's remaining token1 budget on this leg) climbs past ~3.4e20 raw,token1_left * 1e18overflows theUint128result andchecked_mul_floorreturnsErr. That maps straight toContractError::InvariantViolation { reason: "ask mul_floor" }and the entire swap message reverts.The nasty part is placement order: an ask at
price = raw(1)is the lowest possible ask, so it sorts to the head of the ask book — the first maker any crossing swap walks. The loop hits it before it can fill anything real, and dies. Doesn't matter how deep or healthy the rest of the book is.Bid side is the same shape at
orderbook.rs:1429-1433:Here the overflow input is
order.remaining(the maker's own escrowed token1) rather than the taker's budget, so it's the weaker mirror — the attacker has to actually escrow a largeremainingto trip it, and can recover it by cancelling. The ask side is the real problem because the multiplicand is taker-controlled budget, not attacker-locked funds.How to actually hit it
price = Decimal::raw(1)(1e-18) with a trivial amount. Passesvalidate_placement_itemfine — amount and price are both non-zero. It lands at the ask-book head.checked_mul_floorbefore filling. For an 18-decimal token that's ~342 tokens on that leg — not exotic.No special privileges, one cheap order, and book matching for that pair is down until the dust order is expired/cleared.
Impact
Griefing DoS on the order-book match path — crossing swaps abort instead of filling. No fund loss or mispricing; it fails closed on the
InvariantViolation. But "any crossing swap reverts" on a launch pair is a real availability hit, and it's dirt cheap to sustain.Fix direction
Enforce a sane
[MIN_LIMIT_PRICE, MAX_LIMIT_PRICE]band invalidate_placement_item(and mirror it inexpand_limit_ladderso ladder rungs get checked too), rejecting anything outside it before the order ever reaches the book. Pick the min so that1/pricecan't overflowUint128 * price_invagainst any plausibletoken1_left/remaining, and cap the max symmetrically for the reciprocal case. That kills both the head-of-book ask vector and the bid mirror. Belt-and-suspenders, the match loop could also treat the reciprocal overflow as "skip this maker" rather than aborting the whole swap, but the band is the clean fix and keeps garbage orders off the book entirely.Filing under the #381 security-hardening umbrella.
mentioned in commit
e5cb6f3f1cmentioned in merge request !1010
mentioned in commit
86cf616168Verification complete — PASS
Verified on
main(clean working tree, 2026-07-07).Acceptance criteria
MIN_LIMIT_PRICE/MAX_LIMIT_PRICEgate placement, ladder expansion, andUpdateLimitOrderPricevalidate_limit_order_priceindex-common::limit_placement(1e-9 … 1e9); wired in pairlimit_placement.rs,contract.rs,orderbook.rsDecimal::raw(1)(1e-18) rejected at placement (bid + ask)cargo test -p cl8y-dex-tests place_limit_order_dust_price_rejectedcargo test -p cl8y-dex-tests dust_ask_brick_attack_prevented_valid_ask_still_fillscargo test -p cl8y-dex-pair match_asks_skips_legacy_dust_price_without_revertingexpand_limit_ladderrejects out-of-band rungscargo test -p dex-common expand_ladder_rejects_out_of_band+validate_limit_priceunit testsdocs/contracts-security-audit.md,docs/limit-orders.md#limit-price-band-gitlab-467skills/AGENTS_BOOK_MATCH_HINT_SECURITY.md(L20 / #467)make verify-issue-467— 7/7 steps, 0 failuresNotes
match_asks/match_bidsskip legacy out-of-band rows on reciprocalchecked_mul_flooroverflow instead of reverting the swap.Closing as verified on
main. No MR opened (no repo changes during verify).mentioned in issue #529
mentioned in commit
91d90ddba4mentioned in merge request !1060
mentioned in issue #532