В этой статье мы подробно разберем, как работают основные функции протокола Aave V2, и узнаем, как их использовать заемщикам, ликвидаторам и депозитариям.
1. Введение
Aave — это децентрализованный протокол кредитования с открытым исходным кодом, не требующий посредников, банковских счетов или кредитных рейтингов. Изначально запущенный в 2017 году как ETHLend и переименованный в Aave в 2020 году, протокол стал лидером рынка DeFi. По состоянию на 2026 год, общая заблокированная стоимость (TVL) превышает $40 млрд, обеспечивая глубокую ликвидность в Ethereum, Polygon, Arbitrum, Optimism, Avalanche, Base и других сетях L2.
Ключевые особенности экосистемы:
aTokens: Процентные токены (aUSDC, aETH), представляющие долю в пуле. Баланс растёт автоматически каждую секунду («rebasing»). Можно использовать как залог в других протоколах.
Гибкие процентные ставки: Заемщики выбирают между стабильной (фиксированная) и переменной (зависит от утилизации) ставкой.
Флеш-кредиты: Кредиты без залога при условии возврата средств в рамках одной транзакции. Используются для арбитража и рефинансирования.
Безопасность и управление: Управление через DAO (держатели AAVE), Safety Module как страховой фонд.
Инновации V3: E-Mode (LTV до 97%), Isolation Mode, Portals для cross-chain ликвидности.
Широкая поддержка активов: 50+ токенов с индивидуально настроенными параметрами риска (LTV, Liquidation Threshold, Reserve Factor).
Настройка окружения для тестов
Мой репозиторий содержит следующие изменения относительно оригинального:
Добавлены интерфейсы с обновленными версиями pragma.
Добавлен mock-контракт.
Добавлен контракт для теста FlashLoan.
Добавлены библиотеки: forge-std и openzeppelin-contracts.
Добавлены тесты на Solidity.
Пошаговая инструкция для воспроизведения наших тестов
# 1. Установка Foundry curl -L https://foundry.paradigm.xyz | bash foundryup # 2. Клонирование репозитория git clone https://github.com/danilastupin/parsing-aave-v2 cd protocol-v2 forge init --force # 3. Настройка RPC и блока # Зарегистрируйтесь на https://www.ankr.com/rpc/eth/ и скопируйте endpoint # На Etherscan найдите номер блока любой выполненной транзакции контракта Aave: # https://etherscan.io/address/0x7d2768de32b0b80b7a3454c06bdac94a69ddc7a9 # 4. Настройка .env файла MAINNET_RPC_URL=ваш_endpoint MAINNET_BLOCK_NUMBER=номер_блока
Полезные ссылки
Важно: Функции на Etherscan могут отличаться от функций в GitHub. Etherscan показывает прокси и конкретную задеплоенную версию логики «здесь и сейчас». GitHub содержит полную историю, исходный код всех версий и будущие обновления. Для глубокого анализа уязвимостей опирайтесь на код из репозитория, сверяя его с активной реализацией через механизм Proxy.
Запуск тестов
cd test # Запустить все тесты forge test -vv # Запустить конкретный тест forge test lendpool.t.sol --match-test testDeposit -vv # Для более подробного вывода (рекомендуется): forge test -vvv # или -vvvv / -vvvvv
2. Функции для вкладчиков
Функция deposit
function deposit( address asset, uint256 amount, address onBehalfOf, uint16 referralCode ) external;
Параметр | Описание |
asset | Адрес токена для депозита |
amount | Количество токенов |
onBehalfOf | Адрес получателя aToken (может быть ваш или любой другой) |
referralCode | Код для отслеживания партнёра/интегратора |

