Skip to Content
docs

Withdraw Assets on Aave V4

Withdrawing assets from Aave V4 allows you to:

  • Access your supplied assets and earned interest
  • Reduce or fully exit a supply position

Withdrawing collateral while you have open debt lowers your health factor and can put the position at risk of liquidation.

Withdraw Assets

Withdrawing can be broken down into the following steps:

  1. Identify the supply position to withdraw from
  2. Preview the impact of the withdraw operation
  3. Withdraw the assets

Identify the Supply Position

Given a list of the user’s supply positions, identify the supply position matching the token you want to withdraw and with a withdrawable amount that covers your needs.

You can’t withdraw while the reserve is paused.

For example, let’s say you have identified the following UserSupplyItem object.

Example UserSupplyItem
const supplyPosition: UserSupplyItem = {
  reserve: {
    id: "SGVsbG8h",
    onChainId: "42",
    chain: {
      chainId: 1,
      name: "Ethereum",
    },
    spoke: {
      address: "0x123…",
      // …
    },
    asset: {
      underlying: {
        address: "0xa0b86a33e6e2ad05ad6c9ac3b6e5e5f6e7b6c1b2", // USDC
      },
      // …
    },
    status: {
      paused: false,
    },
    // … other reserve properties
  },
  isCollateral: true,
  balance: {
    amount: {
      value: BigDecimal(1042.5), // principal + accrued interest
      // …
    },
    // …
  },
  withdrawable: {
    amount: {
      value: BigDecimal(1000.0), // withdrawable now, capped by available liquidity
      // …
    },
    // …
  },
  // …
};

Keep in mind that if the asset is used as collateral (isCollateral: true), withdrawing may:

  • Reduce your borrowing capacity and lower the position’s health factor, increasing the risk of liquidation.
  • Raise the User Risk Premium, and with it the borrow APY of open borrow positions on the same Spoke, if the remaining collateral has a higher reserve.settings.collateralRisk.

Preview Withdraw with AaveKit

Preview the impact of a withdraw operation before committing to it.

Use the usePreview hook (or the imperative usePreviewAction variant) in React, the preview action in TypeScript, or the preview query in GraphQL to preview the impact of the withdraw operation on the user’s position.

import { type WithdrawRequest, usePreview } from "@aave/react";

function WithdrawPreview({ request }: { request: WithdrawRequest }) {
  const { data, error, loading } = usePreview({
    action: {
      withdraw: request,
    },
  });

  if (loading) return <div>Loading…</div>;
  if (error) return <div>Error: {error.message}</div>;

  // data: PreviewUserPosition
  return (
    <div>
      <h3>Health Factor:</h3>
      <p>From: {data.healthFactor.current?.toFixed(2) ?? "N/A"}</p>
      <p>To: {data.healthFactor.after?.toFixed(2) ?? "N/A"}</p>

      <h3>User Risk Premium:</h3>
      <p>From: {data.riskPremium.current.normalized.toFixed(2)}%</p>
      <p>To: {data.riskPremium.after.normalized.toFixed(2)}%</p>

      <h3>Net APY:</h3>
      <p>From: {data.netApy.current.normalized.toFixed(2)}%</p>
      <p>To: {data.netApy.after.normalized.toFixed(2)}%</p>

      <h3>Net Collateral:</h3>
      <p>From: {data.netCollateral.current.value.toDisplayString(2)}</p>
      <p>To: {data.netCollateral.after.value.toDisplayString(2)}</p>
    </div>
  );
}
  • React: The WithdrawRequest can be as follows.
  • GraphQL: The PreviewRequest can be as follows.
const request: WithdrawRequest = {
  sender: evmAddress("0x789…"), // User's address
  reserve: supplyPosition.reserve.id,
  amount: {
    erc20: {
      exact: bigDecimal(500), // 500 USDC
    },
  },
};

The PreviewUserPosition shows the impact of the withdraw operation by comparing current and after states, with the table below outlining key fields and how to interpret them.

