fix(TransactionClient): don't resubmit repriced (already-mined) replacements#3504
fix(TransactionClient): don't resubmit repriced (already-mined) replacements#3504droplet-rl wants to merge 2 commits into
Conversation
…cements The ensureConfirmation TRANSACTION_REPLACED handler resubmitted under a fresh nonce on *any* replacement. ethers reports a same-nonce speed-up of the *same* logical transaction as TRANSACTION_REPLACED with cancelled === false; in that case the transaction has already mined under a new hash, so resubmitting double-sends (e.g. duplicating an inventory rebalance transfer). Return the mined replacement instead, and only resubmit on genuine cancellations or replacements (cancelled === true), where the original did not take effect. Surfaced by the Codex review on #3503 (P1) and flagged there as a follow-up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 48f761feb8
ℹ️ 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".
| if (cancelled === false && isDefined(replacement)) { | ||
| this.logger.warn({ | ||
| ...common, | ||
| message: `Transaction on ${chain} was repriced and already mined; using replacement.`, | ||
| }); | ||
| return replacement; |
There was a problem hiding this comment.
Preserve failed replacement receipts as failures
When a repriced replacement is mined but reverts, ethers still reports TRANSACTION_REPLACED with cancelled === false and includes the replacement receipt; this branch returns the replacement as a successful TransactionResponse without checking that receipt status. In that scenario an ensureConfirmation caller records a failed on-chain operation as successful instead of following the existing CALL_EXCEPTION failure path, so reverted rebalances/finalizations can be skipped by the bot.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good catch — fixed in 1073374. ethers' waitForTransaction (used internally to fetch the replacement's receipt) doesn't throw on revert, so a repriced replacement that mined with status === 0 was being returned as a confirmed success.
The cancelled === false branch now inspects error.receipt.status: a reverted replacement (status === 0) is thrown, mirroring the CALL_EXCEPTION failure path above, so ensureConfirmation callers no longer record a reverted rebalance/finalization as successful. A successful replacement (status === 1, or an older receipt with no status) is still returned. Added a regression test ("Treats a reverted repriced replacement as a failure").
A repriced replacement (cancelled === false) can mine and revert. ethers attaches the replacement's mined receipt to the TRANSACTION_REPLACED error, and waitForTransaction does not throw on revert, so receipt.status === 0 was previously returned as a confirmed success. Check the receipt status and, when it reverted, throw (mirroring the CALL_EXCEPTION path) so callers don't record a reverted rebalance/finalization as confirmed. Adds a regression test; updates the existing repriced-success test to carry a status === 1 receipt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks @codex — addressed the P2 in 1073374. A repriced replacement ( @codex please re-review the updated diff. |
|
Addressed the Codex review on #3504 (commit 1073374). The single inline P2 ("Preserve failed replacement receipts as failures") was valid: a repriced replacement ( Fix: the
|
|
Codex Review: Didn't find any major issues. More of your lovely PRs please. 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". |
|
Thanks @codex — re-review on |
Summary
Testing / Checks
Note: |
|
Confirmed — no further action. The PR diff is limited to |
Problem
Follow-up to #3503, addressing the Codex P1 raised on that PR (which had already merged ~2 minutes before the review landed).
#3503 opted inventory rebalance transfers into
TransactionClient._submit's confirmation loop (ensureConfirmation: true). That loop'sTRANSACTION_REPLACEDhandler resubmits under a fresh nonce on any replacement:ethers (v5) classifies a same-nonce replacement via
error.cancelled:reason: "repriced"→cancelled === false— a gas speed-up of the same logical transaction. The transfer has already mined under a new hash.reason: "cancelled"/"replaced"→cancelled === true— the original did not take effect.In the
repricedcase the current code resubmits a transfer that already landed → double-send (e.g. a duplicated inventory rebalance). This affects everyensureConfirmationcaller (rebalancer, refiller, finalizers, dataworker), not just the inventory path.Fix
In the
TRANSACTION_REPLACEDbranch, return the minedreplacementwhencancelled === false; only resubmit on genuine cancellations/replacements (cancelled === true). Returningerror.replacementalso hands the caller the actual mined tx (correct hash) rather than the original, now-replaced one.A repriced replacement can still mine and revert. ethers attaches the replacement's mined receipt to the error, and
waitForTransactiondoes not throw on revert, soreceipt.status === 0would otherwise be reported as a confirmed success. Thecancelled === falsebranch now checkserror.receipt.statusand, on a reverted replacement, throws (mirroring the existingCALL_EXCEPTIONfailure path) so callers don't record a reverted rebalance/finalization as confirmed. (Codex P2.)Testing
tsc --noEmit, eslint, prettier: cleantest/TransactionClient.ts— 12 passing, including:wait()called once, mined replacement returned, no resubmit), andstatus === 0→ surfaced as a failure, no resubmit).Docs
No README/AGENTS updates needed — this behavior is documented only in the in-code comment, which is updated here.
🤖 Generated with Claude Code