Запуск теста testDeposit
forge test --match-test testDeposit -vv
Итак, тест сразу падает с ошибкой
Revert 3
Для поиска решения ошибок нам нужно открыть контракт в IDE и найти описание ошибки в src/contracts/protocol/libraries/helpers/Errors.sol
string public constant VL_RESERVE_FROZEN = '3'; // 'Action cannot be performed because the reserve is frozen'
Что же это означает?
Это значит, что резерв для USDT заморожен и не может быть сделан депозит этим токеном, но как так, это же один из самых популярных стейблкоинов в Defi. А дело тут в том, что резерв USDT в Aave V2 на Ethereum Mainnet по-прежнему заморожен (на февраль 2026 г.). Это не временная мера, а долгосрочное решение после серьёзного инцидента. Почему так произошло?
Инцидент в марте 2023 года:
В Aave V2 на Ethereum произошёл эксплойт через flashloan, связанный с USDT и DAI, в результате которого было украдено ~$30 млн. Хотя протокол вернул средства за счёт страхового фонда, команда Aave приняла решение: заморозить резервы USDT и DAI в Aave V2 на Ethereum как мера предосторожности. USDT остался замороженным в отличие от DAI (который позже разморозили), USDT так и не был разморожен в Aave V2 на Ethereum. Причина — особенности контракта USDT (отсутствие return в transfer, централизованная пауза и т.д.), что делает его менее безопасным для DeFi-протоколов. Команда Aave перевела фокус на V3, где USDT доступен и не заморожен.
Но V2 на Ethereum остаётся "в режиме поддержки", и USDT в нём намеренно заблокирован. Но нам нужно как-то тестировать наши функции, и поэтому мы можем использовать другой стейблкоин, либо взять более ранний блок транзакции, в .env замените
MAINNET_BLOCK_NUMBER=16800000
Важно: Этот пример показывает нам, что в дальнейшем тестировании при возникновении ошибок мы можем понять, в чем дело и почему падают наши тесты.
Запускаем наш тест еще раз
Результат выполнения:
Before deposit: balance aUSDT: 0 balance user USDT: 50,000,000 After first deposit: balance aUSDT: 25,000,000 balance user USDT: 25,000,000 After second deposit: balance aUSDT: 50,000,000 balance user USDT: 0
Что делает deposit внутри?
Валидация через validateDeposit:
Проверяются базовые условия перед депозитом: вызывается функция getFlags, которая возвращает 4 булевых флага, в нашем случае нам нужны 2. isActive - активен ли резерв и isFrozen - заморожен ли резерв.
Проверяется, что депозит ненулевой.
Обновление состояния резерва с помощью updateState:
Получает scaledVariableDebt - определяет базу для расчета процентов.
Сохраняет "снимок" состояния - гарантирует корректность расчетов.
Вычисляет новые индексы - актуализирует стоимость денег во времени.
Начисляет проценты в казну - формирует резервный фонд протокола.
Обновляет lastUpdateTimestamp - фиксирует момент последнего расчета.
Пересчёт ставок, вызывая функцию updateInterestRates:
Получает стабильный долг и среднюю ставку - учитывает «историю» фиксированных ставок.
Рассчитывает переменный долг через индекс - газ - эффективный расчёт текущего долга.
Определяет доступную ликвидность.
Корректирует ликвидность на изменения операции - ставки отражают итоговое состояние после транзакции.
Вызывает стратегию для расчёта новых ставок - динамическое ценообразование на основе спроса/предложения.
Перевод токенов с помощью функции SafeTransferFrom от msg.sender (от нас) на адрес aToken, в данном примере на aUSDT.
Проверка разрешения использования как залога, если это первый депозит.
Функция withdraw
function withdraw( address asset, uint256 amount, address to ) external returns (uint256);
Параметр | Описание |
asset | Адрес токена для вывода |
amount | Количество |
to | Адрес получателя |
Запускаем testWithdraw
Balances before withdraw: balance usdt 49999950 balance aUSDT 50 Balances after withdraw: balance usdt 50000000 balance aUSDT 0
Получение резерва токена aUSDT, получение баланса user`a aUSDT.
Проверка, допустим ли вывод , вызывая функцию validateWithdraw:
Проверяет, что сумма вывода ненулевая.
Проверяет обеспеченность баланса пользователя.
Проверяет активности резерва.
Проверяет разрешения уменьшения баланса с помощью функции balanceDecreaseAllowed - проверяет, безопасно ли уменьшить баланс залога на указанную сумму, не приведя позицию пользователя к ликвидации (healhFactor <1.0).
Обновляет состояние резерва.
Вызывает updateInterestRates.
Проверяет, если запрашиваемое кол-во на вывод средств равно максимальному балансу aUSDT, пользователю заблокирована возможность залога.
Сжигает aUSDT токены пользователя.
Переводит USDT токены на указанный адрес.
3. Функции для заемщиков
Функция borrow
function borrow( address asset, uint256 amount, uint256 interestRateMode, uint16 referralCode, address onBehalfOf ) external;
Параметр | Описание |
asset | Адрес токена, который мы хотим занять |
amount | Количество |
interestRateMode | Процентная ставка (STABLE или VARIABLE) |
referralCode | Код для отслеживания партнёра/интегратора |
onBehalfOf | Адрес получателя заемного токена |
Получение резервов выбранного нами токена , в нашем случае это USDC.
Вызов внутренней функции _executeBorrow:
Получает цену токена USDC с помощью оракла в ETH.
Вызывает функцию validateBorrow для проверки условий заёма:
Обновляет состояние резервов.
Далее происходит минтинг долгового токена в зависимости от режима ставки:
Если STABLE - фиксированная ставка - проценты не накапливаются через индекс - ставка фиксирована, расчет происходит при погашении.
Если VARIABLE - плавающая ставка - проценты накапливаются непрерывно через рост индекса, без хранения timestamp'ов.
Пользователь занял 1000 USDC при index = 1.0
Через неделю index = 1.05 (накоплено 5% годовых)
Текущий долг = 1000 * 1.05 / 1.0 = 1050 USDC
В testBorrow мы будем использовать DAI как обеспечение, так как USDT имеет LTV = 0 %, то есть его нельзя использовать как залог!
Результат:
Balances before borrow: balance dai 0 balance aDAI 50000000000000000000 balance usdc 25000000 Balances after borrow: balance dai 0 balance aDAI 50000000000000000000 balance usdc 40000000
Функция repay
function repay( address asset, uint256 amount, uint256 rateMode, address onBehalfOf ) external returns (uint256);
Параметр | Описание |
asset | Адрес заемного базового актива, ранее заимствованного |
amount | Сумма для погашения |
rateMode | Режим процентной ставки по долгу, который пользователь хочет погасить: 1 для стабильного, 2 для переменного |
onBehalfOf | Адрес пользователя, чей долг погашается |
В testRepay мы сначала делаем депозит в DAI и получаем aDAI, затем берем заем за счет обеспечения наших aDAI токены USDC . Затем с помощью функции repay мы возвращаем заем и платим проценты. Ожидалось, что мы получим меньше aDAI, так как мы поставили таймкод 1 день - vm.warp(block.timestamp + 1 days);. Но оказалось совсем наоборот! Мы получили больше aDAI, чем было до заема! На данном этапе мы встречаем причину, почему aave - популярный defi protocol. Чтобы разобраться, мы напишем еще один тест testRepayv2 для разбора, откуда взялись дополнительные aDAI в нашей копилке.
Сценарий теста
Мы делаем депозит в 50 DAI и получаем 50 aDAI .
Затем берем заем на 25 USDC в счет залога наших 50 aDAI.
Проматываем время на день вперед.
Возвращаем заем с помощью функции repay
Результаты тестов:
Balances after deposit , before borrow: balance dai 0 balance aDAI 50000000000000000000 balance usdc 25000000 Balances after borrow: balance dai 0 balance aDAI 50000000000000000000 balance usdc 50000000 BEFORE TIME WARP RESERVE STATE (AFTER WARP): aDAI balance: 50001688841359931915 Balances after repay: balance dai 0 balance aDAI 50001688841359931915 balance usdc 24998081 profit aDAI: 1688841359931915 withheld interest usdc: 1919
Как мы видим, мы получили больше aDAI на выходе - это начисленные проценты за использование наших aDAI в протоколе.
Получили меньше USDC: Наши изначальные 25 USDC минус проценты за взятие заёма.
Как же начисляются проценты для депозиторов?
Когда мы делаем vm.warp(1 days), ничего не происходит. Проценты не начисляются автоматически. Они «накапливаются в воздухе» и применяются только при первом действии с пулом (депозит / заем / погашение).
Еще раз
Мы депонируем 50 DAI, получаем 50 aDAI.
Внутри aDAI сохраняется масштабированный баланс = 50 (без процентов).
Проходит 1 день.
Ничего не происходит. liquidityIndex остаётся 1.0.
Кто-то взаимодействует с пулом, происходит триггер начисления.
Вызывается reserve.updateState() -> _updateIndexes().
Расчёт множителя для депозита:
uint256 cumulatedLiquidityInterest = MathUtils.calculateLinearInterest(currentLiquidityRate, timestamp);
calculateLinearInterest для ставки возвращает 1.02 * RAY.
uint256 timeDifference = block.timestamp.sub(uint256(lastUpdateTimestamp)); return (rate.mul(timeDifference) / SECONDS_PER_YEAR).add(WadRayMath.ray());
Применяется множитель к индексу:
newLiquidityIndex = cumulatedLiquidityInterest.rayMul(liquidityIndex); reserve.liquidityIndex = uint128(newLiquidityIndex);
Мы видим проценты в балансе aDAI:
uint256 aDaiBalance = aDAI.balanceOf(user1);
Внутри AToken.balanceOf() происходит:
return scaledBalance.rayMul(liquidityIndex);
Итог: Мы получили + ~ 0.0005 DAI — это проценты за использование наших средств другими заемщиками. Проценты «встроены» в liquidityIndex.
Как начисляются проценты за наш заем USDC (для заемщиков):
Мы берём заем 25 USDC
Внутри VariableDebtToken сохраняется масштабированный долг = 25 (без процентов).
Проходит 1 день
Ничего не происходит. variableBorrowIndex остаётся 1.0.
Мы погашаем заем, вызывается updateState(), в нем _updateIndexes().
Расчёт множителя для займа:
uint256 cumulatedVariableBorrowInterest = MathUtils.calculateCompoundedInterest(reserve.currentVariableBorrowRate, timestamp)
calculateCompoundedInterest:
uint256 exp = block.timestamp.sub(uint256(lastUpdateTimestamp)); uint256 ratePerSecond = rate / SECONDS_PER_YEAR; uint256 secondTerm = exp.mul(expMinusOne).mul(basePowerTwo) / 2; uint256 thirdTerm = exp.mul(expMinusOne).mul(expMinusTwo).mul(basePowerThree) / 6; return RAY.add(ratePerSecond.mul(exp)).add(secondTerm).add(thirdTerm);
Для ставки возвращает ~ 1.0006 * RAY.
Применяется множитель к индексу:
newVariableBorrowIndex = cumulatedVariableBorrowInterest.rayMul(variableBorrowIndex); reserve.variableBorrowIndex = uint128(newVariableBorrowIndex);
Получение долга с процентами:
(uint256 stableDebt, uint256 variableDebt) = Helpers.getUserCurrentDebt(onBehalfOf, reserve);
Внутри getUserCurrentDebt() происходит:
uint256 scaledDebt = IVariableDebtToken(...).balanceOf(user); // = 25 return scaledDebt.rayMul(reserve.variableBorrowIndex); // 25 * 1.00006 = ~ 25.001919 USDC
Погашение полной суммы:
paybackAmount = variableDebt; // = ~ 25.001919 USDC IERC20(asset).safeTransferFrom(msg.sender, aToken, paybackAmount);
С нашего кошелька списывается ~ 25.001919 USDC, а не 25. Мы вернули 25 USDC основного долга + ~ 0.002 USDC процентов. Проценты «встроены» в variableBorrowIndex.
Куда уходят проценты?
20% процентов уходит в казну протокола.
80% процентов распределяется между ликвидаторами через рост liquidityIndex.
Функция swapBorrowRateMode
function swapBorrowRateMode( address asset, uint256 rateMode ) external;
Параметр | Описание |
asset | Адрес заимствованного базового актива |
rateMode | Режим процентной ставки по долгу, который хотим поменять |
Главная идея, долг не исчезает и не появляется заново. Он просто "перетекает" из одного токена в другой.
В Aave долг представлен токенами:
Если долг под Variable ставку - у нас на руках токены VariableDebtToken.
Если долг под Stable ставку - у нас на руках токены StableDebtToken.
Эта функция делает 2 вещи:
Сжигает (burn) наши текущие долговые токены.
Минтит (mint) новые долговые токены другого типа на ту же сумму (долг появляется в новой форме).
Теперь немного разберемся по шагам функции:
Получение данных резерва.
DataTypes.ReserveData storage reserve = _reserves[asset];
Функция обращается к хранилищу контракта (_reserves) и находит данные именно по тому активу, который мы указали (USDC). В структуре reserve хранятся адреса токенов долга(stableDebtTokenAddress, variableDebtTokenAddress), текущие индексы ставок и другая математика, необходимая для расчета.
Проверка текущего долга пользователя
(uint256 stableDebt, uint256 variableDebt) = Helpers.getUserCurrentDebt(msg.sender, reserve);
Вызывается getUserCurrentDebt функция, которая смотрит балансы долговых токенов у msg.sender. Мы получаем две цифры. Если у нас Variable долг, то stableDebt будет 0, а variableDebt будет равен сумме займа (у нас 25 USDC). Если у нас Stable долг, то наоборот. Нужно это для того, чтобы знать, сколько именно нужно сжечь и сколько заминтить.
Валидация.
Проверяет активен ли резерв.
Проверяет на какую ставку переход.
Проверяет доступность данной ставки.
Защита от злоупотребления:
require(!userConfig.isUsingAsCollateral(reserve.id) || reserve.configuration.getLtv() == 0 || stableDebt.add(variableDebt) > IERC20(reserve.aTokenAddress).balanceOf(msg.sender), Errors.VL_COLLATERAL_SAME_AS_BORROWING_CURRENCY);
Эта проверка защищает протокол от хитрой схемы: "Заложить $100, занять $100 под переменную ставку, а потом сразу переключиться на стабильную, чтобы зафиксировать низкую ставку, имея 100% обеспечение". Протокол запрещает переход на Stable, если ваш долг меньше или равен залогу (в одном и том же активе).
Обновление состояния резерва.
Перед тем как поменять долг, протокол должен начислить все накопленные проценты до текущего времени. Сделано это потому что индексы ставок (variableBorrowIndex) растут со временем. Если не обновить состояние сейчас, расчет суммы нового долга будет неверным. Параметр rateMode, который мы указываем означает "Текущий режим долга, который я подтверждаю и хочу сменить".
Мы напишем 2 теста, что понять разницу происходящего.
Наш первый тест testSwapBorrowRateModev1 нам показывает:
У нас есть 25 USDC долга под Stable ставку. На балансе 25 токенов StableDebtToken. VariableDebtToken = 0.
Вызываем swapBorrowRateMode(address(usdc), 1). "У меня режим 1 (StableDebtToken), хочу сменить".
Проходит 1 год. Стабильная ставка начисляла проценты по фиксированному графику.
Before Swap - Stable: 25000000 Variable: 0 After Swap - Stable: 0 Variable: 27740286
Долг вырос до ~ 27.77.
Наш второй тест testSwapBorrowRateModev2 делает обратное (variable -> stable).
Before Swap - Stable: 0 Variable: 25000000 After Swap - Stable: 25710641 Variable: 0
По результатам долг оказался меньше ~ 25.71.
В итоге мы видим, что за этот год выгоднее была stable ставка, в следующем году ситуация на рынке может измениться, и чтобы не упустить тренд - существует наша функция.
Функция rebalanceStableBorrowRate
Параметр | Описание |
asset | Адрес заимствованного базового актива |
user | Адрес пользователя, которому необходимо выполнить ребалансировку |
Эта функция - механизм защиты для кредиторов (депозиторов), а не для заемщиков. Ее цель: Принудительно обновить процентную ставку по существующему стабильному долгу пользователя до текущей рыночной ставки, если условия в пуле стали критическими.
Простыми словами, если слишком много людей взяли деньги под низкую фиксированную ставку, и из-за этого тем, кто положил деньги в банк (депозиторам), почти ничего не капает, протокол имеет право сказать: «Ваша льготная ставка отменяется, теперь вы платите столько же, сколько все остальные прямо сейчас».
По шагам:
Подготовка данных
DataTypes.ReserveData storage reserve = _reserves[asset]; IERC20 stableDebtToken = IERC20(reserve.stableDebtTokenAddress); IERC20 variableDebtToken = IERC20(reserve.variableDebtTokenAddress); address aTokenAddress = reserve.aTokenAddress; uint256 stableDebt = IERC20(stableDebtToken).balanceOf(user);
Функция находит резерв актива и проверяет, сколько у конкретного пользователя есть долга под стабильную ставку. Если долга нет, дальше идти смысла нет (хотя проверка будет внутри валидации).
Валидация validateRebalanceStableBorrowRate:
Проверка активности резерва.
Расчет общей суммы долга
uint256 totalDebt = stableDebtToken.totalSupply().add(variableDebtToken.totalSupply()).wadToRay();
Расчет доступной ликвидности:
Смотрит сколько свободных денег лежит в пуле.
Функция проверяет баланс базового актива (например USDC) на адресе aUSDC.
Получаем ликвидность в Ray.
uint256 availableLiquidity = IERC20(reserveAddress).balanceOf(aTokenAddress).wadToRay();
Расчет коэффициента использования (Usage Ratio):
Вычисляет процент загруженности пула.
Usage Ratio = Весь долг / Весь долг + Свободные деньги.(общая ликвидность).
Если результат близок к 1.0 ray, то значит пул пуст.
Получение текущей доходности депозиторов:
uint256 currentLiquidityRate = reserve.currentLiquidityRate;
Получение максимальной возможной переменной ставки:
uint256 maxVariableBorrowRate = IReserveInterestRateStrategy(reserve.interestRateStrategyAddress).getMaxVariableBorrowRate();
Проверка на кризис:
Проверяет, пуст ли пул, страдают ли депозиторы.
Сработает, если загруженность > 95 % и доход депозиторов критически низок.
require(usageRatio >= REBALANCE_UP_USAGE_RATIO_THRESHOLD &¤tLiquidityRate <= maxVariableBorrowRate.percentMul(REBALANCE_UP_LIQUIDITY_RATE_THRESHOLD), Errors.LP_INTEREST_RATE_REBALANCE_CONDITIONS_NOT_MET);
Это самый важный этап. Протокол не может просто так менять ставку кому попало. Внутри этой проверки должны соблюдаться два жестких условия: Высокая нагрузка на пул: Использовано более 95% всех доступных средств в резерве (Usage ratio > 95%). То есть денег почти не осталось. Низкий доход депозиторам: Текущая доходность для тех, кто депозитит деньги, упала ниже определенного порога (REBALANCE_UP_THRESHOLD * maxVariableBorrowRate).
Почему это происходит? Потому что много людей заняли деньги давно под низкий стабильный процент (например, 2%), а текущая рыночная ставка уже 10%. Разница "съедается" протоколом, и депозиторы почти не получают прибыли. Это угрожает ликвидности. Если эти условия не выполнены, вызов функции упадет с ошибкой. Любой пользователь может вызвать эту функцию, но сработает она только при кризисе в резерве.
Обновление состояния
Ребалансировка
IStableDebtToken(address(stableDebtToken)).burn(user, stableDebt); IStableDebtToken(address(stableDebtToken)).mint(user, user, stableDebt, reserve.currentStableBorrowRate);
Здесь происходит самое интересное. В отличие от swap, где менялся тип долга (Stable ↔ Variable), здесь тип остается тем же (Stable → Stable), но меняются параметры:
Burn: Сжигается старый долг пользователя с его старой низкой ставкой (например, 2%). Долг обнуляется.
Mint: Сразу же создается новый долг на ту же сумму (например, 100 USDC), но с параметром reserve.currentStableBorrowRate — то есть с текущей высокой ставкой (например, 10%).
Результат для пользователя таков, что сумма долга не меняется мгновенно. Но будущие платежи резко вырастут, так как теперь проценты капают по новой, высокой ставке.
Пересчет ставок пула
reserve.updateInterestRates(asset, aTokenAddress, 0, 0);
После того как часть долгов была переведена на высокую ставку, общая статистика пула меняется. Доходность для депозиторов должна вырасти. Функция пересчитывает глобальные ставки, чтобы депозиторы начали получать справедливые проценты.
Представьте ситуацию:
В пуле есть $1 млн.
$950k занято под стабильные 3% (взяты год назад).
Рыночная ставка сейчас 15%.
Депозиторы видят, что их APY упал до 0.5%, и начинают массово забирать деньги из пула, чтобы положить их куда-то еще.
Возникает риск банкротства пула.
Функция rebalanceStableBorrowRate решает эту проблему:
Кто-то (бот или любой пользователь) вызывает её.
Протокол проверяет: "Да, у нас загруженность 95%, депозиторы страдают".
Протокол говорит заемщикам: "Извините, ваша льготная ставка 3% аннулируется. Теперь вы платите 15%, как все".
Доходность пула восстанавливается, депозиторы успокаиваются и не забирают ликвидность.
Функция rebalanceStableBorrowRate — это «аварийный клапан». Она позволяет протоколу в экстренной ситуации лишить заемщиков их фиксированной низкой ставки и перевести их на текущую высокую, чтобы спасти экономику пула и обеспечить выплаты депозиторам.
Функция setUserUseReserveAsCollateral
function setUserUseReserveAsCollateral( address asset, bool useAsCollateral ) external;
Параметр | Описание |
asset | Адрес депонированного базового актива |
useAsCollateral | true, если пользователь хочет использовать депозит в качестве залога, false в противном случае |
Здесь все просто, позволяет пользователю установить на свой aToken ограничение быть залогом. Если true - aToken может быть залогом, false - нет.
Наш тест testSetUserUseReserveAsCollateral показывает, если мы установим aDAI в false, то при попытке взять заем нам выдаст ошибку "9":
string public constant VL_COLLATERAL_BALANCE_IS_0 = '9'; // 'The collateral balance is 0'
4. Функции для ликвидаторов
Функция liquidationCall
function liquidationCall( address collateralAsset, address debtAsset, address user, uint256 debtToCover, bool receiveAToken ) external;
Параметр | Описание |
collateralAsset | Актив залога, который получит ликвидатор |
debtAsset | Актив долга, который погашает ликвидатор |
user | Адрес заемщика, чья позиция ликвидируется |
debtToCover | Сумма долга для погашения |
receiveAToken | Флаг true - ликвидатор получит aToken, false - получит базовый актив (например USDC) |
Тест testLiquidationCall
Мы депонируем DAI для обеспечения нашего заема, затем занимаем 37 USDC, именно это количество мы можем занять за счет нашего залога 50ти aDAI.
Затем мы проверяем heatlhfactor user3, он > 1.
Далее мы получаем адрес оракла, с помощью которого мы смотрим цены на токены.
Далее мы деплоим наш mock-оракл и подменяем его вместо адреса оригинального оракла. Это нужно для выполнения сценария падения цены.
С помощью функции setAssetPrice устанавливаем цены для DAI и USDC - 1$.
С помощью той же функции искусственно роняем цену dai в 10 раз. Теперь healthfactor user3 < 1.
Теперь ликвидатор может ликвидировать позицию.
Ликвидатор гасит примерно 4.8 USDC (сумма, необходимая, чтобы выкупить весь доступный залог с учетом бонуса для ликвидатора).
В логах тестов видно, что у user3 было 50 DAI, стало 0. То есть весь залог забрали, долг уменьшился с 37 до ~ 32 USDC.
В нашем тесте после ликвидации у пользователя остался долг ~32 USDC при нулевом залоге. Это не мгновенный убыток протокола, но рискованная ситуация. Пользователь заблокирован от новых займов до полного погашения. Если рынок упадет еще сильнее и долг превысит стоимость любого возможного будущего залога, протокол спишет остаток через Reserve Fund (Safety Module), чтобы защитить вкладчиков.
Более подробно, как происходит расчет:
Расчет производится в LendingPool, но логика в в другом контракте.
Инициализация данных
collateralReserve: Данные резерва актива, который заложен у пользователя (например, ETH).
debtReserve: Данные резерва актива, который пользователь занял и должен вернуть (например, USDC).
userConfig: Настройки пользователя (какие активы используются как залог).
Проверка Health Factor
Расчет: Функция суммирует весь залог пользователя (в USD), взвешивает его на коэффициент ликвидации (LTV), делит на общий долг (в USD).
Получение текущего долга пользователя. Считаем реальный долг пользователя на текущий момент с учетом накопленных процентов. Для стабильной ставки: пересчитывается индекс. Для переменной: умножается на текущий индекс займа. vars.userStableDebt + vars.userVariableDebt = Общий долг.
Валидация параметров ликвидации - включен ли актив залога пользователем как обеспечение, есть ли долг в debtAsset, разрешена ли ликвидация для этой пары активов.
Расчет максимального долга для ликвидации
vars.maxLiquidatableDebt = vars.userStableDebt.add(vars.userVariableDebt).percentMul( LIQUIDATION_CLOSE_FACTOR_PERCENT);
Формула: MaxDebt = (Stable+Variable) * 0.5, то есть, если ликвидатор хочет покрыть $1000, а макс. можно $500 -> берем $500. Если хочет $100, а можно $500 -> берем $100. vars.actualDebtToLiquidate — это сумма, которую ликвидатор реально потратит.
Сколько залога получить за этот долг?
(vars.maxCollateralToLiquidate, vars.debtAmountNeeded) = _calculateAvailableCollateralToLiquidate( collateralReserve, debtReserve, collateralAsset, debtAsset, vars.actualDebtToLiquidate, vars.userCollateralBalance);
Здесь вызывается функция _calculateAvailableCollateralToLiquidate . Она возвращает два значения. maxCollateralToLiquidate - cколько токенов залога получит ликвидатор, debtAmountNeeded - cколько точно нужно заплатить долга.
Коррекция суммы долга.
if (vars.debtAmountNeeded < vars.actualDebtToLiquidate) { vars.actualDebtToLiquidate = vars.debtAmountNeeded;}
Если в предыдущем шаге выяснилось, что залога не хватает на всю сумму debtToCover, мы уменьшаем сумму долга, которую будет платить ликвидатор, до эквивалента доступного залога.
Сжигание токенов пользователя, обновление процентных ставок, перевод aToken от user ликвидатору.
Оплата долга ликвидатором. Ликвидатор своими деньгами погашает дыру в балансе протокола, образовавшуюся из-за плохого долга пользователя. Протокол становится "целым" по долговому активу.
FlashLoan
Функция flashLoan
function flashLoan( address receiverAddress, address[] calldata assets, uint256[] calldata amounts, uint256[] calldata modes, address onBehalfOf, bytes calldata params, uint16 referralCode ) external;
Параметр | Описание |
receiverAddress | Адрес контракта, который получит деньги |
assets | Массив адресов токенов |
amounts | Массив сумм для каждого токена |
modes | Вариация действий, если деньги не вернут |
onBehalfOf | Адрес, который берет долг |
params | Данные для контракта |
referralCode | Код для отслеживания партнёра/интегратора |
По шагам:
Валидация проверяются входные данные (массивы активов, сумм и режимов займа) через ValidationLogic.
Расчет комиссии - для каждого актива вычисляется премия (premium) по формуле:
(Обычно комиссия составляет 0.09%).
Трансфер средств - базовые активы переводятся из резервов пула на адрес контракта-получателя (receiverAddress) через вызов transferUnderlyingTo.
Вызов получателя - пул вызывает функцию executeOperation на контракте получателя. Именно здесь размещается пользовательская логика (свопы, ликвидация и т.д.).
Погашение долга (Post-Execution) - после возврата управления из executeOperation, пул проверяет баланс. Возможны два сценария:
Режим NONE (mode 0): Контракт должен вернуть сумму займа + комиссию. Пул обновляет индексы ликвидности и забирает средства через safeTransferFrom.
Режим STABLE/VARIABLE (mode 1/2): Если средства не возвращены, система пытается открыть долговую позицию (borrow) на сумму займа, проверяя обеспечение. Если залога недостаточно — транзакция ревертится.
Теперь к нашему тесту, для этого мы создали отдельный тест flashloan.t.sol
forge test flashloan.t.sol -vv
Start Arbitrage Simulation 1. Contract Balance BEFORE FlashLoan: 0 MockDEX: Transferred profit to contract: 5000000 2. Contract Balance AFTER FlashLoan: 4100000 3. Gross Profit generated: 5000000 4. Premium paid to Aave: 900000 5. Net Profit remaining: 4100000
Контракт исполнителя: aave-v2/protocol-v2/test/FlashLoanArbitrage/FlashLoanArbitrage.sol.
Наш контракт реализует интерфейс IFlashLoanReceiver. Его задача — получить ликвидность, выполнить арбитражную операцию и вернуть долг.
Инициализация - в конструкторе задаются адреса пула и оракла. Оракл используется для симуляции расхождения цен (условие для арбитража).
Запуск заема (executeArbitrage):
Обновляет цену актива в мок-оракле (создает искусственный дисбаланс).
Формирует массивы активов и сумм.
Устанавливает modes[0] = 0, что означает требование полного возврата средств в конце транзакции.
Логика исполнения искусственного арбитража описана в комментариях FlashLoanArbitrage.sol.
Используется только чужая ликвидность. Если стратегия убыточна, транзакция ревертится, и мы теряем только газ. Все действия (заем -> своп -> возврат) происходят в одной транзакции. Функция flashLoan в контракте пула выступает в роли диспетчера. Её главная задача — обеспечить атомарность операции: средства должны быть выданы, использованы и возвращены с комиссией в рамках одной транзакции.

Подводя итоги, мы узнали, что Aave V2 обладает широким функционалом, в рамках которого мы можем реализовывать свои идеи: от получения прибыли при внесении ликвидности в протокол до стратегии арбитража.
Спасибо за уделенное время, буду рад вопросам в комментариях, а так же конструктивной критике.