FieldImpact
healthFactor.[current → after]: BigDecimal|nullHigher is better
(null if not applicable)
riskPremium.[current → after]: PercentNumberLower is better
netApy.[current → after]: PercentNumberHigher is better
netCollateral.[current → after]: ExchangeAmountHigher is better
netBalance.[current → after]: ExchangeAmountUpdated balance
maxBorrowingPower.[current → after]: ExchangeAmountMaximum borrowing power
remainingBorrowingPower.[current → after]: ExchangeAmountRemaining borrowing power
otherConditions: UserPositionConditionVariation[]Dynamic config changes

Withdrawing collateral updates the Dynamic Config within the same user position.

The otherConditions field is an array of objects describing the resulting dynamic config changes.

  • CollateralFactorVariation – Collateral factor change
  • LiquidationFeeVariation – Liquidation fee change
  • MaxLiquidationBonusVariation – Maximum liquidation bonus change

You can also specify a different currency to return fiat amounts in.

import { Currency } from "@aave/react";

const { data, error, loading } = usePreview({
  action: {
    withdraw: request,
  },
  currency: Currency.Eur,
});

Preview Withdraw with Solidity

Start with the spoke address and on-chain reserve ID identified:

Target Reserve
address spokeAddress = 0x123…;
uint256 reserveId = 42;

And an amount to withdraw:

Amount to Withdraw
uint256 amountToWithdraw = 42e6; // 42 USDC

Follow the steps below to preview the impact of a withdraw operation on the user’s position.

1. Get Current Account Data

First, retrieve the user’s current account data from the Spoke contract.

Get Account Data
import { ISpoke } from "aave-v4/src/spoke/interfaces/ISpoke.sol";
import { Math } from "aave-v4/src/dependencies/openzeppelin/Math.sol";

ISpoke spoke = ISpoke(spokeAddress);
ISpoke.UserAccountData memory currentData = spoke.getUserAccountData(user);

2. Calculate Withdraw Value

Then, retrieve the reserve data and asset price from the oracle to calculate the USD value of the withdrawal amount.

Withdraw Value
address oracleAddress = spoke.ORACLE();
ISpoke.Reserve memory reserve = spoke.getReserve(reserveId);
uint256 assetPrice = IAaveOracle(oracleAddress).getReservePrice(reserveId);

uint256 withdrawValue = amountToWithdraw * assetPrice * 10 ** (18 - reserve.decimals);

3. Compute New Health Factor

Finally, calculate the new health factor by removing the withdrawn collateral value and factor.

Compute New Health Factor
ISpoke.UserPosition memory userPosition = spoke.getUserPosition(reserveId, user);
uint32 dynamicConfigKey = userPosition.dynamicConfigKey;
ISpoke.DynamicReserveConfig memory config = spoke.getDynamicReserveConfig(reserveId, dynamicConfigKey);
uint256 collateralFactor = config.collateralFactor;

uint256 currentWeightedSum = (currentData.totalCollateralValue * currentData.avgCollateralFactor) / 1e18;
uint256 removedWeightedValue = (withdrawValue * collateralFactor) / 10000;
uint256 newWeightedSum = currentWeightedSum - removedWeightedValue;

uint256 newHealthFactor = currentData.totalDebtValueRay == 0 ? type(uint256).max : Math.mulDiv(newWeightedSum * 1e18, 1e27, currentData.totalDebtValueRay);

Complete Preview Example

Here’s a complete Solidity example that calculates the health factor before a withdraw operation:

Complete Health Factor Preview
import { ISpoke } from "aave-v4/src/spoke/interfaces/ISpoke.sol";
import { IAaveOracle } from "aave-v4/src/spoke/interfaces/IAaveOracle.sol";
import { Math } from "aave-v4/src/dependencies/openzeppelin/Math.sol";

