fix(trading)!: eth-flow order-id collision avoidance was completely inert - #981
fix(trading)!: eth-flow order-id collision avoidance was completely inert#981gomesalexandre wants to merge 1 commit into
Conversation
…nert
calculateUniqueOrderId computed a collision-free orderId by nudging
buyAmount down 1 wei on collision, but only ever returned the bare
orderId string. getEthFlowTransaction then built the actual on-chain
transaction from its own original (un-nudged) order, so the tx
recreated the exact order that was just detected as colliding -
createOrder reverts with OrderIsAlreadyOwned on-chain, and the whole
collision-avoidance feature did nothing except burn the caller's gas
on a resubmit.
calculateUniqueOrderId now returns { orderId, order } - the order
that actually produced that id - and getEthFlowTransaction (and the
public OrderPostingResult it feeds via postSellNativeCurrencyOrder)
builds everything from that returned order instead of discarding it.
Also added a guard for buyAmount underflow: the existing nudge-by-1
logic had no floor, so a buyAmount of 1 (or a second collision on an
already-adjusted order) would silently produce buyAmount 0 or a
negative BigInt that can't encode as uint256. It now throws a clear
error instead of building an invalid order.
BREAKING CHANGE: calculateUniqueOrderId's return type changed from
Promise<string> to Promise<{ orderId: string; order: UnsignedOrder }>.
Callers reading the bare string need to destructure `.orderId`.
Out of scope, noted for a maintainer to triage separately (not fixed
here to keep this PR to the one bug):
- calculateUniqueOrderId hashes order.kind as supplied, but the
EthFlow contract always treats orders as SELL - a BUY-kind order's
computed id can diverge from the transaction's real id. Pre-existing,
unrelated to the collision-avoidance bug.
- getEthFlowTransaction's gas-estimation catch swallows errors
(including a TOCTOU OrderIsAlreadyOwned revert from a competing
order created between the collision check and submission) and
falls back to a default gas limit rather than surfacing them.
Pre-existing error-handling behavior, not part of this fix.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Team Run ID: 📒 Files selected for processing (4)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthrough
ChangesEthFlow collision handling
Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk: 🔵 Low · up to The fix now submits the same adjusted order used to derive the collision-free ID, while the public calculateUniqueOrderId API changes from returning a string to returning an object; external consumers that have not migrated could break or track the result incorrectly. The PR is mergeable with explicit owner awareness of that bounded migration risk. Sequence Diagram(s)sequenceDiagram
participant Caller
participant getEthFlowTransaction
participant calculateUniqueOrderId
participant EthFlowContract
Caller->>getEthFlowTransaction: Submit order
getEthFlowTransaction->>calculateUniqueOrderId: Generate unique order ID
calculateUniqueOrderId->>EthFlowContract: Check order ID collision
EthFlowContract-->>calculateUniqueOrderId: Collision result
calculateUniqueOrderId-->>getEthFlowTransaction: ID and adjusted order
getEthFlowTransaction->>EthFlowContract: Encode adjusted createOrder calldata
getEthFlowTransaction-->>Caller: Return transaction and orderToSign
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
tl;dr
calculateUniqueOrderId's entire job is to avoid eth-flow order-id collisions. It never actually did — the transaction it hands back always recreates the exact colliding order, which reverts on-chain withOrderIsAlreadyOwnedand burns the user's gas.The bug
calculateUniqueOrderId(packages/trading/src/calculateUniqueOrderId.ts) detects a collision, nudgesbuyAmountdown 1 wei, and recurses to compute a fresh, collision-freeorderId— but it only ever returned that bareorderIdstring, never the adjusted order that produced it:The caller,
getEthFlowTransaction.ts, builds the actual on-chain transaction from its own original, un-nudged order:So the returned
orderIddescribes an order that never actually gets built. The transaction that gets sent recreates the exact ordercheckEthFlowOrderExistsjust reported as colliding, andCoWSwapEthFlow.createOrderreverts withOrderIsAlreadyOwned. This also flows into the publicOrderPostingResultreturned bypostSellNativeCurrencyOrder— any caller pollingorderIdtracks an order that will never exist.The fix
calculateUniqueOrderIdnow returns{ orderId, order }— the order that actually produced that id — andgetEthFlowTransactionbuildsethOrderParams, the gas estimate, the txvalue, and its own returnedorderToSignfrom thatorder, not from its original input.BREAKING CHANGE:
calculateUniqueOrderId's return type changes fromPromise<string>toPromise<{ orderId: string; order: UnsignedOrder }>. It's exported public API (index.ts); any external caller reading the bare string needs to destructure.orderId. Given the old signature was silently unusable for its stated purpose, I think this is worth doing now rather than deprecating in place — happy to go a different route if maintainers prefer.Also added a guard while I was in there: the nudge-by-1 logic had no floor. A
buyAmountof1(or a second collision hitting an already-adjusted order) would silently producebuyAmount: 0or a negativeBigIntthat can't encode asuint256. It now throws a clear error instead of building an invalid order.Out of scope (not fixed here, flagging for a maintainer)
calculateUniqueOrderIdhashesorder.kindas supplied, but the EthFlow contract always treats orders asSELL. For aBUY-kind order, the computed id could diverge from the transaction's real id. This is pre-existing and unrelated to the collision bug — didn't want to conflate two different fixes in one PR.getEthFlowTransaction's gas-estimation.catch()swallows errors (including a TOCTOUOrderIsAlreadyOwnedrevert from a different order created between the collision check and submission) and falls back to a default gas limit rather than surfacing them. Also pre-existing, also a separate concern from what this PR fixes.Testing
Real repro before touching code — ported the control flow of both functions with a stubbed
checkEthFlowOrderExistsreporting a collision, confirmed the returnedorderIddidn't match what the returned transaction's calldata would actually create, and confirmed unfixed code returns the original collidingbuyAmount.New regression tests confirmed to fail on unfixed code and pass on fixed code (verified via
git stashback to the original source with the new tests still in place):All three fail on
main, all three (plus 2 new underflow-guard tests) pass after the fix.Full package suite:
261 passed, 2 skipped(pre-existing skips, unrelated to this change).pnpm run typecheckclean across the whole monorepo (confirms no other consumer of the changed return type broke).eslintclean on all four touched files.Ran Codex (GPT-5.6) as an adversarial reviewer against the diff. It found two real issues, both fixed in this PR: the
buyAmountunderflow case above, and correctly noting thekind/TOCTOU items are pre-existing and out of scope (both listed above rather than folded in).Summary by CodeRabbit