Skip to content

fix(swap): align flash-loan and zero-LTV guards with cow-swap-adapter sellToken#2960

Open
mgrabina wants to merge 2 commits intomainfrom
fix/risk-action-swap-guards
Open

fix(swap): align flash-loan and zero-LTV guards with cow-swap-adapter sellToken#2960
mgrabina wants to merge 2 commits intomainfrom
fix/risk-action-swap-guards

Conversation

@mgrabina
Copy link
Copy Markdown
Contributor

@mgrabina mgrabina commented Apr 29, 2026

Summary

Both InsufficientLiquidityBlockingGuard and ZeroLTVBlockingGuard derived the asset to check from state.sourceToken / state.destinationToken gated on isInvertedSwap. For inverted swaps (DebtSwap, RepayWithCollateral) the mapping was the wrong direction: the guards inspected the user's debt asset when the cow-swap-adapter actually flash-loans and withdraws the destination.

The cow-swap-adapter contracts in cow-swap-adapters/src/adapters/v3/*Adapter.sol unambiguously flash-loan and withdraw _sellToken for every position swap. The interface already exposes that asset as state.sellAmountToken in useSwapOrderAmounts. Reading it directly aligns both guards with the on-chain behavior and removes the inverted-swap heuristic.

Use-case behavior — before vs. after

InsufficientLiquidityBlockingGuard

Scenario Before After
Direct Swap (no Aave) not relevant not relevant — isProtocolSwapState excludes it
DebtSwap, healthy reserves not blocked not blocked
DebtSwap, target reserve drained reads sourceReserve (old debt) → typically misses reads sellAmountToken reserve (new debt) → blocks
DebtSwap, borrow-cap exhausted on target already blocked still blocked (clamp kept, only for DebtSwap)
CollateralSwap WETH → USDC, WETH reserve drained (rsETH) not checked at all blocks (source reserve insufficient)
WithdrawAndSwap, source reserve drained, useFlashloan=true not checked at all blocks
RepayWithCollateral, withdrawn collateral reserve drained dispatcher excluded RepayWithCollateral blocks
ParaSwap DebtSwap (always flash-loans regardless of state.useFlashloan) check disabled, settlement reverts blocks

ZeroLTVBlockingGuard

Scenario Before After
Direct Swap could fall through explicitly skipped
DebtSwap (any) already skipped still skipped (no aToken withdraw on-chain)
CollateralSwap with rsETH (LTV=0), swapping unrelated collateral blocked blocked
CollateralSwap, swapping the LTV=0 asset itself out wrongly blocked not blocked (matches assetLTV == 0 exception)
RepayWithCollateral with LTV=0 asset enabled (e.g. rsETH), repaying USDe debt with USDT collateral (issue 3) not blocked — compared against user's debt (sourceToken) blocks (compares against withdrawn collateral = sellAmountToken)
RepayWithCollateral, withdrawing the LTV=0 asset itself as collateral wrongly blocked not blocked

Real cases this fixes

  1. CollateralSwap WETH → USDC when WETH reserve is drained (recent rsETH-related risk action). Aave V3 mainnet pool's virtualUnderlyingBalance(WETH) was 0.022 WETH while the adapter tried to flash-loan 1 WETH. Pool.flashLoan reverted with arithmetic underflow at the receiver-callback boundary. The previous guard only fired for DebtSwap, so the order reached the orderbook and the solver hit the panic at settlement.
  2. RepayWithCollateral with non-LTV-0 collateral when the user also has an LTV=0 asset (e.g. rsETH) enabled as collateral. On-chain validateHFAndLtvzero reverted because the withdrawn collateral has positive LTV while a zero-LTV collateral is enabled. The previous check compared against the user's debt asset (sourceToken) instead of the withdrawn collateral, so the order went out and reverted on settlement with LtvValidationFailed.

Developer Notes

  • Single source of truth across the interface: state.sellAmountToken matches _sellToken in every cow-swap-adapter v3/v4 file. Source/destination + isInvertedSwap is no longer needed for these guards.
  • formattedAvailableLiquidity is the aToken's underlying balanceOf, which is a strict upper bound on the pool's virtualUnderlyingBalance for typical reserves and catches the rsETH-style drainage cleanly. A live Pool.getVirtualUnderlyingBalance(asset) read would be more precise but is out of scope for this fix.
  • The borrow-cap clamp stays only for DebtSwap; CollateralSwap, RepayWithCollateral, and WithdrawAndSwap repay the flash loan in-flight and never touch the borrow cap.
  • The check is not gated on state.useFlashloan because several protocol paths flash-loan unconditionally regardless of the flag (CoW DebtSwap / RepayWithCollateral via forceFlashloanFlow, ParaSwap DebtSwap via DebtSwitchAdapter with no non-flashloan branch, ParaSwap CollateralSwap with useFlashLoan: true hardcoded). Even non-flashloan paths still withdraw/borrow from the pool, which decrements virtualUnderlyingBalance — the same liquidity ceiling.
  • Net diff: 3 files, ~+68/-46 lines. Most of the addition is comments anchoring the guards to the relevant Aave V3 / cow-swap-adapter source.

Reviewer Checklist

  • End-to-end tests are passing without any errors
  • Code changes do not significantly increase the application bundle size
  • If there are new 3rd-party packages, they do not introduce potential security threats
  • If there are new environment variables being added, they have been added to the .env.example file as well as the pertinent .github/actions/* files
  • There are no CI changes, or they have been approved by the DevOps and Engineering team(s)

Test plan

  • On a fork pinned to the failing block, open CollateralSwap WETH → USDC for 1 WETH and confirm the inline error renders with WETH and the action is disabled.
  • On a fork with the failing user position (USDe debt + USDT collateral + LTV=0 asset enabled), open RepayWithCollateral and confirm the LTV-zero error fires before submission.
  • Regression: DebtSwap on ParaSwap with healthy reserves still computes borrow-cap room correctly and only blocks when the new debt asset would exceed it (verifies the dropped useFlashloan gate doesn't false-positive).
  • Regression: CollateralSwap of an LTV=0 asset itself (e.g. swap rsETH out) is not over-blocked.
  • Regression: plain Swap (direct DEX, no flash loan) does not trip the liquidity guard.

… sellToken

For inverted swaps (DebtSwap, RepayWithCollateral) both guards were deriving
the asset to check from sourceToken/destinationToken via isInvertedSwap and
landing on the wrong side. The cow-swap-adapter contracts unambiguously
flash-loan and withdraw _sellToken, exposed in state as sellAmountToken.

Switching both guards to read sellAmountToken aligns them with the on-chain
behavior and removes the inverted-swap heuristic. Also gates the liquidity
guard on useFlashloan, supports all flash-loan swap types (drops the
RepayWithCollateral exclusion in the dispatcher), and applies the borrow-cap
clamp only for DebtSwap (the only flow that leaves the user holding new debt).

Fixes the rsETH-style virtual-balance reverts on CollateralSwap and the
validateHFAndLtvzero reverts on RepayWithCollateral when the user holds an
LTV=0 collateral asset alongside positive-LTV collateral.
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 29, 2026

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

Project Deployment Actions Updated (UTC)
interface Ready Ready Preview, Comment Apr 29, 2026 6:36pm

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: d36a6d8dc6

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

@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! 🙌

…ashloan

Codex review caught that gating on state.useFlashloan disabled the guard for
several flows that always flash-loan regardless of the flag: DebtSwap on both
CoW (forceFlashloanFlow) and ParaSwap (DebtSwitchAdapter has no non-flashloan
branch), plus ParaSwap CollateralSwap (useFlashLoan: true hardcoded). Even on
non-flashloan paths the withdraw/borrow still decrements virtualUnderlyingBalance,
so the same liquidity ceiling applies.

Removes the gate; the check now runs for every ProtocolSwap state. Direct
SwapType.Swap is still excluded by isProtocolSwapState.
@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! 🙌

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.

2 participants