Skip to Content
docs

Aave V4 Liquidations

Liquidate unhealthy positions to earn liquidation bonuses and help maintain protocol stability.

Identify Target Position

First, identify a position with a health factor below 1.0 that is eligible for liquidation.

User Position
const position: UserPosition = {
  __typename: "UserPosition",
  id: "aGVsbG8",
  user: "0x456…",
  healthFactor: {
    value: BigDecimal(0.85), // Below 1.0 - eligible for liquidation
    // …
  },
  spoke: {
    address: "0x123…",
    chain: {
      chainId: 1,
      name: "Ethereum",
    },
    // …
  },
  // …
};

Finding positions with a health factor below 1.0 is out of scope here. See Health Factor.

Select Liquidation Targets

A liquidation targets one debt reserve to repay and one collateral reserve to seize, including the liquidation bonus.

Only liquidate when the liquidation bonus exceeds gas costs.

1. Choose the Debt Position

First, identify which debt position to target by looking at the user’s borrow positions. Consider targeting debts with larger amounts.

Fetch borrow positions from User Borrows, then pick the specific position you want to liquidate:

Borrow Position
const debtPosition: UserBorrowItem = {
  reserve: {
    id: "SGVsbG8h",
    onChainId: "42",
    asset: {
      underlying: { address: "0xa0b8…", info: { symbol: "USDC", name: "USD Coin" } },
    },
    // …
  },
  debt: {
    amount: {
      value: BigDecimal(2000),
    },
    exchange: {
      value: BigDecimal(2000),
      // …
    },
    // …
  },
  // …
};

If less than $1,000 USD of debt would remain in the target reserve, you must liquidate all of it.

2. Choose the Collateral Position

Next, identify which collateral position to target by examining the user’s supply positions that are enabled as collateral. Only supplies with isCollateral: true can be targeted for liquidation.

Choose collateral in the token you prefer to receive as your liquidation bonus by fetching the supply positions from User Supplies:

Supply Position
const collateral: UserSupplyItem = {
  reserve: {
    id: "V29ybGQ=",
    onChainId: "43",
    asset: {
      underlying: { address: "0xc02a…", info: { symbol: "WETH", name: "Wrapped Ether" } },
    },
    // …
  },
  isCollateral: true, // Can be liquidated
  withdrawable: {
    amount: {
      value: BigDecimal(1.5),
      // …
    },
    // …
  },
  // …
};

In Aave V4, liquidation bonuses follow a Dutch auction where lower health factors result in higher liquidation bonuses.

BonusHealth FactorMax Bonus
Liquidation Bonus by Health Factor

Liquidate the Position

Step-by-Step with AaveKit

After selecting the debt and collateral reserves, choose the repayment amount. You can send an exact value or let the protocol determine the needed amount by using max, restoring the position to a healthy state (HF ≥ 1.0).

To liquidate an unhealthy position with AaveKit, follow these steps.

1. Configure Wallet Integration

In React, first instantiate the hooks for the wallet library of your choice. This wallet will pay the debt tokens and receive the seized collateral.

  • useSendTransaction: used to send ERC-20 approval and liquidation transactions
  • useSignTypedData: used to sign ERC-20 permits when available
Viem
import { useWalletClient } from "wagmi";
import { useSendTransaction, useSignTypedData } from "@aave/react/viem";

// …

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

2. Define the Liquidation Flow

Then, in React, use the useLiquidatePosition hook to prepare the liquidation operation.

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