function previewWithdrawHealthFactor(
  address spokeAddress,
  uint256 reserveId,
  uint256 amountToWithdraw,
  address user
) external view returns (uint256) {
  ISpoke spoke = ISpoke(spokeAddress);

  // Step 1: Get current account data
  ISpoke.UserAccountData memory currentData = spoke.getUserAccountData(user);

  // Step 2: Calculate withdraw value
  ISpoke.Reserve memory reserve = spoke.getReserve(reserveId);
  uint256 assetPrice = IAaveOracle(spoke.ORACLE()).getReservePrice(reserveId);
  uint256 withdrawValue = amountToWithdraw * assetPrice * 10 ** (18 - reserve.decimals);

  // Step 3: Compute new health factor
  uint32 dynamicConfigKey = spoke.getUserPosition(reserveId, user).dynamicConfigKey;
  uint256 collateralFactor = spoke.getDynamicReserveConfig(reserveId, dynamicConfigKey).collateralFactor;

  uint256 currentWeightedSum = (currentData.totalCollateralValue * currentData.avgCollateralFactor) / 1e18;
  uint256 newWeightedSum = currentWeightedSum - (withdrawValue * collateralFactor) / 10000;
  return currentData.totalDebtValueRay == 0 ? type(uint256).max : Math.mulDiv(newWeightedSum * 1e18, 1e27, currentData.totalDebtValueRay);
}

Step-by-Step with AaveKit

Withdraw assets from the selected supply position.

Withdrawing collateral with open debt increases risk and updates the Dynamic Config, which updates the position’s User Risk Premium.

To withdraw assets from an Aave supply position with AaveKit, follow these steps.

1. Configure Wallet Integration

In React, first instantiate the useSendTransaction hook for the wallet library of your choice.

Viem
import { useWalletClient } from "wagmi";
import { useSendTransaction } from "@aave/react/viem";

// …

const { data: wallet } = useWalletClient();
const [sendTransaction] = useSendTransaction(wallet);

2. Define the Withdraw Flow

Then, in React, use the useWithdraw hook to prepare the withdraw operation.

import { useWithdraw } from "@aave/react";

const [withdraw, { loading, error }] = useWithdraw((plan) => {
  switch (plan.__typename) {
    case "TransactionRequest":
      return sendTransaction(plan);

    case "PreContractActionRequired":
      return sendTransaction(plan.transaction);
  }
});

3. Execute the Withdraw Operation

Then, execute the desired withdraw operation.

  • TypeScript: Use the withdraw action to prepare the withdraw execution plan.
  • GraphQL: Use the withdraw query to prepare the withdraw execution plan.
import { bigDecimal, evmAddress } from "@aave/react";

const execute = async () => {
  const result = await withdraw({
    sender: evmAddress(wallet.account.address), // User's address
    reserve: supplyPosition.reserve.id,
    amount: {
      erc20: {
        exact: bigDecimal(500), // Withdraw 500 USDC
      },
    },
  });

  // …
};

In GraphQL, below are some examples of WithdrawRequest objects.

{
  "request": {
    "sender": "0x742d35cc6e5c4ce3b69a2a8c7c8e5f7e9a0b1234",
    "reserve": "SGVsbG8h",
    "amount": {
      "erc20": {
        "exact": "500"
      }
    }
  }
}

4. Process the Execution Plan

In TypeScript, use sendWith for the wallet library of your choice to send the transactions in the execution plan, and await indexing with client.waitForTransaction.

import { withdraw } from "@aave/client/actions";
import { sendWith } from "@aave/client/viem";

import { client } from "./client";
import { wallet } from "./wallet";

const result = await withdraw(client, {
  // …
})
  .andThen(sendWith(wallet))
  .andThen(client.waitForTransaction);

In GraphQL, if the response is a TransactionRequest, you can send it directly using the wallet associated with the from address.

TransactionRequest
{
  "data": {
    "withdraw": {
      "__typename": "TransactionRequest",
      "to": "0x94e7a5dcbe816e498b89ab752661904e2f56c485",
      "from": "0x742d35cc6e5c4ce3b69a2a8c7c8e5f7e9a0b1234",
      "data": "0x0ad58d2f…",
      "value": "0",
      "chainId": 1,
      "operations": ["SPOKE_WITHDRAW"]
    }
  }
}

If the response is a PreContractActionRequired, you must first send the transaction request. Once it succeeds, you can then send the originalTransaction.

