Прграммирование стратегий DeFi

Знакомство

В наше время экосистема Ethereum достаточно богата и сложна. Существует множество приложений DeFi: децентрализованные биржи, протоколы стейкинга, денежные рынки, хранилища для выращивания урожая, деривативы, кредитное плечо и т. Д. По мере развития протокола DeFi они становятся все более и более переплетенными, что позволяет создавать новые сложные стратегии.

В этой статье я покажу вам, как запрограммировать пользовательскую стратегию DeFi, которая взаимодействует с несколькими платформами DeFi, чтобы создать позицию с использованием заемных средств, которая приносит пассивный доход. Более того, мы будем запускать его против основной сети Ethereum без развертывания и оплаты газа — это позволит нам проверить стратегию перед ее развертыванием.

Подход, который я собираюсь показать, требует навыков программирования и знания языка программирования Solidity. Если вы не программист или предпочитаете более короткий путь, есть удобный сервис, который позволяет построить точно такую же стратегию — DeFi Saver. Кроме того, Instadapp предоставляет решение в 1 клик для той же стратегии.

Вы найдете полный исходный код здесь.

Пример внедрения

В стратегии, которую мы собираемся реализовать, мы постараемся получать пассивный доход на ставках ETH. Более того, мы постараемся максимизировать нашу прибыль, взяв флеш-кредит.

Вы, вероятно, знаете, что Ethereum находится в процессе перехода от Proof-of-Work к Proof-of-Stake. Не вдаваясь в подробности, PoS заменяет майнеров валидаторами блоков и освобождает их от решения головоломки PoW. Чтобы стать валидатором, нужно поставить 32 ETH — сумму, которая не всем доступна в наши дни. В качестве вознаграждения за производство блоков валидаторы получают долю эмиссии ETH.

Lido — это сервис, который делает ставку Ethereum PoS доступной для всех: с Lido любой, у кого есть ETH, может сделать ставку и начать получать вознаграждение (около 4% годовых на момент написания). В обмен на ETH, Lido дает вам stETH, который является токеном rebase, то есть токеном, предложение которого изменяется со временем.

В феврале 2022 года Aave добавила stETH в качестве актива и разрешила использовать его в качестве залога. Это означает, что мы можем поставить наш ETH в Lido, а затем внести stETH в Aave, чтобы получить кредит и использовать этот кредит где-то еще. И мы по-прежнему будем получать награду от Лидо. Хороший!

И заключительная часть нашей стратегии — Balancer, которая является децентрализованной биржей. Но мы будем использовать его, чтобы взять флэш-кредит — кредит, который должен быть возвращен в той же сделке.

Давайте посмотрим, как все эти элементы играют вместе.

Стратегия

  1. Возьмите немного ETH через флэш-кредит.
  2. Поставьте наш ETH + флэш-одолженный ETH в Лидо. Lido дает нам stETH взамен.
  3. Поставьте на карту stETH в Aave.
  4. Займите достаточно ETH у Aave, чтобы погасить флэш-кредит.

В конечном итоге мы получим больше stETH, депонированных в Aave, что означает более высокую прибыль! Но поскольку около 2/3 ETH в этой сумме одалживаются, существует риск ликвидации.

Займы Aave должны быть обеспечены до определенного фактора залога. Чтобы максимизировать нашу прибыль, мы возьмем все доступные ETH. Поскольку stETH (наше обеспечение) и ETH (то, что мы заимствуем) являются коррелированными активами (stETH выпускается 1-к-1 к поставленному ETH), риск потери stETH корреляции с ETH низок. Но это все еще риск, и это наш самый большой.

Еще один факт, который стоит отметить: мы будем платить проценты на заемную сумму. Ставка составляет всего 0,23% APY на момент написания статьи, и она может вырасти выше, если будет высокий спрос на ETH на Aave.

Инструменты

Для реализации стратегии мы напишем смарт-контракт в Solidity. Хардкор! Но это дает нам полный контроль над процессом.

