fix(deposit-address): include logIndex in dedup keys so sibling transfers in one tx all sweep - #3748
fix(deposit-address): include logIndex in dedup keys so sibling transfers in one tx all sweep#3748amateima wants to merge 4 commits into
Conversation
…fers in one tx all sweep One transaction can carry multiple transfers to the same deposit address, but both dedup guards collapsed them into one: getDepositKey was depositAddress:txHash and the Redis-persisted executed set keyed on the bare txHash, so only the first transfer was swept and the rest were silently skipped until the indexer window closed. getDepositKey is now depositAddress:txHash:logIndex, which fixes every set keyed on it (in-flight deposit/withdraw locks, executed/skipped/ refund-only withdraw sets) on both v1 and v3 paths. The executed-deposits set now stores depositKeys instead of bare txHashes, letting the prune loop reuse the depositKey set built from indexer messages. Old-format Redis entries self-prune on the first poll; rows still inside the indexer window fall through to the existing on-chain balance checks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8c422c2fd6
ℹ️ 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".
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e52d0c0046
ℹ️ 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".
…e depositKey Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@codex review |
|
Codex Review: Didn't find any major issues. Nice work! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
Summary
When a single transaction delivers multiple token transfers to the same deposit address, the bot swept only the first one. The rest were silently skipped — every retry hit the "already executed" guard — and once the indexer's 15-minute serving window closed, they became invisible to the bot permanently, requiring a manual sweep. This happened in production: one Ethereum tx carried two 10 USDC transfers to the same address, and the second breached the fill-latency SLA before being swept by hand.
The root cause is that the bot's duplicate-detection keys identified a transfer by transaction hash (plus deposit address), which cannot tell two transfers in the same transaction apart. This PR adds the transfer's
logIndex— its position within the transaction — to those keys, so every transfer gets its own identity. Sibling transfers in one tx now each get swept (or refunded), on both the v1 and v3 paths, for deposits and withdraws alike.Technical details
Two dedup keys were logIndex-blind, and the two transfers collided on both:
getDepositKey()(src/utils/DepositAddressUtils.ts) wasdepositAddress:transactionHash. It guards the in-flight locks (observedExecutedDeposits,observedExecutedWithdraws) and the Redis-persistedexecutedWithdrawKeys/terminallySkippedWithdrawKeys/refundOnlyDepositKeys. SinceforEachAsyncprocesses a poll's messages concurrently, the losing sibling hit the winner's freshly-added in-flight key and returned.executedDepositTxHashes(DepositAddressHandler) keyed on the baretransactionHash, persisted to Redis. After the winner executed, every redelivery of the loser matched this guard until the indexer window closed and the row stopped being served.Changes:
getDepositKey()is nowdepositAddress:transactionHash:logIndex. This single change fixes all five sets keyed on it.Erc20Transfer.logIndexis a required field on every message version (native transfers get a synthetic one), so no data-availability concerns.executedDepositTxHashes→executedDepositKeys: stores depositKeys instead of bare txHashes. The prune loop inevaluateDepositAddressesnow reuses thedepositKeysFromIndexerset, and the redundant bare-txHash set is deleted.deposit-address:executed:*) is unchanged: old-format entries cannot false-positive against the composite format and are pruned on the first poll.deposit-address-service/message.ts(transferId()docstring described the old collision; that service already keys onchainId:txHash:logIndexand needs no change).getDepositKeyinstead of literals; added the incident-shape assertion that two messages differing only inlogIndexproduce distinct keys.Accepted trade-offs:
Testing
test/DepositAddressUtils.ts: key-format assertions updated; new test that two transfers in one tx (differing only inlogIndex) produce different keys.tscandeslintclean.🤖 Generated with Claude Code