fix(rebalancer): tie Binance swap-deposit tag lifecycle to order state#3560
Conversation
The SWAP tag set by _depositToBinance expired on a 30-minute wall clock while the Binance finalizer's deposit-history lookback is far longer, so every swap deposit — including successfully swapped ones — was later re-counted into the finalizer's amountToFinalize ledger as if it were a bridge deposit awaiting withdrawal. This phantom debt was then paid out of the next order's just-credited deposit, starving that order until its REBALANCER_PENDING_ORDER_TTL pruned it (7 occurrences 2026-07-01..08 on USDT->USDC HyperEVM routes; e.g. cloid 0xe42d273cb1ce29c53aad23e9241606cd). Make the tag lifecycle follow order state instead of wall clock: - Deposit: tag SWAP with the Redis default TTL (1 week, matching the withdrawal-side tag) so it outlives the finalizer lookback, and store a cloid -> deposit txn mapping alongside the order state. - Order abandoned in PENDING_DEPOSIT (TTL prune): a new BaseAdapter._onExpiredOrderPruned hook lets the Binance adapter delete the tag, handing the unconsumed deposit to the finalizer for reclaim. - Order progressed past PENDING_DEPOSIT: the deposit was consumed by the spot order, so the tag stays and the ledger remains accurate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per review feedback, let hook errors propagate instead of swallowing them with a warn log. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Addressed the review in bdf7ede: removed the try-catch (and its comment) around the |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Applied the suggestion in 3236701: the "Released SWAP deposit tag" log in |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 93c1416cef
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The SWAP tag previously relied on the Redis default TTL (1 week), which is shorter than the Binance finalizer's default 14-day deposit-history lookback — a consumed swap deposit aged 7-14 days would lose its tag and be re-counted as finalizable. Tag both the deposit type key and the cloid -> deposit txn mapping with 2x FINALIZER_TOKENBRIDGE_LOOKBACK. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Addressed Codex's P1 in 4843d2a. Verified the finding was real: the SWAP tag relied on the Redis default TTL (1 week, Fix: @codex review |
c06f4d7 to
4843d2a
Compare
|
Held off on applying the suggestion and replied inline with why: removing the explicit TTL from If the intent is that prod finalizer lookback is always under a week and the default is fine, say the word and I'll apply it plus trim the comment, mapping TTL, and README to match in one commit. |
Problem
_depositToBinancetags each swap depositSWAPin Redis so the Binance finalizer excludes it from itsamountToFinalizeledger — but the tag expired on a 30-minute wall clock, while the finalizer's deposit-history lookback is far longer. Once the tag lapsed,getBinanceDepositTypereturnedUNKNOWNand the deposit was re-counted as a bridge deposit awaiting withdrawal — even when its swap had completed and the funds were long gone. This phantom debt was then paid out of the next order's just-credited deposit, starving that order untilREBALANCER_PENDING_ORDER_TTLpruned it.Observed 7 times over 2026-07-01 → 07-08 on USDT→USDC (HyperEVM) routes. Traced example (cloid
0xe42d273cb1ce29c53aad23e9241606cd, 2026-07-08): its 82,385.72 USDT deposit (tx) was credited at ~04:58 and withdrawn back to BSC by the finalizer at 04:59:27 against a 158k phantom backlog created by the previous, successfully completed order (0x32e9b903, swap filled 04:16, finalized 04:21, tag expired ~04:31). The starved order was pruned at 05:56:12 — the PagerDuty alert that kicked this off.Change
Tag lifecycle now follows order state instead of wall clock (the withdrawal-side SWAP tag already worked this way — set with the Redis default 1-week TTL):
setBinanceDepositType(..., SWAP)without explicit TTL → Redis default (1 week), which outlives the finalizer lookback. Acloid → {chainId, transactionHash}mapping is stored alongside the order state so later transitions can find the tag.PENDING_DEPOSIT(TTL prune) — newBaseAdapter._onExpiredOrderPrunedhook (no-op by default, wrapped so it can't break the shared read path); the Binance adapter deletes the deposit's tag, handing the unconsumed funds to the finalizer for reclaim on its next run.PENDING_DEPOSIT— the deposit was consumed by the spot order; the tag stays, so the finalizer's ledger never re-counts consumed deposits.Behavior change: stuck-funds recovery starts when the order dies (
REBALANCER_PENDING_ORDER_TTL, default 1h) rather than 30 minutes after deposit. This also removes the race where the finalizer could reclaim a live order's deposit that was legitimately waiting out Binance's deposit-unlock confirmations (RW00441).Not touched:
scripts/swapOnBinance.ts(interactive operator flow manages its own tag TTL); orders created before this change have no txn mapping — the prune hook logs and skips, falling back to today's behavior.Replaces #3559, which protected the balance from the phantom debt but didn't stop the phantom debt from forming.
Testing
yarn tsc --noEmitcleanhardhat test test/BinanceAdapter.helpers.ts test/BinanceAdapter.withdrawals.ts test/BinanceUtils.ts test/BinanceFinalizer.ts— 48 passing🤖 Generated with Claude Code