Blacklisted maker's resting limit orders keep filling — maker side of a fill is never blacklist-checked (freeze bypass) #468
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#468
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 QA security sweep on the pair contract.
BlacklistWalletis supposed to freeze a wallet completely, but it doesn't touch a wallet's resting limit orders. If a blacklisted maker M already has orders sitting on the book, any taker that crosses those orders still fills them, delivers M's escrowed leg to the taker, and pays M's offer token straight into M's own balance. M keeps trading and keeps receiving freely-transferable CW20 while frozen.Where
The blacklist gate on the swap/receive path only looks at the taker side. In
execute_receive,contract.rs:735-747:walletsis only[token_sender, optional trader]— i.e. the taker (and the address they're trading on behalf of).gate_trading_blacklist(contract.rs:239) just forwards those to the blacklist guard. Nothing in that set is the resting maker.The maker side is built entirely from
order.ownerwith zero blacklist reference:orderbook.rs:1470(bid book) andorderbook.rs:1638(ask book):let entry = maker_payouts.entry(order.owner.clone()).or_default();maker_payout_transfer_messages(orderbook.rs:143-162) turns that map into one CW20Transfer { recipient: owner, amount }per distinct owner — no filtering.contract.rs:1213.So the fill-time code path never consults the blacklist for the order owner at all.
Why it happens
BlacklistWallet(M)blocks every path where M is the acting address — cancel, claim, place, and swap-as-taker are all gated (see thegate_trading_blacklistcalls atcontract.rs:604/641/651/661/671/686/742). But a fill against M's resting order is driven by the taker's transaction, not M's. M isn't the sender, isn't thetrader, and the matching engine only cares aboutorder.ownerfor payout bookkeeping. The guard is checked at entry against the taker; the maker is discovered later during the book walk and never re-checked.How to hit it
BlacklistWallet(M).recipient: M(contract.rs:1213via the map fromorderbook.rs:1470/1638).Net result: M completes a trade and receives spendable CW20 into its balance while blacklisted. Since T can be M-controlled, M can drain its own frozen escrow out through matched self-fills and land the proceeds in a fresh wallet. The freeze is effectively cosmetic against a maker who already has book depth.
Impact
Defeats the containment
BlacklistWalletis meant to provide — a frozen wallet with pre-existing resting orders can still transact and pull value out. This is the maker-side gap in blacklist enforcement, so it belongs with #456, under the #381 hardening umbrella.Fix direction
Enforce the blacklist at fill time on the maker owner, not just at entry on the taker. Options, roughly in order of cleanliness:
orderbook.rsbid path ~:1470, ask path ~:1638), checkorder.owneragainst the blacklist and skip that order — ideally auto-cancel/refund it so frozen makers' liquidity drops off the book rather than lingering. Refund would need to respect that the owner is frozen (park it as claimable-post-unfreeze, or route per policy) since paying a blacklisted owner is the thing we're preventing.maker_payoutsbeforemaker_payout_transfer_messages(orderbook.rs:143) so blacklisted owners don't receive the offer-token transfer — but that leaves their escrow half-consumed and their order state weird, so skipping/cancelling at match time is the sounder route.Whichever way, the maker owner set needs to reach the same blacklist check the taker already goes through. The guard already exists (
blacklist_guard::assert_trade_not_blacklisted_deps); it just isn't wired into the maker side of a match.mentioned in commit
48bee6ae44mentioned in merge request !1009
mentioned in commit
529b34e1a7mentioned in commit
23a36d0f4fmentioned in merge request !1010
Verification — #468 (blacklisted maker resting limit fill bypass)
Result: PASS — fix is on
main; no repo changes required.Acceptance criteria (issue body + docs)
cargo test -p cl8y-dex-tests blacklisted_maker_resting_limit_not_filled_taker_can_still_swap— maker offer-token (token A) balance unchanged after taker crosses bidassert_eq!(maker_token_a_after, maker_token_a_before)ExpiredLimitRefund { order_id: 1 }returns parked row withowner == makercargo test -p cl8y-dex-tests wallet_blacklist_blocks_swap_lp_limits_and_unban_restores(limit cancel + claim rejected; unblacklist restores)order.ownervia factoryBlacklistCheck(L19)orderbook.rsskip_blacklisted_maker_order/maker_owner_is_trade_blacklisted;blacklist_guard.rsTradeBlacklistGate; wired fromcontract.rshybrid execute + simulatesimulate_match_*skips blacklisted makers read-only (no park)orderbook.rssimulate paths usemaker_owner_is_trade_blacklistedwithpark_off_book=falsedocs/contracts-security-audit.mdL19 + B1;docs/limit-orders.md§ Blacklisted maker;docs/security-model.md;docs/user-incident-faq.md;skills/AGENTS_BLACKLIST_DECISION.mdmake check-blacklist-decision-docscargo test -p cl8y-dex-tests blacklist_tests -- --test-threads=1(10/10)Notes
match_asks/simulate_match_asks(sameskip_blacklisted_maker_orderhelper); only bid path has a dedicated integration test today.Follow-up (optional)
make verify-issue-468script (grep doc anchors + run regression test) for parity withverify-issue-467and other security issues.mentioned in issue #504
mentioned in issue #710