Borrow из Aave V3
Как занять USDC в Aave V3 под WETH collateral, вызвать Pool.borrow и проверить health factor в ethers v6.
Borrow работает только под collateral
В Aave V3 пользователь занимает один asset под залог другого asset.
Контрактный flow такой: внести collateral через supply, включить его как collateral, проверить borrowing power и вызвать borrow. Для обычного займа в Aave V3 используют variable debt: interestRateMode = 2.
Занимаем USDC под WETH
Пример запускается на локальном fork Ethereum mainnet. Он использует настоящие контракты Aave V3, WETH и USDC, но не отправляет транзакции в реальную сеть.
Создайте проект:
mkdir aave-v3-borrow-example
cd aave-v3-borrow-example
npm init -y
npm pkg set type="module"
npm install ethers tsx
npm install --save-dev hardhat typescript @types/node
mkdir srcСоздайте файл hardhat.config.js:
export default {
solidity: "0.8.20",
};В первом терминале запустите mainnet fork:
npx hardhat node --fork https://ethereum-rpc.publicnode.comСоздайте файл src/borrow-usdc.ts:
import { ethers } from "ethers";
const LOCAL_FORK_RPC_URL = "http://127.0.0.1:8545";
const HARDHAT_ACCOUNT_PRIVATE_KEY =
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
const AAVE_V3_POOL_ADDRESS = "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2";
const WETH_ADDRESS = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
const USDC_ADDRESS = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
const WETH_DECIMALS = 18;
const USDC_DECIMALS = 6;
const BASE_CURRENCY_DECIMALS = 8;
const REFERRAL_CODE = 0;
const VARIABLE_RATE_MODE = 2;
const WETH_ABI = [
"function deposit() external payable",
"function approve(address spender,uint256 amount) external returns (bool)",
"function balanceOf(address account) external view returns (uint256)",
];
const ERC20_ABI = [
"function balanceOf(address account) external view returns (uint256)",
];
const AAVE_POOL_ABI = [
"function supply(address asset,uint256 amount,address onBehalfOf,uint16 referralCode) external",
"function setUserUseReserveAsCollateral(address asset,bool useAsCollateral) external",
"function borrow(address asset,uint256 amount,uint256 interestRateMode,uint16 referralCode,address onBehalfOf) external",
"function getUserAccountData(address user) external view returns (uint256 totalCollateralBase,uint256 totalDebtBase,uint256 availableBorrowsBase,uint256 currentLiquidationThreshold,uint256 ltv,uint256 healthFactor)",
];
function formatBaseCurrency(value: bigint) {
return ethers.formatUnits(value, BASE_CURRENCY_DECIMALS);
}
function formatHealthFactor(value: bigint) {
if (value === ethers.MaxUint256) {
return "infinite (no debt)";
}
return ethers.formatUnits(value, 18);
}
async function printAccountData(
pool: ethers.Contract,
user: string,
label: string,
) {
const [
totalCollateralBase,
totalDebtBase,
availableBorrowsBase,
currentLiquidationThreshold,
ltv,
healthFactor,
] = await pool.getUserAccountData(user);
console.log(`\n${label}`);
console.log(`Collateral: ${formatBaseCurrency(totalCollateralBase)} base`);
console.log(`Debt: ${formatBaseCurrency(totalDebtBase)} base`);
console.log(`Available borrows: ${formatBaseCurrency(availableBorrowsBase)} base`);
console.log(`LTV: ${ltv.toString()} bps`);
console.log(`Liquidation threshold: ${currentLiquidationThreshold.toString()} bps`);
console.log(`Health factor: ${formatHealthFactor(healthFactor)}`);
}
const provider = new ethers.JsonRpcProvider(LOCAL_FORK_RPC_URL);
const wallet = new ethers.Wallet(HARDHAT_ACCOUNT_PRIVATE_KEY, provider);
const weth = new ethers.Contract(WETH_ADDRESS, WETH_ABI, wallet);
const usdc = new ethers.Contract(USDC_ADDRESS, ERC20_ABI, provider);
const pool = new ethers.Contract(AAVE_V3_POOL_ADDRESS, AAVE_POOL_ABI, wallet);
const collateralAmount = ethers.parseUnits("2", WETH_DECIMALS);
const borrowAmount = ethers.parseUnits("1000", USDC_DECIMALS);
console.log(`Borrower: ${wallet.address}`);
const wrapTx = await weth.deposit({ value: collateralAmount });
await wrapTx.wait();
await (await weth.approve(AAVE_V3_POOL_ADDRESS, collateralAmount)).wait();
await (
await pool.supply(
WETH_ADDRESS,
collateralAmount,
wallet.address,
REFERRAL_CODE,
)
).wait();
await (
await pool.setUserUseReserveAsCollateral(WETH_ADDRESS, true)
).wait();
await printAccountData(pool, wallet.address, "Before borrow");
const usdcBefore = await usdc.balanceOf(wallet.address);
const borrowTx = await pool.borrow(
USDC_ADDRESS,
borrowAmount,
VARIABLE_RATE_MODE,
REFERRAL_CODE,
wallet.address,
);
const receipt = await borrowTx.wait();
const usdcAfter = await usdc.balanceOf(wallet.address);
const received = usdcAfter - usdcBefore;
console.log(`\nBorrow tx: ${receipt?.hash}`);
console.log(`USDC received: ${ethers.formatUnits(received, USDC_DECIMALS)}`);
await printAccountData(pool, wallet.address, "After borrow");Во втором терминале запустите borrow:
npx tsx src/borrow-usdc.tsСкрипт выведет примерно такой результат:
Borrower: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Before borrow
Collateral: 7084.36 base
Debt: 0.0 base
Available borrows: 5667.48 base
LTV: 8000 bps
Liquidation threshold: 8250 bps
Health factor: infinite (no debt)
Borrow tx: 0x...
USDC received: 1000.0
After borrow
Collateral: 7084.36 base
Debt: 1000.0 base
Available borrows: 4667.48 base
LTV: 8000 bps
Liquidation threshold: 8250 bps
Health factor: 5.844597Числа зависят от цен Aave oracle на момент fork.
Как это работает
pool.supply(WETH_ADDRESS, collateralAmount, wallet.address, 0) — вносит WETH в Aave V3 и создаёт collateral-позицию.
setUserUseReserveAsCollateral(WETH_ADDRESS, true) — явно включает WETH как collateral. Без collateral borrow невозможен.
getUserAccountData() — возвращает общую картину аккаунта: collateral, debt, доступный borrow, LTV и health factor.
VARIABLE_RATE_MODE = 2 — variable debt. В Aave V3 это стандартный режим для borrow.
pool.borrow(USDC_ADDRESS, borrowAmount, 2, 0, wallet.address) — занимает USDC и записывает долг на onBehalfOf.
USDC balance — показывает полученный asset. Долг хранится не в USDC балансе, а во внутреннем debt accounting Aave.
Health factor
Health factor показывает, насколько позиция далека от ликвидации.
Если healthFactor > 1, позиция считается безопасной. Если он падает ниже 1, её могут ликвидировать. На health factor влияют цена collateral, размер долга, liquidation threshold и начисленные проценты.
В примере health factor высокий, потому что мы внесли 2 WETH и заняли только 1000 USDC.
Частые ошибки
Занимать без collateral → borrow откатится, если у пользователя нет достаточного collateral или он не включён.
Путать LTV и liquidation threshold → LTV ограничивает максимальный borrow, liquidation threshold определяет момент ликвидации.
Игнорировать health factor после транзакции → цена collateral может упасть, а долг со временем растёт из-за процентов.
Использовать interestRateMode = 1 → stable borrow в Aave V3 для многих рынков недоступен или отключён. Для обычного borrow используйте 2.
Что дальше
- Deposit в Aave V3 — внести collateral и получить aToken
- Oracle manipulation — понять, почему lending-протоколы зависят от цен оракула
- ERC-20: approve и allowance — разобраться, почему supply требует allowance