Repay Loans on Aave V4
Repaying borrowed assets allows you to:
- Reduce or eliminate your debt position
- Improve your position’s health factor and reduce liquidation risk
- Free up collateral for withdrawal or additional borrowing
- Close your borrow position entirely when repaying the full amount
Repay Loans
Repaying a loan can be broken down into the following steps:
- Identify the borrow position to repay
- Preview the impact of the repay operation
- Repay the borrowed assets
Identify the Borrow Position
Given a list of the user’s borrow positions, identify the position (token) you want to reduce and choose the repayment amount (partial or full).
You can’t repay while the reserve is paused.
For example, let’s say you have identified the following User object.
const borrowPosition: UserBorrowItem = {
reserve: {
id: "SGVsbG8h",
onChainId: "42",
chain: {
chainId: 1,
name: "Ethereum",
},
spoke: {
address: "0x123…",
// …
},
asset: {
underlying: {
address: "0xa0b86a33e6e2ad05ad6c9ac3b6e5e5f6e7b6c1b2", // USDC
},
// …
},
status: {
paused: false,
},
// … other reserve properties
},
debt: {
amount: {
value: BigDecimal(512.023456), // ~512 USDC debt
// …
},
// …
},
// …
};
Preview Repay with AaveKit
Preview the impact of a repay operation before committing to it.
Use the use hook (or the imperative use variant) in React, the preview action in TypeScript, or the preview query in GraphQL to preview the impact of the repay operation on the user’s position.
import { type RepayRequest, usePreview } from "@aave/react";
function RepayPreview({ request }: { request: RepayRequest }) {
const { data, error, loading } = usePreview({
action: {
repay: 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>
);
}
import { type RepayRequest, usePreview } from "@aave/react";
function RepayPreview({ request }: { request: RepayRequest }) {
const { data, error, loading } = usePreview({
action: {
repay: request,
},
suspense: true,
});
// 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>
);
}
import { type RepayRequest, usePreviewAction } from "@aave/react";
const [preview, { called, data, error, loading }] = usePreviewAction();
const handler = async (request: RepayRequest) => {
const result = await preview({
action: {
repay: request,
},
});
if (result.isOk()) {
console.log(result.value); // PreviewUserPosition
} else {
console.error(result.error);
}
};
import { preview } from "@aave/client/actions";
import { bigDecimal, evmAddress } from "@aave/client";
const result = await preview(client, {
action: {
repay: {
sender: evmAddress("0x789…"), // User's address
reserve: borrowPosition.reserve.id,
amount: {
erc20: {
value: {
exact: bigDecimal(250), // 250 USDC
},
},
},
},
},
});
if (result.isOk()) {
console.log(result.value); // PreviewUserPosition
} else {
console.error(result.error);
}
import { preview } from "@aave/client/actions";
import { evmAddress } from "@aave/client";
const result = await preview(client, {
action: {
repay: {
sender: evmAddress("0x789…"), // User's address
reserve: borrowPosition.reserve.id,
amount: {
erc20: {
value: {
max: true, // Repay the full debt amount
},
},
},
},
},
});
if (result.isOk()) {
console.log(result.value); // PreviewUserPosition
} else {
console.error(result.error);
}
query ($request: PreviewRequest!, $currency: Currency = USD) {
preview(request: $request) {
__typename
id
healthFactor {
...HealthFactorResult
}
netApy {
...PercentNumberVariation
}
riskPremium {
...PercentNumberVariation
}
netCollateral(currency: $currency) {
...ExchangeAmountVariation
}
netBalance(currency: $currency) {
...ExchangeAmountVariation
}
projectedEarnings {
...ExchangeAmountVariation
}
maxBorrowingPower {
...ExchangeAmountVariation
}
remainingBorrowingPower {
...ExchangeAmountVariation
}
otherConditions {
...UserPositionConditionVariation
}
}
}
fragment HealthFactorResult on HealthFactorResult {
__typename
... on HealthFactorVariation {
...HealthFactorVariation
}
... on HealthFactorError {
...HealthFactorError
}
}
fragment HealthFactorVariation on HealthFactorVariation {
__typename
current
after
}
fragment HealthFactorError on HealthFactorError {
__typename
reason
current
after
}
fragment UserPositionConditionVariation on UserPositionConditionVariation {
__typename
... on CollateralFactorVariation {
reserveId
token {
...Erc20Token
}
current {
...PercentNumber
}
after {
...PercentNumber
}
}
... on LiquidationFeeVariation {
reserveId
token {
...Erc20Token
}
current {
...PercentNumber
}
after {
...PercentNumber
}
}
... on MaxLiquidationBonusVariation {
reserveId
token {
...Erc20Token
}
current {
...PercentNumber
}
after {
...PercentNumber
}
}
}
fragment PercentNumberVariation on PercentNumberVariation {
__typename
current {
...PercentNumber
}
after {
...PercentNumber
}
}
fragment ExchangeAmountVariation on ExchangeAmountVariation {
__typename
current {
...ExchangeAmount
}
after {
...ExchangeAmount
}
}
- React: The
Repaycan be as follows.Request - GraphQL: The
Previewcan be as follows.Request
const request: RepayRequest = {
sender: evmAddress("0x789…"), // User's address
reserve: borrowPosition.reserve.id,
amount: {
erc20: {
value: {
exact: bigDecimal(250), // 250 USDC
},
},
},
};
const request: RepayRequest = {
sender: evmAddress("0x789…"), // User's address
reserve: borrowPosition.reserve.id,
amount: {
erc20: {
value: {
max: true, // Repay the full debt amount
},
},
},
};
{
"request": {
"action": {
"repay": {
"sender": "0x789…",
"reserve": "SGVsbG8h",
"amount": {
"erc20": {
"value": {
"exact": "250"
}
}
}
}
}
}
}
{
"request": {
"action": {
"repay": {
"sender": "0x789…",
"reserve": "SGVsbG8h",
"amount": {
"erc20": {
"value": {
"max": true
}
}
}
}
}
}
}
The Preview shows the impact of the repay operation by comparing current and after states, with the table below outlining key fields and how to interpret them.
| Field | Impact |
|---|---|
health | Higher is better ( null if not applicable) |
risk | Lower is better |
net | Higher is better |
net | Higher is better |
net | Updated balance |
max | Maximum borrowing power |
remaining | Remaining borrowing power |
You can also specify a different currency to return fiat amounts in.
import { Currency } from "@aave/react";
const { data, error, loading } = usePreview({
action: {
repay: request,
},
currency: Currency.Eur,
});
import { Currency } from "@aave/react";
const [preview, { called, data, error, loading }] = usePreviewAction({
currency: Currency.Eur,
});
import { Currency } from "@aave/client";
const result = await preview(
client,
{
action: {
repay: {
/* … */
},
},
},
{
currency: Currency.Eur,
},
);
{
"request": {
"action": {
"repay": {
// …
}
}
},
"currency": "EUR"
}
Preview Repay with Solidity
Start with the spoke address and on-chain reserve ID identified:
address spokeAddress = 0x123…;
uint256 reserveId = 42;
And an amount to repay:
uint256 amountToRepay = 42e6; // 42 USDC
Follow the steps below to preview the impact of a repay operation on the user’s position.
1. Get Current Account Data
First, retrieve the user’s current account data from the Spoke contract.
import { ISpoke } from "aave-v4/src/spoke/interfaces/ISpoke.sol";
ISpoke spoke = ISpoke(spokeAddress);
ISpoke.UserAccountData memory currentData = spoke.getUserAccountData(user);
2. Calculate Repay Value
Then, retrieve the reserve data and asset price from the oracle to calculate the USD value of the repayment amount.
address oracleAddress = spoke.ORACLE();
ISpoke.Reserve memory reserve = spoke.getReserve(reserveId);
uint256 assetPrice = IAaveOracle(oracleAddress).getReservePrice(reserveId);
uint256 repayValue = amountToRepay * assetPrice * 10 ** (18 - reserve.decimals);
3. Compute New Health Factor
Finally, calculate the new health factor by reducing the total debt value.
uint256 totalDebtValue = currentData.totalDebtValueRay / 1e27;
if (repayValue >= totalDebtValue) return type(uint256).max;
uint256 newHealthFactor = (currentData.totalCollateralValue * currentData.avgCollateralFactor) / (totalDebtValue - repayValue); // WAD
Complete Preview Example
Here’s a complete Solidity example that calculates the health factor before a repay operation:
import { ISpoke } from "aave-v4/src/spoke/interfaces/ISpoke.sol";
import { IAaveOracle } from "aave-v4/src/spoke/interfaces/IAaveOracle.sol";
function previewRepayHealthFactor(
address spokeAddress,
uint256 reserveId,
uint256 amountToRepay,
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 repay value
ISpoke.Reserve memory reserve = spoke.getReserve(reserveId);
uint256 assetPrice = IAaveOracle(spoke.ORACLE()).getReservePrice(reserveId);
uint256 repayValue = amountToRepay * assetPrice * 10 ** (18 - reserve.decimals);
// Step 3: Compute new health factor
uint256 totalDebtValue = currentData.totalDebtValueRay / 1e27;
if (repayValue >= totalDebtValue) return type(uint256).max;
return (currentData.totalCollateralValue * currentData.avgCollateralFactor) / (totalDebtValue - repayValue); // WAD
}
Step-by-Step with AaveKit
Repay the selected borrow position.
Repaying ERC-20 tokens requires token approval. You can choose between two approaches:
- Transaction-based approval: Sends a separate ERC-20
approve()transaction before the repay transaction (2 transactions total). - Permit-based approval: Signs an EIP-2612 permit to approve and repay in a single transaction. More gas-efficient, but requires the token to support permits.
To repay a loan with AaveKit, follow these steps.
1. Configure Wallet Integration
In React, first instantiate the hooks for the wallet library of your choice:
use: used to send ERC-20 approval and repay transactionsSend Transaction use: used to sign ERC-20 permits when availableSign Typed Data
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 Repay Flow
Then, in React, use the use hook to prepare the repay operation.
import { useRepay } from "@aave/react";
const [repay, { loading, error }] = useRepay((plan) => {
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);
}
});
import { useRepay } from "@aave/react";
const [status, setStatus] = useState("");
const [repay, { loading, error }] = useRepay((plan) => {
switch (plan.__typename) {
case "TransactionRequest":
setStatus("Please sign the Repay transaction in your wallet");
return sendTransaction(plan).andTee(() => setStatus("Repaying…"));
case "Erc20Approval":
if (plan.bySignature) {
setStatus("Please sign the Permit approval in your wallet");
return signTypedData(plan.bySignature);
}
setStatus("Please sign the Approval transaction in your wallet");
return sendTransaction(plan.byTransaction).andTee(() =>
setStatus("Approving…"),
);
case "PreContractActionRequired":
setStatus("Please sign the Pre-Contract Action in your wallet");
return sendTransaction(plan.transaction).andTee(() =>
setStatus("Processing…"),
);
}
});
import { useRepay } from "@aave/react";
const [repay, { loading, error }] = useRepay((plan, { cancel }) => {
if (window.confirm("Are you sure you want to continue?") === false) {
return cancel("User cancelled the operation");
}
switch (plan.__typename) {
case "TransactionRequest":
return sendTransaction(plan);
case "Erc20Approval":
return sendTransaction(plan.byTransaction);
case "PreContractActionRequired":
return sendTransaction(plan.transaction);
}
});
In the Erc20 case, the by 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.
by is null for these.
3. Execute the Repay Operation
Then, execute the desired repay operation.
- TypeScript: Use the
repayaction to prepare the repay execution plan. - GraphQL: Use the
repayquery to prepare the repay execution plan.
import { bigDecimal, evmAddress } from "@aave/react";
const execute = async () => {
const result = await repay({
sender: evmAddress(wallet.account.address), // User's address
reserve: borrowPosition.reserve.id,
amount: {
erc20: {
value: {
exact: bigDecimal(250), // Repay 250 USDC
},
},
},
});
// …
};
import { evmAddress } from "@aave/react";
const execute = async () => {
const result = await repay({
sender: evmAddress(wallet.account.address), // User's address
reserve: borrowPosition.reserve.id,
amount: {
erc20: {
value: {
max: true, // Repay the full debt amount
},
},
},
});
// …
};
import { repay } from "@aave/client/actions";
import { bigDecimal, evmAddress } from "@aave/client";
import { client } from "./client";
const result = await repay(client, {
sender: evmAddress(wallet.account.address), // User's address
reserve: borrowPosition.reserve.id,
amount: {
erc20: {
value: {
exact: bigDecimal(250), // Repay 250 USDC
},
},
},
});
// …
import { AaveClient } from "@aave/client";
export const client = AaveClient.create();
query ($request: RepayRequest!) {
repay(request: $request) {
__typename
... on TransactionRequest {
...TransactionRequest
}
... on Erc20ApprovalRequired {
reason
requiredAmount {
...DecimalNumber
}
currentAllowance {
...DecimalNumber
}
approvals {
byTransaction {
...TransactionRequest
}
bySignature {
...PermitTypedData
}
}
originalTransaction {
...TransactionRequest
}
}
... on PreContractActionRequired {
reason
transaction {
...TransactionRequest
}
originalTransaction {
...TransactionRequest
}
}
... on InsufficientBalanceError {
required {
...DecimalNumber
}
available {
...DecimalNumber
}
}
}
}
fragment TransactionRequest on TransactionRequest {
to
from
data
value
chainId
operations
}
fragment PermitTypedData on PermitTypedData {
types
primaryType
domain {
name
version
chainId
verifyingContract
}
message
signedAmount
}
In GraphQL, below are some examples of Repay objects.
{
"request": {
"sender": "0x742d35cc6e5c4ce3b69a2a8c7c8e5f7e9a0b1234",
"reserve": "SGVsbG8h",
"amount": {
"erc20": {
"value": {
"exact": "250"
}
}
}
}
}
{
"request": {
"sender": "0x742d35cc6e5c4ce3b69a2a8c7c8e5f7e9a0b1234",
"reserve": "SGVsbG8h",
"amount": {
"erc20": {
"value": {
"max": true
}
}
}
}
}
{
"request": {
"sender": "0x742d35cc6e5c4ce3b69a2a8c7c8e5f7e9a0b1234",
"reserve": "SGVsbG8h",
"amount": {
"erc20": {
"value": {
"exact": "250"
},
"permit": {
"permitSig": {
"deadline": 1700000000, // use approvals[0].bySignature.message.deadline
"value": "0xabcdef…"
},
"signedAmount": "250.000000" // use approvals[0].bySignature.signedAmount
}
}
}
}
}
In TypeScript, to handle ERC-20 permit-based approval, first get the execution plan, sign the permit typed data when available, then call repay again with both the signature and signed amount. Unlike supply, repay also needs the signed from the permit typed data, so permit cannot be used here.
import { repay } from "@aave/client/actions";
import { signTypedDataWith } from "@aave/client/viem";
import { bigDecimal, evmAddress, okAsync } from "@aave/client";
import { client } from "./client";
import { wallet } from "./wallet";
const result = await repay(client, {
sender: evmAddress(wallet.account.address),
reserve: borrowPosition.reserve.id,
amount: {
erc20: {
value: {
exact: bigDecimal(250), // Repay 250 USDC
},
},
},
})
.andThen((plan) => {
if (
plan.__typename !== "Erc20ApprovalRequired" ||
plan.approvals.length !== 1 ||
!plan.approvals[0].bySignature
) {
return okAsync(plan);
}
const permitTypedData = plan.approvals[0].bySignature;
return signTypedDataWith(wallet, permitTypedData).andThen((signature) =>
repay(client, {
sender: evmAddress(wallet.account.address),
reserve: borrowPosition.reserve.id,
amount: {
erc20: {
value: {
exact: bigDecimal(250),
},
permit: {
permitSig: {
deadline: permitTypedData.message.deadline as number,
value: signature,
},
signedAmount: permitTypedData.signedAmount,
},
},
},
}),
);
});
// …
import { createWalletClient, http, type Hex } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { mainnet } from "viem/chains";
export const wallet = createWalletClient({
account: privateKeyToAccount(process.env.PRIVATE_KEY as Hex),
chain: mainnet,
transport: http(),
});
The permit-based repay flow:
- Call
repaywithout a permit - If the response is
Erc20with a singleApproval Required approvals[0]., sign itby Signature - Call
repayagain withamount.anderc20. permit. permit Sig amount.erc20. permit. signed Amount - Otherwise, keep the original execution plan (no permit needed or token doesn’t support permits)
For tokens that need an allowance reset, like USDT on Ethereum Mainnet, permits aren’t available and the execution plan uses transaction-based approvals.
4. Process the Execution Plan
In TypeScript, use send for the wallet library of your choice to send the transactions in the execution plan, and await indexing with client..
import { repay } from "@aave/client/actions";
import { sendWith } from "@aave/client/viem";
import { client } from "./client";
import { wallet } from "./wallet";
const result = await repay(client, {
// …
})
.andThen(sendWith(wallet))
.andThen(client.waitForTransaction);
import { repay } from "@aave/client/actions";
import { sendWith, signTypedDataWith } from "@aave/client/viem";
import { bigDecimal, evmAddress } from "@aave/client";
import { client } from "./client";
import { wallet } from "./wallet";
const result = await repay(client, {
sender: evmAddress(wallet.account.address),
reserve: borrowPosition.reserve.id,
amount: {
erc20: {
value: { exact: bigDecimal(250) },
},
},
})
.andThen((plan) => {
if (
plan.__typename !== "Erc20ApprovalRequired" ||
plan.approvals.length !== 1 ||
!plan.approvals[0].bySignature
) {
return sendWith(wallet, plan);
}
const permitTypedData = plan.approvals[0].bySignature;
return signTypedDataWith(wallet, permitTypedData)
.andThen((signature) =>
repay(client, {
sender: evmAddress(wallet.account.address),
reserve: borrowPosition.reserve.id,
amount: {
erc20: {
value: { exact: bigDecimal(250) },
permit: {
permitSig: {
deadline: permitTypedData.message.deadline as number,
value: signature,
},
signedAmount: permitTypedData.signedAmount,
},
},
},
}),
)
.andThen(sendWith(wallet));
})
.andThen(client.waitForTransaction);
send handles all required approvals, including the allowance reset for
tokens like USDT.
In GraphQL, if the response is a Transaction, you can send it directly using the wallet associated with the from address.
{
"data": {
"repay": {
"__typename": "TransactionRequest",
"to": "0x94e7a5dcbe816e498b89ab752661904e2f56c485",
"from": "0x742d35cc6e5c4ce3b69a2a8c7c8e5f7e9a0b1234",
"data": "0xb1e8f8ef…",
"value": "0",
"chainId": 1,
"operations": ["SPOKE_REPAY"]
}
}
}
If the response is Erc20, you have two options:
Transaction-based approval
- Send each transaction in the
approvalsarray in sequence - Then send
originalTransaction
Permit-based approval
This is available only when approvals contains a single Erc20
item.
- Sign the
approvals[0].typed data with the sender’s account.by Signature - Call the
repayquery again with both the permit signature andsigned(see theAmount Permit-based Approvalexample above) - The response will be a
Transactionyou can then send directlyRequest
{
"data": {
"repay": {
"__typename": "Erc20ApprovalRequired",
"reason": "Insufficient allowance for ERC-20 token transfer",
"requiredAmount": {
"value": "250.000000",
"decimals": 6
},
"currentAllowance": {
"value": "0.000000",
"decimals": 6
},
"approvals": [
{
"byTransaction": {
"to": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"from": "0x742d35cc6e5c4ce3b69a2a8c7c8e5f7e9a0b1234",
"data": "0x095ea7b3000000000000000000000000878…",
"value": "0",
"chainId": 1,
"operations": null
},
"bySignature": {
"types": { /* … */ },
"primaryType": "Permit",
"domain": { /* … */ },
"message": { /* … */ },
"signedAmount": "250.000000"
}
}
],
"originalTransaction": {
"to": "0x94e7a5dcbe816e498b89ab752661904e2f56c485",
"from": "0x742d35cc6e5c4ce3b69a2a8c7c8e5f7e9a0b1234",
"data": "0xb1e8f8ef…",
"value": "0",
"chainId": 1,
"operations": ["SPOKE_REPAY"]
}
}
}
}
{
"data": {
"repay": {
"__typename": "Erc20ApprovalRequired",
"reason": "Token requires allowance reset before new approval",
"requiredAmount": {
"value": "250.000000",
"decimals": 6
},
"currentAllowance": {
"value": "100.000000",
"decimals": 6
},
"approvals": [
{
"byTransaction": {
"to": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
"from": "0x742d35cc6e5c4ce3b69a2a8c7c8e5f7e9a0b1234",
"data": "0x095ea7b3000000000000000000000000878…0",
"value": "0",
"chainId": 1,
"operations": null
},
"bySignature": null
},
{
"byTransaction": {
"to": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
"from": "0x742d35cc6e5c4ce3b69a2a8c7c8e5f7e9a0b1234",
"data": "0x095ea7b3000000000000000000000000878…250",
"value": "0",
"chainId": 1,
"operations": null
},
"bySignature": null
}
],
"originalTransaction": {
"to": "0x94e7a5dcbe816e498b89ab752661904e2f56c485",
"from": "0x742d35cc6e5c4ce3b69a2a8c7c8e5f7e9a0b1234",
"data": "0xb1e8f8ef…",
"value": "0",
"chainId": 1,
"operations": ["SPOKE_REPAY"]
}
}
}
}
With multiple approvals in Erc20, send each by
in order, then original. For USDT on Ethereum
Mainnet,
the first resets the allowance to 0 and the second sets the required amount.
If the response is Pre, you must first send the transaction request, then send the original.
{
"data": {
"repay": {
"__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": "0xb1e8f8ef…",
"value": "0",
"chainId": 1,
"operations": ["SPOKE_REPAY"]
}
}
}
}
If the response is an Insufficient, inform the user that they don’t have enough tokens to repay the requested amount.
{
"data": {
"repay": {
"__typename": "InsufficientBalanceError",
"required": {
"value": "250.000000",
"decimals": 6
},
"available": {
"value": "150.500000",
"decimals": 6
}
}
}
}
5. Handle the Result
Finally, in React and TypeScript, handle the result.
const execute = async () => {
const result = await repay(/* … */);
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("Repay successful with hash:", result.value.txHash);
};
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;
}
} else {
console.log("Repay successful with hash:", result.value.txHash);
}
Step-by-Step with Solidity
Use the same identifiers and amount to repay used to preview the repay operation:
address spokeAddress = 0x123…;
uint256 reserveId = 42;
uint256 amountToRepay = 42e6; // 42 USDC
Complete the repay process with the following steps:
1. Import the Spoke Interface
First, create an instance of the ISpoke interface.
import { ISpoke } from "aave-v4/src/spoke/interfaces/ISpoke.sol";
ISpoke spoke = ISpoke(spokeAddress);
2. Approve Token Transfer
Then, approve the Spoke to transfer the specified amount of ERC-20 tokens.
import { IERC20 } from "aave-v4/src/dependencies/openzeppelin/IERC20.sol";
IERC20 token = IERC20(spoke.getReserve(reserveId).underlying);
token.approve(spokeAddress, amountToRepay);
3. Repay Tokens
Finally, call the repay function on the Spoke contract, specifying the reserve ID, amount, and the user address.
spoke.repay(
reserveId,
amountToRepay,
msg.sender
);
spoke.repay(
reserveId,
type(uint256).max, // Repay the full debt amount
msg.sender
);
Complete Repay Example
Here’s a complete Solidity example that executes the full repay flow:
import { ISpoke } from "aave-v4/src/spoke/interfaces/ISpoke.sol";
import { IERC20 } from "aave-v4/src/dependencies/openzeppelin/IERC20.sol";
function repayDebt(
address spokeAddress,
uint256 reserveId,
uint256 amountToRepay,
address onBehalfOf
) external {
ISpoke spoke = ISpoke(spokeAddress);
// Step 1: Pull tokens from the caller and approve the Spoke
IERC20 token = IERC20(spoke.getReserve(reserveId).underlying);
token.transferFrom(msg.sender, address(this), amountToRepay);
token.approve(spokeAddress, amountToRepay);
// Step 2: Repay debt
spoke.repay(reserveId, amountToRepay, onBehalfOf);
}
The contract must be an active Position Manager approved by on, otherwise repay reverts.
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 Preview you pass to the use hook.
For example:
import { type PreviewAction } from "@aave/react";
const action: PreviewAction = {
repay: {
sender: evmAddress("0x123…"), // User's address
reserve: borrowPosition.reserve.id,
amount: {
erc20: {
value: { exact: bigDecimal(42) }, // 42 USDC
},
},
},
};
Use the use hook to estimate both the network fee for the provided action and its fiat equivalent.
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 repay the debt using the chain’s native token with the Native Token Gateway.
Use the reserve. flag to determine if the underlying token is a wrapped native token. The Native Token Gateway address is available from the chain details.
const borrowPosition: UserBorrowItem = {
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…",
},
// …
},
// …
};
Specify the amount in the amount field as a native value.
const execute = async () => {
const result = await repay({
sender: evmAddress(wallet.account.address), // User's address
reserve: borrowPosition.reserve.id,
amount: {
native: {
exact: bigDecimal(1), // Repay 1 ETH
},
},
});
// …
};
const execute = async () => {
const result = await repay({
sender: evmAddress(wallet.account.address), // User's address
reserve: borrowPosition.reserve.id,
amount: {
native: {
max: true, // Repay the full debt amount
},
},
});
// …
};
const result = await repay(client, {
sender: evmAddress(wallet.account.address), // User's address
reserve: borrowPosition.reserve.id,
amount: {
native: {
exact: bigDecimal(1), // Repay 1 ETH
},
},
});
// …
const result = await repay(client, {
sender: evmAddress(wallet.account.address), // User's address
reserve: borrowPosition.reserve.id,
amount: {
native: {
max: true, // Repay the full debt amount
},
},
});
// …
{
"request": {
"sender": "0x742d35cc6e5c4ce3b69a2a8c7c8e5f7e9a0b1234",
"reserve": "SGVsbG8h",
"amount": {
"native": {
"exact": "1"
}
}
}
}
{
"request": {
"sender": "0x742d35cc6e5c4ce3b69a2a8c7c8e5f7e9a0b1234",
"reserve": "SGVsbG8h",
"amount": {
"native": {
"max": true
}
}
}
}
Native Tokens with Solidity
Start with the spoke address, reserve ID, and native gateway address identified:
address spokeAddress = 0x123…;
uint256 reserveId = 42;
address nativeGateway = 0xabc…;
Follow the steps below to repay debt with native tokens (e.g., ETH) for 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.
import { ISpoke } from "aave-v4/src/spoke/interfaces/ISpoke.sol";
ISpoke spoke = ISpoke(spokeAddress);
spoke.setUserPositionManager(nativeGateway, true);
2. Repay Native Tokens
Then, call repay on the gateway, sending the native token amount via msg..
import { INativeTokenGateway } from "aave-v4/src/position-manager/interfaces/INativeTokenGateway.sol";
INativeTokenGateway gateway = INativeTokenGateway(nativeGateway);
gateway.repayNative{value: amountToRepay}(spokeAddress, reserveId, amountToRepay);
import { INativeTokenGateway } from "aave-v4/src/position-manager/interfaces/INativeTokenGateway.sol";
INativeTokenGateway gateway = INativeTokenGateway(nativeGateway);
uint256 repayAmount = spoke.getUserTotalDebt(reserveId, msg.sender) * 10_001 / 10_000; // buffer for accrued interest
gateway.repayNative{value: repayAmount}(spokeAddress, reserveId, repayAmount);
The gateway wraps the native token (ETH → WETH), approves the Spoke, and calls spoke. on your behalf. Any amount above the current debt is refunded to you.
Complete Native Token Example
Here’s a complete Solidity example that executes the native token repay flow. This helper acts on the contract’s own position. To repay for a user’s wallet, the user calls set and the gateway directly.
import { ISpoke } from "aave-v4/src/spoke/interfaces/ISpoke.sol";
import { INativeTokenGateway } from "aave-v4/src/position-manager/interfaces/INativeTokenGateway.sol";
function repayNativeToken(
address spokeAddress,
address nativeGateway,
uint256 reserveId,
uint256 amountToRepay
) external payable {
require(msg.value == amountToRepay, "Incorrect ETH amount");
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: Repay native tokens via gateway
INativeTokenGateway(nativeGateway).repayNative{value: amountToRepay}(spokeAddress, reserveId, amountToRepay);
}