PreContractActionRequired
{
  "data": {
    "withdraw": {
      "__typename": "PreContractActionRequired",
      "reason": "Authorize the native wrapping gateway to operate on behalf of the user",
      "transaction": {
        "to": "0x94e7a5dcbe816e498b89ab752661904e2f56c485",
        "from": "0x742d35cc6e5c4ce3b69a2a8c7c8e5f7e9a0b1234",
        "data": "0x095ea7b3000000000000000000000000878…",
        "value": "0",
        "chainId": 1,
        "operations": null
      },
      "originalTransaction": {
        "to": "0x94e7a5dcbe816e498b89ab752661904e2f56c485",
        "from": "0x742d35cc6e5c4ce3b69a2a8c7c8e5f7e9a0b1234",
        "data": "0x0ad58d2f…",
        "value": "0",
        "chainId": 1,
        "operations": ["SPOKE_WITHDRAW"]
      }
    }
  }
}

If the response is an InsufficientBalanceError, inform the user that they don’t have enough balance to withdraw the requested amount.

InsufficientBalanceError
{
  "data": {
    "withdraw": {
      "__typename": "InsufficientBalanceError",
      "required": {
        "value": "500.000000",
        "decimals": 6
      },
      "available": {
        "value": "250.500000",
        "decimals": 6
      }
    }
  }
}

5. Handle the Result

Finally, in React and TypeScript, handle the result.

Example
const execute = async () => {
  const result = await withdraw(/* … */);

  if (result.isErr()) {
    switch (result.error.name) {
      case "CancelError":
        // The user cancelled the operation
        return;

      case "SigningError":
        console.error(
          `Failed to sign the transaction: ${result.error.message}`,
        );
        break;

      case "TimeoutError":
        console.error(`Transaction timed out: ${result.error.message}`);
        break;

      case "TransactionError":
        console.error(`Transaction failed: ${result.error.message}`);
        break;

      case "ValidationError":
        console.error(`Invalid withdrawal amount: ${result.error.message}`);
        break;

      case "UnexpectedError":
        console.error(result.error.message);
        break;
    }
    return;
  }

  console.log("Withdraw successful with hash:", result.value.txHash);
};

Step-by-Step with Solidity

Use the same identifiers and amount to withdraw used to preview the withdraw operation:

Target Reserve
address spokeAddress = 0x123…;
uint256 reserveId = 42;
uint256 amountToWithdraw = 42e6; // 42 USDC

Complete the withdraw process with the following steps:

1. Import the Spoke Interface

First, create an instance of the ISpoke interface.

Import the Spoke Interface
import { ISpoke } from "aave-v4/src/spoke/interfaces/ISpoke.sol";

ISpoke spoke = ISpoke(spokeAddress);

2. Withdraw Assets

Finally, call the withdraw function on the Spoke contract, specifying the reserve ID, amount, and the onBehalfOf address (the position owner). The withdrawn assets are sent to msg.sender.

Withdraw
spoke.withdraw(
  reserveId,
  amountToWithdraw,
  msg.sender
);

Complete Withdraw Example

Here’s a complete Solidity example that executes the full withdraw flow:

Complete Withdraw Flow
import { ISpoke } from "aave-v4/src/spoke/interfaces/ISpoke.sol";

function withdrawFromReserve(
  address spokeAddress,
  uint256 reserveId,
  uint256 amountToWithdraw,
  address onBehalfOf
) external {
  ISpoke(spokeAddress).withdraw(reserveId, amountToWithdraw, onBehalfOf);
}

Advanced Usage

Network Fee

This experimental React hook only works with Viem or Wagmi integrations.

Estimate the network cost of any action using the same PreviewAction you pass to the usePreview hook.

For example:

PreviewAction
import { type PreviewAction } from "@aave/react";

const action: PreviewAction = {
  withdraw: {
    sender: evmAddress("0x123…"), // User's address
    reserve: supplyPosition.reserve.id,
    amount: {
      erc20: {
        exact: bigDecimal(42), // USDC
      },
    },
  },
};

Use the useNetworkFee hook to estimate both the network fee for the provided action and its fiat equivalent.

Viem
import { type PreviewAction, Currency } from "@aave/react";
import { useNetworkFee } from "@aave/react/viem";

