Skip to content

fix(swap): optimize quote on aTokens for non-flashloan CoW paths#2978

Merged
mgrabina merged 4 commits into
mainfrom
fix/cow-quote-token-alignment
May 13, 2026
Merged

fix(swap): optimize quote on aTokens for non-flashloan CoW paths#2978
mgrabina merged 4 commits into
mainfrom
fix/cow-quote-token-alignment

Conversation

@mgrabina
Copy link
Copy Markdown
Contributor

@mgrabina mgrabina commented May 12, 2026

Summary

For CollateralSwap (no flashloan) and WithdrawAndSwap, the CoW order is posted via SwapActionsViaCoW with state.sourceToken.addressToSwap / state.destinationToken.addressToSwap — the aTokens. But the quote in useSwapQuote.ts was always fetched on underlyingAddress for any CoW path. That mismatch erodes the slippage cushion because CoW's volume-fee policy and gas estimate are per-pair: the quote's tier and rate apply to the underlying pair while the order settles on the aToken pair.

On a recent aEthRLUSD -> aEthUSDS collateral swap, this contributed ~1.7 bps (0.3 bps protocol-fee tier for the underlying RLUSD/USDS pair vs 2 bps tier for the aToken pair), which flipped a 2 bps limit order to ~0.11 bps above market — solver-unfillable, expired.

What changed

Behavioral change is CoW-only. The Paraswap branch of the conditional is preserved bit-for-bit; the new code only adds CoW to the path that already used addressToSwap for non-flashloan Paraswap.

  • Renamed the existing conditional in getTokenSelectionForQuote to a single usesAddressToSwap flag.
  • For CoW with state.useFlashloan === false, quote tokens are now addressToSwap (matching what the order posts).
  • Flashloan CoW paths unchanged: quote stays on underlying because the flashloan adapter unwraps aTokens in pre/post hooks.
  • All Paraswap paths unchanged.

Behavioral changes (truth table)

provider useFlashloan swapType before after
CoW false CollateralSwap underlying ❌ aToken ✅
CoW false WithdrawAndSwap underlying ❌ aToken (src) / token (dest) ✅
CoW false Swap underlying (= aToken for plain ERC20) aToken (= same value, no-op)
CoW true any underlying ✅ underlying ✅ (unchanged)
Paraswap any any per existing logic per existing logic (unchanged)

DebtSwap and RepayWithCollateral on CoW always have useFlashloan = true via forceFlashloanFlow, so they're unaffected.

Out of scope (follow-ups)

  • The × 3 networkFee hack in useSwapOrderAmounts.ts is a workaround for getAppDataForQuote returning undefined. The disabled implementation block right below it would send flashloan/hook hints so solvers' quote includes the right gas; once that's wired the hack can go away.

Test plan

  • CoW CollateralSwap (no flashloan) on an aToken pair where the underlying and aToken pairs land in different CoW volume-fee tiers — confirm the order's buyAmount cushion matches the user-selected slippage when checked against a fresh CoW quote on the aToken pair.
  • CoW WithdrawAndSwap from an aToken supply to a regular ERC20 — confirm quote tokens match what the order posts.
  • CoW CollateralSwap with flashloan (HF below threshold) — confirm quote still goes on underlying, order still posts on underlying via the flashloan adapter, no regression.
  • CoW DebtSwap and RepayWithCollateral — confirm quote tokens unchanged (both still on underlying via forceFlashloanFlow).
  • Paraswap paths across all swap types — confirm no quote-token regression.

For CollateralSwap (no flashloan) and WithdrawAndSwap, the order is
posted via SwapActionsViaCoW with state.sourceToken.addressToSwap
(the aToken), but the quote was always fetched on the underlying.

CoW's volume-fee policy and gas estimate are per-pair, so quoting on
the underlying produced a different protocol-fee tier and rate than
the pair the order actually settles on. The cushion baked into
buyAmount by useSwapOrderAmounts ended up short by that difference;
on aEthRLUSD -> aEthUSDS that was ~1.7 bps (0.3 bps tier for the
underlying vs 2 bps tier for the aToken pair), enough to flip a 2 bps
limit order to ~0.11 bps above market and leave it unfillable.

Use addressToSwap for any non-flashloan CoW path so the quote and
the order reference the same pair. Flashloan paths still quote on
underlying because the adapter unwraps aTokens in pre/post hooks.
Paraswap carve-outs preserved as-is.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 12, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
interface Ready Ready Preview, Comment May 12, 2026 6:07pm