const [liquidatePosition, { loading, error }] = useLiquidatePosition(
  (plan, { cancel }) => {
    switch (plan.__typename) {
      case "TransactionRequest":
        return sendTransaction(plan);
      case "Erc20Approval":
        // If token supports EIP-2612 permits, sign permit (recommended)
        if (plan.bySignature) {
          return signTypedData(plan.bySignature);
        }
        // Otherwise use traditional approval transaction
        return sendTransaction(plan.byTransaction);

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

In the Erc20Approval case, the bySignature field is only available if the token supports EIP-2612 permits.

For tokens that need an allowance reset, like USDT on Ethereum Mainnet, the hook calls your callback twice: reset to 0, then set the new value. bySignature is null for these.

3. Execute the Liquidation

Then, execute the liquidation operation.

  • TypeScript: Use the liquidatePosition action to create the transaction request for liquidating the unhealthy position.
  • GraphQL: Use the liquidatePosition query to prepare the liquidation execution plan. Specify an exact debt amount to cover, or use amount: { max: true } to bring the position back to a healthy state.

Set receiveShares: true to receive the seized collateral as supply shares instead of the underlying asset (defaults to false).

import {
  bigDecimal,
  evmAddress,
  type UserBorrowItem,
  type UserPosition,
  type UserSupplyItem,
} from "@aave/react";

const execute = async (
  position: UserPosition,
  debt: UserBorrowItem,
  collateral: UserSupplyItem,
) => {
  const result = await liquidatePosition({
    collateral: collateral.reserve.id,
    debt: debt.reserve.id,
    amount: {
      exact: { value: bigDecimal("1000") }, // 1000 USDC
    },
    liquidator: evmAddress(wallet.account.address), // User's address
    user: position.user,
  });

  // …
};

In GraphQL, below is an example of a LiquidatePositionRequest object.

{
  "request": {
    "collateral": "V29ybGQ=",
    "debt": "SGVsbG8h",
    "amount": {
      "exact": {
        "value": "1000"
      }
    },
    "liquidator": "0x789…",
    "user": "0x456…"
  }
}

In TypeScript, to handle ERC-20 permit-based approval, wrap the liquidatePosition call with permitWith for the wallet library of your choice.

import { liquidatePosition } from "@aave/client/actions";
import { permitWith } from "@aave/client/viem";
import { bigDecimal, evmAddress } from "@aave/client";

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

const result = await permitWith(wallet, (permitSig) =>
  liquidatePosition(client, {
    collateral: collateral.reserve.id,
    debt: debtPosition.reserve.id,
    amount: {
      exact: { value: bigDecimal("1000"), permitSig },
    },
    liquidator: evmAddress(wallet.account.address),
    user: position.user,
  }),
);

// …

The permitWith helper:

  1. Calls the liquidatePosition action without a permit signature
  2. If the response is Erc20ApprovalRequired with permit signature available, signs the permit and calls liquidatePosition again with the signature to get an updated execution plan
  3. Otherwise, returns the execution plan unchanged (no permit needed or token doesn’t support permits)

For tokens that need an allowance reset, like USDT on Ethereum Mainnet, permitWith skips the permit and the next step handles both approvals.

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. This wallet will pay the debt tokens and receive the seized collateral.

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

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

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

sendWith handles all required approvals, including the allowance reset for tokens like USDT.

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

TransactionRequest
{
  "data": {
    "liquidatePosition": {
      "__typename": "TransactionRequest",
      "to": "0x123…",
      "from": "0x789…",
      "data": "0xc2fa746c…",
      "value": "0",
      "chainId": 1,
      "operations": ["LIQUIDATION"]
    }
  }
}

If the response is Erc20ApprovalRequired, you have two options:

Transaction-based approval

  1. Send each transaction in the approvals array in sequence
  2. Then send originalTransaction

Permit-based approval

This is available only when approvals contains a single Erc20Approval item.

  1. Sign the approvals[0].bySignature typed data with the sender’s account.
  2. Call the liquidatePosition query again with the permit signature
  3. The response will be a TransactionRequest you can then send directly
{
  "data": {
    "liquidatePosition": {
      "__typename": "Erc20ApprovalRequired",
      "reason": "Insufficient allowance for ERC-20 token transfer",
      "requiredAmount": {
        "value": "1000.000000",
        "decimals": 6
      },
      "currentAllowance": {
        "value": "0.000000",
        "decimals": 6
      },
      "approvals": [
        {
          "byTransaction": {
            "to": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
            "from": "0x789…",
            "data": "0x095ea7b3000000000000000000000000878…",
            "value": "0",
            "chainId": 1,
            "operations": null
          },
          "bySignature": {
            "types": { /* … */ },
            "primaryType": "Permit",
            "domain": { /* … */ },
            "message": { /* … */ }
          }
        }
      ],
      "originalTransaction": {
        "to": "0x123…",
        "from": "0x789…",
        "data": "0xc2fa746c…",
        "value": "0",
        "chainId": 1,
        "operations": ["LIQUIDATION"]
      }
    }
  }
}

With multiple approvals in Erc20ApprovalRequired, send each byTransaction in order, then originalTransaction. For USDT on Ethereum Mainnet, the first resets the allowance to 0 and the second sets the required amount.

If the response is an InsufficientBalanceError, inform the user that they don’t have enough funds to perform the liquidation.

InsufficientBalanceError
{
  "data": {
    "liquidatePosition": {
      "__typename": "InsufficientBalanceError",
      "required": {
        "value": "1000.000000",
        "decimals": 6
      },
      "available": {
        "value": "500.000000",
        "decimals": 6
      }
    }
  }
}

5. Handle the Result

Finally, in React and TypeScript, handle the result.

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

  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(
          "Insufficient balance:",
          `required: ${result.error.cause.required.value.toDisplayString(2)}`,
          `available: ${result.error.cause.available.value.toDisplayString(2)}`,
        );
        break;

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

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

Step-by-Step with Solidity

Start with the spoke address and target user identifying the position:

Target User Position
address spokeAddress = 0x123…;
address targetUser = 0x456…;

And with the on-chain reserve IDs for the collateral and debt:

Target Reserves
uint256 collateralReserveId = 43;
uint256 debtReserveId = 42;

Choose the amount of debt to cover:

uint256 debtToCover = 1000e6; // 1000 USDC

And follow these steps to liquidate the position onchain:

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. Approve Token Transfer

Then, retrieve the debt reserve’s underlying token and approve the Spoke to transfer the debt tokens.

Retrieve Reserve and Approve
import { IERC20 } from "aave-v4/src/dependencies/openzeppelin/IERC20.sol";

ISpoke.Reserve memory debtReserve = spoke.getReserve(debtReserveId);
IERC20 token = IERC20(debtReserve.underlying);
token.approve(address(spoke), debtToCover);

3. Execute Liquidation

Finally, call the liquidationCall function on the Spoke contract, specifying the liquidation parameters.

Liquidate Position
spoke.liquidationCall(
  collateralReserveId,
  debtReserveId,
  targetUser,
  debtToCover,
  false
);

Complete Example

Here are complete Solidity examples for checking position health and executing liquidation:

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

function canLiquidate(
  address spokeAddress,
  address user
) external view returns (bool, uint256 healthFactor) {
  ISpoke spoke = ISpoke(spokeAddress);
  ISpoke.UserAccountData memory accountData = spoke.getUserAccountData(user);

  healthFactor = accountData.healthFactor;

  if (accountData.totalDebtValueRay == 0) {
    return (false, type(uint256).max);
  }

  return (healthFactor < 1e18, healthFactor);
}