function NetworkFee({ action }: { action: PreviewAction }) {
  const {
    data: fee,
    loading,
    error,
  } = useNetworkFee({
    query: { estimate: action },
    currency: Currency.Eur,
  });

  if (loading) return <p>Loading fee…</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <p>
      Network Fee: {fee.amount.value.toDisplayString(2)} {fee.token.info.symbol}
      <span>
        ≈{fee.exchange.symbol}
        {fee.exchange.value.toDisplayString(2)}
      </span>
    </p>
  );
}

Native Tokens with AaveKit

When the Reserve’s underlying token is the wrapped version of the chain’s native token (e.g., WETH on Ethereum), you can withdraw the asset as the chain’s native token using the Native Token Gateway.

Use the reserve.asset.underlying.isWrappedNativeToken flag to determine if the underlying token is a wrapped native token. The Native Token Gateway address is available from the chain details.

WETH Supply Position
const supplyPosition: UserSupplyItem = {
  reserve: {
    id: "SGVsbG8h",
    onChainId: "42",
    asset: {
      underlying: {
        address: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
        info: {
          name: "Wrapped Ether",
          symbol: "WETH",
          decimals: 18,
          // …
        },
        isWrappedNativeToken: true,
        // …
      },
      // …
    },
    spoke: {
      address: "0x123…",
      // …
    },
    chain: {
      chainId: 1,
      name: "Ethereum",
      nativeGateway: "0xabc…",
    },
    // …
  },
  balance: {
    amount: {
      value: BigDecimal(1.25),
      // …
    },
    // …
  },
  // …
};

Specify the amount in the amount field as a native value.

const execute = async () => {
  const result = await withdraw({
    sender: evmAddress(wallet.account.address), // User's address
    reserve: supplyPosition.reserve.id,
    amount: {
      native: {
        exact: bigDecimal(1), // Withdraw 1 ETH
      },
    },
  });

  // …
};

Native Tokens with Solidity

Start with the spoke address, reserve ID, and native gateway address identified:

Native Token Gateway
address spokeAddress = 0x123…;
uint256 reserveId = 42;
address nativeGateway = 0xabc…;

Follow the steps below to withdraw native tokens (e.g., ETH) from an existing supply position in a wrapped native token reserve (e.g., WETH).

1. Approve the Gateway

First, approve the Native Token Gateway as your position manager. This is a one-time setup step.

Approve Gateway
import { ISpoke } from "aave-v4/src/spoke/interfaces/ISpoke.sol";

ISpoke spoke = ISpoke(spokeAddress);
spoke.setUserPositionManager(nativeGateway, true);

2. Withdraw Native Tokens

Then, call withdrawNative on the gateway to withdraw and unwrap the tokens.

import { INativeTokenGateway } from "aave-v4/src/position-manager/interfaces/INativeTokenGateway.sol";

INativeTokenGateway gateway = INativeTokenGateway(nativeGateway);
gateway.withdrawNative(spokeAddress, reserveId, amountToWithdraw);

The gateway calls spoke.withdraw on your behalf, unwraps the token (WETH → ETH), and sends the native token to the caller.

Complete Native Token Example

Here’s a complete Solidity example that executes the native token withdraw flow. This helper acts on the contract’s own position. The gateway sends the native tokens to the contract, so it needs a receive() function. To withdraw for a user’s wallet, the user calls setUserPositionManager and the gateway directly.

Complete Native Withdraw Flow
import { ISpoke } from "aave-v4/src/spoke/interfaces/ISpoke.sol";
import { INativeTokenGateway } from "aave-v4/src/position-manager/interfaces/INativeTokenGateway.sol";

function withdrawNativeToken(
  address spokeAddress,
  address nativeGateway,
  uint256 reserveId,
  uint256 amountToWithdraw
) external {
  ISpoke spoke = ISpoke(spokeAddress);

  // Step 1: Approve gateway as position manager (one-time setup)
  if (!spoke.isPositionManager(address(this), nativeGateway)) {
    spoke.setUserPositionManager(nativeGateway, true);
  }

  // Step 2: Withdraw native tokens via gateway
  INativeTokenGateway(nativeGateway).withdrawNative(spokeAddress, reserveId, amountToWithdraw);
}

receive() external payable {}