Skip to content

feat: leverage a supplied position through the CoW flash-loan adapters - #3102

Draft
mgrabina wants to merge 9 commits into
feat/cow-adapters-v3-redeployfrom
feat/leverage-adapter
Draft

mgrabina wants to merge 9 commits into
feat/cow-adapters-v3-redeployfrom
feat/leverage-adapter

Conversation

@mgrabina

@mgrabina mgrabina commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Adds a Leverage flow to the v3 dashboard. From a supplied position's overflow menu the user picks how much more of that collateral to acquire (or a 1.5x / 2x / 3x / Max multiplier). A CoW order flash-loans a borrowable asset, sells it for the collateral, and the post-hook supplies the collateral and draws the debt on the user's behalf through credit delegation. The order is inverted relative to the other position swaps: the first input is the collateral acquired, the second is the debt that finances it.

What changed

  • Uses AaveFlashLoanType.Leverage from @cowprotocol/sdk-flash-loans. The SDK encodes the leverageWithFlashLoan post-hook, with the credit delegation as its second argument, and sets the leverage dapp id.
  • Leverage orders record their adapter factory in the local history, so cancel goes to the factory that created them.
  • LeverageModal / LeverageModalContent / LeverageDetails / LeverageMultiplierSelector / LeverageActionsViaCoW.
  • calculateHFAfterLeverage in hfUtils: collateral and debt both grow, which no existing from/to pair expresses. Collateral that cannot count for the user (zero effective LTV, isolation) is left out so the projection does not flatter the position.
  • Leverage sizing math in helpers/shared/leverage.helpers.ts, with unit tests.
  • Supplied row actions collapse into a single overflow menu (Withdraw, Leverage). Cypress withdraw steps open the menu first.
  • New blocking guard: swapping into an asset the user has not enabled as collateral is blocked for collateral swap and leverage, since the adapter reverts with BuyTokenNotUsingAsCollateral.
  • Zero-LTV guard exempts leverage (it withdraws nothing), supply cap guard covers the supplied collateral, adapter approval notice and APY input labels cover leverage.

Stacked on #3134

Based on #3134, which moves the adapters to the new V3 deployment, including the leverage adapter 0xaEE8B0666BF8a584e9D67bFaa65007E74c70b26a. Needs cowprotocol/cow-sdk#1059 (the Leverage flash-loan type and the adapter domain name), which #3134 bumps to once it's released.

Rebased onto main

Adapted to cow-sdk 9 (flashLoanAmount replaces sellAmount/buyAmount in the posting settings, Lens dropped and Solana added to SupportedChainId) and the v3 uplift (secondary14 typography and the Warning primitive are gone, overflow buttons use DotsHorizontalIcon on a tertiary button).

Not done in this PR

  • Order history has no ActionName for leverage. Settled leverage orders fall back to the plain Swap label.

Checks

Against a local build of cowprotocol/cow-sdk#1059, tsc --noEmit and pnpm lint pass and leverage.helpers.spec.ts passes 12/12. The i18n catalogs were regenerated after the rebase. Rerun pnpm build once the SDK version is published. Cypress e2e runs in CI.

@vercel

vercel Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
interface Error Error Oct 12, 2026 5:06pm UTC

Request Review

@odin-by-borg

odin-by-borg Bot commented Sep 17, 2026

Copy link
Copy Markdown

Mjolnir Security Review

StatusQueued for review...

View in Odin

@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. 🤖

⚠️ Global Bundle Size Increased

Page Size (compressed)
global 1.34 MB (🟡 +1.42 KB)
Details

The global bundle is the javascript bundle that loads alongside every page. It is in its own category because its impact is much higher - an increase to its size means that every page on your website loads slower, and a decrease means every page loads faster.

Any third party scripts you have added directly to your app using the <script> tag are not accounted for in this analysis

If you want further insight into what is behind the changes, give @next/bundle-analyzer a try!

Seven Pages Changed Size

The following pages changed size from the code in this PR compared to its base branch:

Page Size (compressed) First Load
/ 92.18 KB (🟡 +370 B) 1.43 MB
/dashboard 82.1 KB (🟡 +370 B) 1.42 MB
/history 32.57 KB (🟡 +6 B) 1.37 MB
/reserve-overview 38.85 KB (🟡 +6 B) 1.38 MB
/safety-module 55.18 KB (🟡 +4 B) 1.39 MB
/sgho 92.25 KB (🟡 +4 B) 1.43 MB
/staking 31.51 KB (🟡 +2 B) 1.37 MB
Details

Only the gzipped size is provided here based on an expert tip.

First Load is the size of the global bundle plus the bundle for the individual page. If a user were to show up to your website and land on a given page, the first load size represents the amount of javascript that user would need to download. If next/link is used, subsequent page loads would only need to download that page's bundle (the number in the "Size" column), since the global bundle has already been downloaded.

Any third party scripts you have added directly to your app using the <script> tag are not accounted for in this analysis

Next to the size is how much the size has increased or decreased compared with the base branch of this PR. If this percentage has increased by 20% or more, there will be a red status indicator applied, indicating that special attention should be given to this.

Replaces the imported AaveFlashLoanType with a local FlashLoanFlow enum. The upstream
one in @cowprotocol/sdk-flash-loans has only CollateralSwap, DebtSwap and
RepayCollateral, so a Leverage adapter could not be named at all. The string values
match theirs, and the one place that widening happens is toSdkFlashLoanType, so the
SDK's per-type helpers keep working; those helpers select the adapter from
hookAdapterPerType[type][chainId] and so will drive Leverage unchanged once it has an
address.

Adds SwapType.Leverage and fills the four exhaustive maps the type checker flagged:
CoW gas limit 0 (flash-loan flows take gas from the hook limits), Paraswap 0 (it
cannot route leverage), an app code, and OrderKind.BUY, matching the V4 backend, since
leverage targets a collateral amount to acquire and finances it by selling the
flash-loaned debt.

The Leverage adapter addresses are left empty on purpose. The suite is not on mainnet
yet, and an empty entry makes the flow unavailable rather than encoding against the
zero address.
The V3 adapters gained `_enforceUsingAsCollateral(_buyToken, owner)`, which reverts the
postHook with `BuyTokenNotUsingAsCollateral`. Only the collateral swap and leverage
adapters call it, so those are the two flows guarded here; debt swap and repay with
collateral never run the check.

Without this the order is accepted and fails at settlement, which the user sees as a
swap that silently never executes.

A reserve the user has never supplied is deliberately not blocked: it has no user
configuration yet, and supplying through the flow sets it as collateral. Only an
explicit "enabled: false" blocks.

Also registers Leverage in ProtocolSwapState and isProtocolSwapState, since leverage
carries source and destination reserves like the other protocol flows.
Adds the Leverage modal, its entry point on supplied positions, and the CoW
encoding the flow needs, so the groundwork from the previous commits becomes a
reachable flow.

The order is inverted like DebtSwap: the first input is the collateral to
acquire, the swap sells the flash-loaned debt that finances it, and the
post-hook supplies the collateral before drawing the debt. That reuses the
proven BUY-side path rather than the untested non-inverted BUY branch.

`@cowprotocol/sdk-flash-loans` cannot encode this post-hook: each of its
encoders passes a single tuple, while `leverageWithFlashLoan` takes an ERC20
permit and a credit delegation. Everything else in `getOrderPostingSettings` is
type-agnostic, so a subclass overrides only the post-hook calldata and the dapp
id, and the adapter address arrives through the existing `hookAdapterPerType`
config.

Health factor needed a new projection: leverage raises collateral and debt
together, which no `fromAssetType`/`toAssetType` pair in `calculateHFAfterSwap`
expresses, since every combination there withdraws or repays one side.

Limit orders are off for now, and the adapter address stays empty until the
mainnet deployment, so the flow renders and quotes but cannot post an order yet.
Three buttons overflow the supplied position row: ListButtonsColumn has a fixed
min and max width, so Withdraw was clipped. Desktop now follows the Assets to
Supply pattern, a "..." button opening a menu, which also leaves room for
whatever action comes next. Mobile keeps a plain button on its own line, where
there is vertical space for it.
Matches the Assets to Supply row: a single primary button plus a "..." menu.
Withdraw joins Leverage in the menu, which keeps the row inside the fixed width
of ListButtonsColumn however many actions we add later.

The menu renders in a portal, so the row-scoped `button:contains("Withdraw")`
the e2e withdraw steps relied on no longer matches. Both call sites now open the
menu and click the item by data-cy.
Adds the multiplier the flow is actually about: the preview shows position
leverage before and after, and a picker sets the collateral amount from a target
multiple instead of a token amount.

Buying t USD of collateral with t USD of new debt leaves equity untouched, so a
target solves in closed form as t = target * equity - collateral. `Max` instead
solves for the safe health factor, making that factor the only ceiling, the same
rule the backend quote applies.

Net apy was a bare difference of the two reserve rates, which ignored how much
is being supplied against how much is borrowed. It is now weighted by the
supplied and borrowed amounts of the quote.

Limit orders are enabled, matching the other CoW adapter flows.
cow-sdk 9 derives hook sell/buy amounts from the signed order and takes a
single flashLoanAmount, matching how the collateral swap now posts. The SDK
also dropped Lens and added Solana to SupportedChainId, so the leverage
adapter map follows. The uplift removed the secondary14 typography variant
and the Warning primitive; the leverage details and blocking error use h5
and a small Alert like the surrounding swap UI.
The zero-LTV guard treated the flash-loaned debt asset as a withdrawal and
blocked leverage for anyone holding an LTV-0 collateral. Leverage withdraws
nothing, so it is exempt like the debt swap. The supply cap guard now also
covers the collateral leverage supplies, the adapter approval notice shows for
the leverage instance, and the asset inputs label supply and borrow APY on
their respective sides.

Adds unit tests for the leverage sizing math and regenerates the catalogs.
@cowprotocol/sdk-flash-loans now has AaveFlashLoanType.Leverage and encodes
the leverage post-hook itself, with the credit delegation as its second
argument and an empty ERC-20 permit. That replaces the local FlashLoanFlow
enum, its casts to the SDK type, and the AaveLeverageFlashLoanSdk subclass.

Leverage orders also record their factory in the local history, so cancel
goes to the new factory rather than the legacy fallback.
@mgrabina
mgrabina force-pushed the feat/leverage-adapter branch from bb8e226 to d3cd07d Compare October 12, 2026 16:14
@mgrabina
mgrabina changed the base branch from main to feat/cow-adapters-v3-redeploy October 12, 2026 16:14

This branch had an error being deployed

1 failed deployment
Preview — d3cd07d1 Deployed Oct 12, 2026 by vercel[bot]
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.

1 participant