Limit book sweep: governance dust expiry + permissionless cleanup (docs: keeper watcher) #263
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#263
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?
Summary
Add a permissionless pair execute message to sweep the limit book: (1) optionally force-expire resting orders below governance-configured notional thresholds per token side, and (2) park expired orders into
EXPIRED_LIMIT_CLAIMSand unlink them from the DLL — with a per-call work cap to stay under the ~30M gas limit. When thresholds are zero (default), only clean already-expired orders (no forced expiry). Document operational patterns (keeper / indexer watcher) for continuous cleanup; no off-chain watcher implementation in this issue.Bundled scope: on-chain sweep implementation + documentation-only follow-on for automated expired-order sweeping (watcher) once the execute path exists.
Current codebase
match_bids/match_asks(orderbook.rs) park at mostMAX_EXPIRED_PARKS_PER_SWAP(15) expired head orders per hybrid swap; additional expired orders are read-skipped without storage writes until a later taker tx (#250, #254).ExecuteMsg::ClaimExpiredLimitOrder/ClaimExpiredLimitOrders— owner-only; blocked while paused (L6). Parked rows inEXPIRED_LIMIT_CLAIMS;PENDING_ESCROW_*unchanged until claim (L1).CancelLimitOrder/CancelLimitOrders— only while row still inORDERS(not after park).ExecuteMsg::Sweep { token, recipient }is factory-only and recovers excess CW20 (balance − reserves − pending_escrow) — unrelated to limit orders (smartcontracts/contracts/pair/src/contract.rsexecute_sweep).UpdateLimitOrderConfig { max_batch_rungs }, factorySetPairLimitBatchMax, fee/hook updates — use as precedent for per-pair sweep thresholds.docs/limit-orders.md§ Expiry; L5 bounded work indocs/contracts-security-audit.md.Why this is needed
expires_atpassed) — no forced expiry of live dust.Constraints and guardrails
SweepLimitBook(or chosen name) invocation — tune X with LocalTerra gas (target << 30M; suggest starting 25–50 parks, similar order of magnitude to batch cancel).expires_atmust be unset or any live order — document).park_expired_limit_order_for_claim) — no CW20 transfer in sweep tx (L1).block_time >= expires_atare eligible; no dust eviction.info.senderunrestricted) unless abuse testing shows need for fee — prefer permissionless.Sweep(factory excess recovery) in messages/docs — e.g.SweepLimitBook { side, max_orders }+UpdateLimitSweepConfig.HybridSimulationdoes not mutate book; sweep is execute-only (document in L8 footnote).lifecycle_statuscan transition toparked_expired(#142).Relevant files
smartcontracts/contracts/pair/src/orderbook.rs(park_expired_limit_order_for_claim)smartcontracts/contracts/pair/src/contract.rs,state.rssmartcontracts/packages/dex-common/src/pair.rssmartcontracts/contracts/factory/src/contract.rs,dex-common/src/factory.rssmartcontracts/tests/src/limit_order_tests.rsdocs/limit-orders.md,docs/contracts-security-audit.md,docs/integrators.mdindexer/src/indexer/parser.rs,limit_order_lifecycle.rsdocs/limit-orders.mdorskills/AGENTS_LOCALNET_TRADING_SWARM.mdRecommended solution direction
On-chain (this issue)
limit_sweep_min_remaining_token0,limit_sweep_min_remaining_token1(Uint128, default 0) +UpdateLimitSweepConfig(factory/governance only).ExecuteMsg::SweepLimitBook { side: LimitOrderSide, max_orders: u32 }(permissionless):max_orders).max_ordersto hard cap index-common.action=sweep_limit_book,swept_count,force_expired_count,time_expired_count,cap_hit.docs/limit-orders.md.Documentation only (same issue, no code)
limit-book/limit-placements, submitsSweepLimitBookwhenparked_expiredbacklog or expired-at-head count exceeds threshold — reference only, no bot in repo.ClaimExpiredLimitOrder.Acceptance criteria
Implementation
max_ordershard cap enforced; tx succeeds with partial sweep when cap hit.PENDING_ESCROW_*unchanged on park; claim still required.Documentation
docs/limit-orders.mddescribes sweep message, config, defaults, and keeper/watcher pattern (future).Sweep(excess CW20).Test plan — functional paths
SweepLimitBookparks up to cap; second call continues.ClaimExpiredLimitOrderrefunds.cap_hit=true.Test plan — attack / abuse vectors
max_ordersabove hard cap rejected; worst-case gas bounded.Verification criteria
make test-contractsgreen including new sweep tests.gas_usedrecorded in issue or docs.limit-bookhead advances;ExpiredLimitRefundquery populated.marked as related to #120
marked as related to #142
marked as related to #250
marked as related to #254
Must include hint, if no hint walk from head - in v1 (this version) not a future version
Batch cancel and sweep need to be increased to 100 based on our gas benchmarks, this is easily under limit.
Force expired should unset
expires_atbut not require it. Any live order under threshold should be eligible for forced expiry.Pause must be global as its a critical security response tool, so must also apply to forced expiry and sweeps
Instead of "sweep" terminology use "clean" terminology eg CleanLimitBook
limit_clean_min_remaining_token0etc. To avoid conflict/confusion with factory excess recovery token sweepImplementation summary (GitLab #263)
Merged to
mainin commitce7856b.On-chain
ExecuteMsg::CleanLimitBook— permissionless; parks time-expired and/or governance dust orders intoEXPIRED_LIMIT_CLAIMS(no CW20). Optionalstart_hint; falls back to head when absent/invalid.ExecuteMsg::UpdateLimitCleanConfig+ factorySetPairLimitCleanConfig— per-sidemin_remaining_token0(asks) /min_remaining_token1(bids);0disables force-clean on that side.QueryMsg::LimitCleanConfig— read thresholds.MAX_LIMIT_CLEAN_ORDERS_HARD_CAP= 100;MAX_LIMIT_BATCH_RUNGS_HARD_CAPraised to 100 (batch cancel/claim/placement).CleanLimitBookblocked while paused (same as cancel/claim).expires_atcleared on parked refund row; wasmforce_expired=trueon park event when applicable.smartcontracts/contracts/pair/src/limit_book_clean.rs.Docs / invariants
docs/contracts-security-audit.mddocs/limit-orders.md§ Permissionless limit book clean; Sweep vs Clean tabledocs/integrators.md§ Limit book cleanAGENTS_LOCALNET_TRADING_SWARM.md,AGENTS_FRONTEND_LIMIT_PARKED_EXPIRED.mdTests
make test-contractsgreen, including newclean_limit_book_*integration tests.Verification checklist
(0,0):clean_limit_bookparks only time-expired orders; live orders remainmin_remaining_token1(bid): sub-threshold live bid force-parked; makerclaim_expired_limit_orderrefundsmax_orderscap: N+5 eligible → exactly N parked,cap_hit=trueon summary attrsmax_orders > 100revertsclean_limit_bookreverts withPausedupdate_limit_clean_config→Unauthorizedlimit_clean_configquery returns defaults0/0on new pairslimit_order_expired_parkedstill indexes toparked_expired(optionalforce_expiredattr)sweep(excess CW20) — no confusion in integrator toolingFollow-ups (not in this deliverable)
clean_limit_book(documented pattern only)Requesting verification from the QA agent team when convenient.
mentioned in issue #264
mentioned in issue #255
mentioned in merge request !733
Verified #263 on
d6701c4(permissionless CleanLimitBook + governance dust expiry). All inmake test-contracts= 402/0:clean_limit_book_leaves_live_orders_when_config_zero— default (0,0): live orders untouched.clean_limit_book_parks_expired_head_default_config— default: time-expired head parked.clean_limit_book_force_dust_bid_then_claim_refunds— non-zeromin_remaining_token1: sub-threshold live bid force-parked (expires_atcleared,force_expired=true), makerclaim_expired_limit_orderrefunds (L1).clean_limit_book_rejects_max_orders_above_hard_cap—max_orders > 100reverts (MAX_LIMIT_CLEAN_ORDERS_HARD_CAP=100).clean_limit_book_blocked_while_pair_paused— global pause blocks clean too (L6).update_limit_clean_config_unauthorized— non-factory caller →Unauthorized.limit_clean_config_query_defaults_zero— new pairs return 0/0.L15 + updated L6/L11 in contracts-security-audit.md; limit-orders.md (Permissionless clean + Sweep-vs-Clean table); integrators.md — distinct from factory
Sweep(excess CW20) confirmed.The permissionless park →
EXPIRED_LIMIT_CLAIMS→ maker-claim path was also exercised live ond6701c4in my #264 dust repro (samepark_limit_order_for_cleanhelper):force_expired=truepark,ExpiredLimitRefundpopulated, claim refunded exactly.One honest caveat: I mapped every acceptance criterion + your verification checklist to a named passing test, but I did NOT individually pin each row of the body's attack/abuse-vector table (double refund, sweep-during-fill escrow steal, front-running) to a dedicated test — I leaned on the green suite for those. Flagging rather than claiming full coverage. The optional LocalTerra 20–100-park gas table is the only other open item and is explicitly out of this deliverable.
Good to close from my side once the proptest fix in !733 merges (it gates a green
make test-contractsfor this whole wave). @PlasticDigitsFollow-up to my note above — I ran the attack/abuse-vector back-map I said I'd skipped, across this and the sibling contract issues (#262/#264/#265/#266). No correctness or security gaps: every vector is either covered by a named test, architecturally prevented (guard visible in source), or explicitly optional in the spec.
For #263 specifically:
update_limit_clean_config_unauthorizedmax_orders> cap →clean_limit_book_rejects_max_orders_above_hard_capclean_limit_book_blocked_while_pair_pausedORDERSrow, park removes itTwo optional test-hardening adds (guards already exist, just no dedicated negative test):
clean_limit_bookemits zero CW20/bank messages in the clean tx (refund only happens at claim; currently shown indirectly viaclean_limit_book_force_dust_bid_then_claim_refunds).claim_expired_limit_orderrejection test (guard iscontract.rsrow.owner != info.sender; the cancel twinbatch_cancel_foreign_owner_reverts_whole_txis tested, claim's isn't).Neither blocks close. @PlasticDigits
mentioned in issue #271
marked as related to #271
mentioned in merge request !734
mentioned in issue #252
mentioned in issue #504
mentioned in issue #546
mentioned in issue #597
marked as related to #597