Request Review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6b78f35115

ℹ️ 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".

Comment thread src/components/transactions/Swap/hooks/useSwapQuote.ts Outdated
@github-actions
Copy link
Copy Markdown

@github-actions
Copy link
Copy Markdown

📦 Next.js Bundle Analysis for aave-ui

This analysis was generated by the Next.js Bundle Analysis action. 🤖

This PR introduced no changes to the JavaScript bundle! 🙌

@mgrabina mgrabina force-pushed the fix/cow-quote-token-alignment branch from 828e5d4 to 6b78f35 Compare May 12, 2026 16:12
state.provider lags the freshly-computed provider during transitions
(it's only synced via a later useEffect), so when CoW <-> Paraswap
switches, this branch can misclassify the active quote's provider.
For non-flashloan WithdrawAndSwap / RepayWithCollateral that would
bypass the Paraswap underlying-token carve-out and send aToken
addresses to Paraswap.

Pass the resolved provider into getTokenSelectionForQuote and key
the useMemo on it instead of state.provider.
@mgrabina
Copy link
Copy Markdown
Contributor Author

@codex

@github-actions
Copy link
Copy Markdown

@github-actions
Copy link
Copy Markdown

📦 Next.js Bundle Analysis for aave-ui

This analysis was generated by the Next.js Bundle Analysis action. 🤖

This PR introduced no changes to the JavaScript bundle! 🙌

@mgrabina mgrabina changed the title fix(swap): quote on aTokens for non-flashloan CoW paths fix(swap): optimize quote on aTokens for non-flashloan CoW paths May 12, 2026
AGMASO
AGMASO previously approved these changes May 12, 2026
@mgrabina
Copy link
Copy Markdown
Contributor Author

@codex

@github-actions
Copy link
Copy Markdown

@github-actions
Copy link
Copy Markdown

📦 Next.js Bundle Analysis for aave-ui

This analysis was generated by the Next.js Bundle Analysis action. 🤖

This PR introduced no changes to the JavaScript bundle! 🙌

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Chef's kiss.

ℹ️ 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".

@mgrabina mgrabina requested a review from AGMASO May 12, 2026 16:59
sammdec
sammdec previously approved these changes May 12, 2026
useMaxNativeAmount was unconditionally writing forcedMaxValue =
sourceToken.balance for every token. SwapAssetInput's Max button then
passed that forced value directly (skipping handleInputChange's '-1'
path), so the aToken 1-wei shave in getMaxBalanceForToken never ran
for non-native sells — the order kept posting with the SDK's
underlyingBalance, which is 1 wei above aToken.balanceOf for aTokens.

Only force a max for native source tokens (where we actually need to
reserve gas). For everything else leave forcedMaxValue undefined so
the Max button passes '-1' and the shave fires.
@mgrabina mgrabina force-pushed the fix/cow-quote-token-alignment branch from 33157e6 to 47b0c81 Compare May 12, 2026 18:02
The SDK's underlyingBalance is computed by getLinearBalance which does
rayToWad(rayMul(wadToRay(scaledBal), normalizedIncome)) — two half-up
rayMul rounds where Aave's on-chain rayMul only does one. The result
can be 1 wei above aToken.balanceOf at the same block, so a "max" CoW
order's sellAmount ends up exceeding what's actually transferable and
the swap fails.

When the source/destination token of a max click is an aToken
(addressToSwap differs from underlyingAddress), shave the last wei
before setting inputAmount/outputAmount. Caps the dust loss at 1 wei
and keeps the swap fillable.
@mgrabina
Copy link
Copy Markdown
Contributor Author

@codex

@github-actions
Copy link
Copy Markdown

@github-actions
Copy link
Copy Markdown

📦 Next.js Bundle Analysis for aave-ui

This analysis was generated by the Next.js Bundle Analysis action. 🤖

This PR introduced no changes to the JavaScript bundle! 🙌

@mgrabina mgrabina requested a review from sammdec May 12, 2026 18:15
@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Delightful!

ℹ️ 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".

@mgrabina mgrabina merged commit b0890e2 into main May 13, 2026
20 of 21 checks passed
@mgrabina mgrabina deleted the fix/cow-quote-token-alignment branch May 13, 2026 13:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants