Skip to main content
Advanced flash-loan pattern. Moves an open borrow position from one lending protocol to another in a single signed transaction — no intermediate capital, no window where the position is unhealthy.
Unaudited preview — not for production funds. This recipe relies on flashloans, which are available in the ETHGlobal preview only. Those contracts are unaudited and not deployed to production — use this flow for experimentation only, not with significant value.

Scenario

A user holds a WETH-collateralised USDC borrow on Aave V3 (Base) and wants the same position on Morpho Blue instead — to capture a better rate, say. Doing this manually means finding capital to repay Aave before you can withdraw the collateral, which is exactly the chicken-and-egg that flash loans solve. The flow borrows two flash loans concurrently and unwinds both within the same transaction:
  • Flash loan #1 (Aave V3, USDC) repays the Aave debt before the Morpho borrow exists.
  • Flash loan #2 (Balancer V2, WETH) supplies the Morpho collateral before the Aave collateral is freed.
The Morpho borrow is sized to exactly principal + Aave fee so it settles flash loan #1 with zero residual; the WETH freed from Aave settles flash loan #2 (Balancer V2 charges no fee).

What this recipe demonstrates

  • The flashloan materialiser sourcing two flow inputs from two different providers.
  • Settling each leg with lifi.flashloanRepay, keyed by leg to the input it repays.
  • Threading receipt handles across protocols: aave.repaylifi.zap withdraw → morphoBlue.supplyCollateral / morphoBlue.borrow.
  • A safe sweepTo: the Aave aToken collateral is fully unwound (withdrawn to WETH), so nothing non-transferable is left on the proxy. (Contrast with flows that keep an aToken position — see the sweepTo caveat.)

Full example

Adapted from aaveToMorphoDebtMigration.ts in the composer-sdk-examples repo. Two variants that route the destination debt through a swap — ...WithSwap and ...WithSwapExactOut — build on the same shape.
import { createComposeSdk, materialisers, resources } from '@lifi/composer-sdk';

// Base mainnet (chain 8453)
const BASE_WETH = '0x4200000000000000000000000000000000000006';
const BASE_USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';

// Aave V3 on Base (pool + aWETH receipt + variable-debt USDC)
const AAVE_V3_POOL_BASE = '0xA238Dd80C259a72e81d7e4664a9801593F98d1c5';
const A_BAS_WETH = '0xD4a0e0b9149BCee3C920d2E00b5dE09138fd8bb7';
const VDEBT_BAS_USDC = '0x59dca05b6c26dbd64b5381374aAaC5CD05644C28';

// Morpho Blue WETH/USDC market on Base
const MORPHO_WETH_USDC_MARKET = {
  loanToken: BASE_USDC,
  collateralToken: BASE_WETH,
  oracle: '0xFEa2D58cEfCb9fcb597723c6bAE66fFE4193aFE4',
  irm: '0x46415998764C29aB2a25CbeA6254146D50D22687',
  lltv: '860000000000000000',
};

const collateralAmount = '1000000000000000000'; // 1 WETH
const debtAmount = '1000000000'; // 1,000 USDC
const debtFlashloanFee = '500000'; // 5 bps of principal (Aave V3 flashloan fee)

// Morpho borrow must equal principal + Aave flashloan fee so `borrowed`
// settles the USDC flashloan leg exactly.
const morphoBorrowAmount = (
  BigInt(debtAmount) + BigInt(debtFlashloanFee)
).toString();

const sdk = createComposeSdk({ baseUrl: 'https://composer.li.quest' });

const builder = sdk.flow(8453, {
  name: 'aave-to-morpho-debt-migration',
  inputs: {
    initialCollateral: resources.erc20(BASE_WETH, 8453),
    debtFlashloan: resources.erc20(BASE_USDC, 8453),
    collateralFlashloan: resources.erc20(BASE_WETH, 8453),
  },
});

// --- Phase 1: provision the Aave V3 borrow position ---

// WETH → aBasWETH (Aave V3 supply via routing edge).
const supplyToAave = builder.lifi.zap('supply-to-aave', {
  bind: { amountIn: builder.inputs.initialCollateral },
  config: { resourceOut: resources.erc20(A_BAS_WETH, 8453) },
});

// Borrow USDC against the supplied collateral; proceeds are swept to the signer.
builder.aave.borrow('borrow-from-aave', {
  bind: {},
  config: {
    pool: AAVE_V3_POOL_BASE,
    asset: BASE_USDC,
    variableDebtToken: VDEBT_BAS_USDC,
    amount: debtAmount,
  },
});

// --- Phase 2: migrate to Morpho with two concurrent flash loans ---

// Repay the Aave USDC debt with flash loan #1 (mode: 'max' clamps to the debt).
builder.aave.repay('repay-aave', {
  bind: {
    assetIn: builder.inputs.debtFlashloan,
    onBehalfOf: builder.context.executionAddress,
  },
  config: { pool: AAVE_V3_POOL_BASE, mode: 'max' },
});

// Withdraw the freed WETH from Aave (aBasWETH → WETH).
const withdrawFromAave = builder.lifi.zap('withdraw-from-aave', {
  bind: { amountIn: supplyToAave.amountOut },
  config: { resourceOut: resources.erc20(BASE_WETH, 8453) },
});

// Open the Morpho position with flash loan #2's WETH.
builder.morphoBlue.supplyCollateral('supply-to-morpho', {
  bind: { assetIn: builder.inputs.collateralFlashloan },
  config: { marketParams: MORPHO_WETH_USDC_MARKET, mode: 'exact' },
});

// Borrow exactly principal + fee from Morpho to settle the USDC leg.
const morphoBorrow = builder.morphoBlue.borrow('borrow-from-morpho', {
  bind: {},
  config: { marketParams: MORPHO_WETH_USDC_MARKET, amount: morphoBorrowAmount },
});

// Settle the USDC flash loan with the Morpho borrow.
builder.lifi.flashloanRepay('repay-debt-flashloan', {
  bind: { funds: morphoBorrow.borrowed },
  config: { leg: 'debtFlashloan' },
});

// Settle the WETH flash loan with the Aave withdrawal (Balancer V2: no fee).
builder.lifi.flashloanRepay('repay-collateral-flashloan', {
  bind: { funds: withdrawFromAave.amountOut },
  config: { leg: 'collateralFlashloan' },
});

const request = sdk.request(builder.build(), {
  signer: '0xYourSignerAddress',
  inputs: {
    initialCollateral: materialisers.directDeposit({ amount: collateralAmount }),
    debtFlashloan: materialisers.flashloan({
      providerKind: 'aave-v3',
      amount: debtAmount,
    }),
    collateralFlashloan: materialisers.flashloan({
      providerKind: 'balancer-v2',
      amount: collateralAmount,
    }),
  },
  sweepTo: builder.context.sender,
});

What to observe

  • Inputs. Three resource inputs: the user’s collateral (directDeposit) plus two flashloan-sourced inputs from different providers.
  • Why two providers. Aave V3 supplies the USDC to repay; Balancer V2 supplies the WETH collateral (and charges no fee, so its leg is settled by the withdrawn principal alone).
  • Sizing the Morpho borrow. principal + debtFlashloanFee so the borrowed resource settles the USDC leg with zero leftover.
  • Repayment is mandatory. Both flash loans must be repaid in-flow via lifi.flashloanRepay, or simulation reverts.
  • sweepTo is safe here because the Aave aToken is fully withdrawn to WETH — no bound collateral receipt remains on the proxy. When a flow keeps an aToken position, omit the blanket sweepTo and transfer only loose tokens (see the caveat).