fix(swap): optimize quote on aTokens for non-flashloan CoW paths#2978
Conversation
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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
💡 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".
|
📦 Next.js Bundle Analysis for aave-uiThis analysis was generated by the Next.js Bundle Analysis action. 🤖 This PR introduced no changes to the JavaScript bundle! 🙌 |
828e5d4 to
6b78f35
Compare
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.
|
📦 Next.js Bundle Analysis for aave-uiThis analysis was generated by the Next.js Bundle Analysis action. 🤖 This PR introduced no changes to the JavaScript bundle! 🙌 |
|
📦 Next.js Bundle Analysis for aave-uiThis analysis was generated by the Next.js Bundle Analysis action. 🤖 This PR introduced no changes to the JavaScript bundle! 🙌 |
|
Codex Review: Didn't find any major issues. Chef's kiss. ℹ️ 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". |
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.
33157e6 to
47b0c81
Compare
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.
|
📦 Next.js Bundle Analysis for aave-uiThis analysis was generated by the Next.js Bundle Analysis action. 🤖 This PR introduced no changes to the JavaScript bundle! 🙌 |
|
Codex Review: Didn't find any major issues. Delightful! ℹ️ 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
For
CollateralSwap(no flashloan) andWithdrawAndSwap, the CoW order is posted viaSwapActionsViaCoWwithstate.sourceToken.addressToSwap/state.destinationToken.addressToSwap— the aTokens. But the quote inuseSwapQuote.tswas always fetched onunderlyingAddressfor 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 -> aEthUSDScollateral 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
addressToSwapfor non-flashloan Paraswap.getTokenSelectionForQuoteto a singleusesAddressToSwapflag.state.useFlashloan === false, quote tokens are nowaddressToSwap(matching what the order posts).Behavioral changes (truth table)
CollateralSwapWithdrawAndSwapSwapDebtSwapandRepayWithCollateralon CoW always haveuseFlashloan = trueviaforceFlashloanFlow, so they're unaffected.Out of scope (follow-ups)
× 3networkFeehack inuseSwapOrderAmounts.tsis a workaround forgetAppDataForQuotereturningundefined. 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
CollateralSwap(no flashloan) on an aToken pair where the underlying and aToken pairs land in different CoW volume-fee tiers — confirm the order'sbuyAmountcushion matches the user-selected slippage when checked against a fresh CoW quote on the aToken pair.WithdrawAndSwapfrom an aToken supply to a regular ERC20 — confirm quote tokens match what the order posts.CollateralSwapwith flashloan (HF below threshold) — confirm quote still goes on underlying, order still posts on underlying via the flashloan adapter, no regression.DebtSwapandRepayWithCollateral— confirm quote tokens unchanged (both still on underlying viaforceFlashloanFlow).