Мы будем использовать Foundry, который является фантастическим современным инструментарием для разработки смарт-контрактов Ethereum. Мы будем использовать Forge для создания, тестирования и проверки нашей стратегии.

Давайте напишем договор!

Стратегический контракт

Инициализируйте новый проект и создайте контракт Стратегии:

$ mkdir defi-strategy
$ cd defi-strategy
$ forge init

Создавать:Strategy.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

contract Strategy {
    error NotOwner();

    uint256 constant funds = 1 ether;
    uint256 constant flashLoanFunds = (funds * 230) / 100;

    address constant aaveAddress = 0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9;
    address constant balancerAddress =
        0xBA12222222228d8Ba445958a75a0704d566BF2C8;
    address constant lidoAddress = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84;
    address constant stethAddress = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84;
    address constant wethAddress = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;

    address private immutable owner;

    constructor() public {
        owner = msg.sender;
    }

    function go() public payable {
        if (msg.sender != owner) revert NotOwner();

        ...
    }
}

1 ether это наши собственные средства, которые мы будем делать в Lido и депонировать в Aave. это сумма, которую мы хотим взять в качестве флеш-кредита, она равна х2,3 наших собственных средств. Этот множитель рассчитывается как 0,7/0,3, где 0,7 — залоговый коэффициент stETH на Aave, а 0,3 — 1 минус залоговый коэффициент. Важно правильно рассчитать эту сумму: именно эту сумму мы позже займем у Aave, чтобы погасить флэш-кредит, и именно эту сумму мы в конечном итоге задолжим Aave.flashLoanFunds

Я немного округлил число вниз, так как цена stETH в ETH была немного ниже 1, а Aave не разрешил кредитовать 2,33 ETH.

Мы также устанавливаем адреса всех служб DeFi, которые мы собираемся использовать. Поскольку мы будем использовать эту стратегию только в основной сети, адреса могут быть жестко закодированы как константы.

В качестве первого шага мы хотим взять флеш-кредит, мы сделаем это, вызвав метод в контракте Balancer:flashLoan

import {IBalancer} from "./interfaces.sol";

...

function go() public payable {
    if (msg.sender != owner) revert NotOwner();

    address[] memory tokens = new address[](1);
    tokens[0] = wethAddress;

    uint256[] memory amounts = new uint256[](1);
    amounts[0] = flashLoanFunds;

    IBalancer(balancerAddress).flashLoan(
        address(this),
        tokens,
        amounts,
        ""
    );
}

Первый параметр – это адрес, на который будут поступать средства – мы получаем их по контракту. Следующим параметром является список токенов (в нашем случае только WETH) и сумм кредитов ().flashLoanflashLoanFunds

Когда мы звоним, Balancer отправляет необходимые суммы на адрес получателя и вызывает специальный метод на этом адресе. Это означает, что флэш-кредиты могут быть взяты и использованы только смарт-контрактами!flashLoan

Наш контракт должен реализовать специальную функцию, которая будет вызываться контрактом Balancer после того, как он отправит токены. Именно в эту функцию мы будем реализовывать остальную часть стратегии. И именно эту функцию нужно будет возвращать одолженные суммы.

В случае Balancer функция называется:receiveFlashLoan

