fix(finalizer): protect pending swap orders' source balance from Binance finalizer withdrawals#3559
Closed
droplet-rl wants to merge 1 commit into
Closed
fix(finalizer): protect pending swap orders' source balance from Binance finalizer withdrawals#3559droplet-rl wants to merge 1 commit into
droplet-rl wants to merge 1 commit into
Conversation
…nce finalizer withdrawals The Binance finalizer deducts pending orders' destination-token amounts (getPendingRebalances) from the shared exchange account's withdrawable balance, but nothing protects the source-token side. When a swap order's deposit credits while the finalizer has withdrawal backlog in the same coin, the finalizer withdraws the just-credited funds, the order can never place its spot order, and it starves until REBALANCER_PENDING_ORDER_TTL prunes it. Observed 7 times over 2026-07-01..08 on USDT->USDC (HyperEVM) orders, e.g. cloid 0xe42d273cb1ce29c53aad23e9241606cd, whose 82,385.72 USDT deposit was withdrawn back to BSC within 3 minutes of crediting. Add RebalancerAdapter.getPendingDepositSourceAmounts(), implemented generically in BaseAdapter from the PENDING_DEPOSIT status set, and deduct these source-token amounts in the Binance finalizer. The source- and destination-side maps are netted to positive amounts independently before being summed, so negative on-chain adjustments (intermediate bridge legs) cannot cancel the source-side protection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
Author
|
Closing in favor of #3560 per discussion in Slack: rather than deducting pending orders' source-token amounts from the finalizer's withdrawable balance (treating the symptom), #3560 stops the phantom |
nicholaspai
added a commit
that referenced
this pull request
Jul 8, 2026
#3560) ## Problem `_depositToBinance` tags each swap deposit `SWAP` in Redis so the Binance finalizer excludes it from its `amountToFinalize` ledger — but the tag expired on a **30-minute wall clock**, while the finalizer's deposit-history lookback is far longer. Once the tag lapsed, `getBinanceDepositType` returned `UNKNOWN` and 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 until `REBALANCER_PENDING_ORDER_TTL` pruned 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](https://bscscan.com/tx/0x2a845bbb98e1347aa594e8fdde705d74d42d4ae22ed9884fdfe1d014603a60ff)) 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): - **Deposit** — `setBinanceDepositType(..., SWAP)` without explicit TTL → Redis default (1 week), which outlives the finalizer lookback. A `cloid → {chainId, transactionHash}` mapping is stored alongside the order state so later transitions can find the tag. - **Order abandoned in `PENDING_DEPOSIT`** (TTL prune) — new `BaseAdapter._onExpiredOrderPruned` hook (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. - **Order progressed past `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 --noEmit` clean - `hardhat test test/BinanceAdapter.helpers.ts test/BinanceAdapter.withdrawals.ts test/BinanceUtils.ts test/BinanceFinalizer.ts` — 48 passing - eslint + prettier clean on touched files 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: droplet-rl <284132418+droplet-rl@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: nicholaspai <npai.nyc@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The Binance finalizer deducts pending rebalance orders' destination-token amounts (
getPendingRebalances) from the shared exchange account's withdrawable balance, but nothing protects the source-token side of an order inPENDING_DEPOSIT. When a swap order's deposit is credited while the finalizer has withdrawal backlog in the same coin, the finalizer withdraws the just-credited funds to service that backlog. The order then fails its balance check every cycle ("Available balance for input token ... is less than amount to transfer"), never places its spot order, and starves untilREBALANCER_PENDING_ORDER_TTL(default 1h) elapses andBaseAdapter._redisCleanupPendingOrdersprunes it.Observed 7 times over 2026-07-01 → 2026-07-08 on USDT→USDC (HyperEVM) swap-rebalancer orders. Fully traced example (2026-07-08, cloid
0xe42d273cb1ce29c53aad23e9241606cd):pending-depositamountToFinalizebacklog (pendingRebalanceDeductionwas 0 for USDT — only the destination-side USDC was protected)Change
RebalancerAdapter.getPendingDepositSourceAmounts(account)— source-token amounts of orders inPENDING_DEPOSIT, keyed by source chain/token — implemented generically inBaseAdapterfrom the existing Redis status set (all adapters extendBaseAdapter).getPendingBinanceRebalanceDeductionsnow also deducts these source-token amounts. The source- and destination-side maps are netted to positive per-coin amounts independently before being summed, so negative on-chain adjustments ingetPendingRebalances(e.g. intermediate bridge legs) cannot cancel the source-side protection.src/rebalancer/README.md.The deduction is deliberately conservative: for same-coin (no-swap) routes the order is briefly protected on both source and destination sides while in
PENDING_DEPOSIT; over-deduction only delays finalizer sweeps, which self-resolves when the order progresses or is pruned.Not in scope (follow-up)
The prune events also revealed a second issue: the 30-minute
SWAPdeposit tag expires while the finalizer's deposit-history lookback is much longer, so every swap deposit — including successfully swapped ones — is later re-counted into the finalizer'samountToFinalizeledger (this is where the 158k/248k backlog that consumed the traced order's deposit came from). That accounting fix is intentionally left to a separate PR.Testing
yarn tsc --noEmitcleanhardhat test test/BinanceFinalizer.ts— 9 passing (2 new)hardhat test test/RebalancerClient.cumulativeRebalancing.ts— 19 passing🤖 Generated with Claude Code