function receiveFlashLoan(
    IERC20[] memory tokens,
    uint256[] memory amounts,
    uint256[] memory feeAmounts,
    bytes memory userData
) public {
    if (msg.sender != balancerAddress) revert NotBalancer();

    ...

Когда функция вызывается Balancer, кредит уже депонируется на адрес нашего договора.

tokens и являются теми же параметрами, которые мы передали . это сборы, которые нам нужно заплатить за взятие кредита — кредиты ETH на Balancer на данный момент бесплатны.amountsflashLoanfeeAmounts

Обратите внимание, что функция является общедоступной, что делает ее вызываемой другим контрактом (Balancer), но мы хотим, чтобы ее вызывал только контракт Balancer.

Теперь мы можем приступить к реализации этой стратегии. Следующим шагом является размещение нашего ETH + одолженного ETH в Lido. Тем не менее, то, что мы одолжили, это WETH, токен ERC20, представляющий ETH, но Lido требует ETH, а не WETH. Нам нужно развернуть одолженные токены:

IERC20 loanToken = tokens[0];
uint256 loanAmount = amounts[0];

// Unwrap WETH
IWETH(wethAddress).withdraw(loanAmount);

Контракт WETH также отвечает за конверсию ETH↔WETH. Чтобы развернуть WETH (преобразовать WETH в ETH), нам нужно вызвать .withdraw

Теперь мы готовы сделать ставку на ETH в Lido:

// Stake ETH
ILido(lidoAddress).submit{value: funds + flashLoanFunds}(address(0x0));
uint256 stethBalance = IERC20(stethAddress).balanceOf(address(this));

Чтобы сделать ставку на ETH, мы звоним в контракт Lido и отправляем все наши ETH вместе с вызовом. Единственным параметром функции является реферальный адрес, мы можем просто установить его на ноль.submit

Теперь мы можем депонировать наш stETH на Aave:

// Deposit stETH
IERC20(stethAddress).approve(aaveAddress, stethBalance);
IAAVE(aaveAddress).deposit(stethAddress, stethBalance, owner, 0);

Сначала нам нужно позволить Aave взять наш stETH — мы делаем это, вызывая (Aave звонит ERC20, чтобы вытащить средства пользователей). Затем мы призываем фактически внести средства. Это общая функция, которая работает со всеми рынками Aave. В качестве параметров мы передаем адрес токена stETH, сумму, которую мы хотим внести, и адрес, от имени которого мы вносим депозит.approvetransferFromdeposit

Следующий шаг: займите у Aave для погашения флеш-кредита.

// Borrow ETH
IAAVE(aaveAddress).borrow(wethAddress, loanAmount, 2, 0, owner);

Мы заимствуем WETH, чтобы погасить флэш-кредит, который мы взяли в самом начале. Третьим параметром функции является режим процентной ставки. Aave поддерживает переменные и стабильные проценты по займам, при этом стабильные часто значительно выше. Мы проходим здесь, что означает переменную ставку.2

Последним параметром является адрес, на имя которого мы возьмем кредит. Мы установили его на , что означает, что придется разрешить контракту брать кредит от его имени — мы сделаем это в тесте.ownerowner

И последний шаг, флэш-погашение кредита.

// Repay flash loan
loanToken.transfer(balancerAddress, loanAmount);

Мы просто переводим одолженную сумму WETH на адрес контракта Balancer. После завершения выполнения этой функции () элемент управления вернется к функции из контракта Balancer. Затем Balancer проверит, что флэш-кредит был погашен. Если он не будет погашен, транзакция будет отменена.receiveFlashLoanflashLoan

Ну вот! Наша стратегия завершена!

Оценка стратегии

Чтобы оценить стратегию, мы напишем тест в Solidity. Тест будет имитировать развертывание и выполнение стратегии. Затем мы будем использовать Forge для запуска теста на основной сети Ethereum! Это позволит нам убедиться в том, что мы правильно назвали все контракты и что наши расчеты также были правильными. В тесте мы также получим информацию о залоге и задолженности от Aave и проверим LTV и фактор здоровья.

Давайте настроим тест:

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.13;

import "ds-test/test.sol";
import {Strategy} from "../Strategy.sol";
import {IAAVE, IERC20, VariableDebtToken} from "../interfaces.sol";

contract StrategyTest is DSTest {
    address constant aaveAddress = 0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9;
    address constant variableDebtWethAddress =
        0xF63B34710400CAd3e044cFfDcAb00a0f32E33eCf;

    Strategy s;

    function setUp() public {
        s = new Strategy();
    }

    ...

Нам в основном нужно только развернуть стратегический контракт во время настройки.

А теперь давайте оценим стратегию:

function testGo() public {
    VariableDebtToken(variableDebtWethAddress).approveDelegation(
        address(s),
        2.3 ether
    );
    ...

Стратегический контракт берет кредит на Aave от имени владельца контракта — это означает, что владелец должен позволить стратегическому контракту сделать это. Для каждого актива, поддерживаемого Aave, aave имеет два развернутых специальных токена: и . Они используются для отслеживания заимствованных позиций пользователей с соответствующим режимом процентной ставки. На самом деле это токенизированные заимствования. И они также позволяют делегировать заимствование: функция позволяет делегату заимствовать активы у Aave от имени делегата. Вот что мы здесь делаем: мы позволяем стратегическому контракту заимствовать от имени тестового контракта.StableDebtTokenVariableDebtTokenapproveDelegation

Затем мы запускаем стратегию:

s.go{value: 1 ether}();

И проверка его результата путем получения данных учетной записи пользователя из Aave:

(
    uint256 totalCollateralETH,
    uint256 totalDebtETH,
    uint256 availableBorrowsETH,
    uint256 currentLiquidationThreshold,
    uint256 ltv,
    uint256 healthFactor
) = IAAVE(aaveAddress).getUserAccountData(address(this));

assertEq(
    totalCollateralETH,
    3.298112774422300227 ether,
    "invalid total collateral"
);
assertEq(totalDebtETH, 2.3 ether, "invalid total debt");
assertEq(
    availableBorrowsETH,
    0.008678942095610159 ether,
    "invalid available borrows ETH"
);
assertEq(
    currentLiquidationThreshold,
    7500,
    "invalud current liquidation threshold"
);
assertEq(ltv, 7000, "invalid LTV");
assertEq(
    healthFactor,
    1.07547155687683703 ether,
    "invalid health factor"
);
  1. Общая стоимость нашего залога составляет 3.2981 ETH. Поскольку stETH торгуется чуть ниже 1 ETH, стоимость нашего залога оказывается немного ниже 3.3 ETH (мы внесли 1 stETH + 2.3 stETH).
  2. Наш общий долг составляет 2,3 ETH — это сумма, которую мы заимствовали у Aave для погашения флэш-кредита.
  3. 0.00867 ETH по-прежнему доступен для заимствования, что означает, что множитель, который мы рассчитали в самом начале, был правильным. Мы все еще можем уменьшить его, чтобы снизить риск ликвидации.
  4. Текущий порог ликвидации составляет 75%, что означает, что наша позиция будет ликвидирована при/>= 75%. Соотношение долга к залогу нашей позиции составляет ~ 70%, что означает, что есть некоторое пространство для движения цены.totalDebtETHtotalCollateralETH
  5. Loan-to-Value (LTV) составляет 70%, что означает, что stETH, в качестве залога, позволяет занимать до 70% своей стоимости в ETH.
  6. Коэффициент здоровья составляет 1,07. А это значит, что наша позиция близка к ликвидации. Как я уже говорил выше, поскольку stETH и ETH являются коррелированными активами, колебания цен маловероятны, а риск ликвидации низок. Но она все еще существует. ?

Теперь вы хотите спросить: «Но как вы получили все эти цифры?» Я провел тест против основной сети Ethereum. Вот как это сделать.

Во-первых, вам нужен узел Ethereum. Если вы не используете локальный, вы можете использовать Alchemy или Infura. Вам нужно получить URL-адрес узла HTTP.

Далее вам нужно выполнить следующую команду:

$ forge test --fork-url=$NODE_URL

--fork-url включает режим форка, в котором Forge запускается как прокси-сервер, передающий все вызовы RPC API указанному серверу . Это означает, что все, что мы делаем в тесте, выполняется против основной сети Ethereum. Конечно, он не выполняет транзакции в основной сети: транзакции по-прежнему выполняются в локальной тестовой сети.NODE_URL

Режим форка — отличная среда эмуляции, которая позволяет взаимодействовать с основной сетью Ethereum.

Источник