Автор: Павел Найданов 🕵️♂️
Опр! Vault - это смарт-контракт "хранилище", который позволяет пользователям максимизировать прибыль с активов, которыми они владеют. Пользователи передают свои активы смарт-контракту, который реализует некоторую стратегию заработка за счет использования предоставленных активов с автоматическим начислением процентов и ребалансировки.
В момент передачи пользователем активов в vault, взамен выдаются другие токены(share), приносящие доход. Эти токены со временем растут в цене и представляют собой частичное владение пользователем активов в vault. Их стоимость растет пропорционально росту стоимости активов в пуле vault.
Предположим, что vault принимает ETH(нативная валюта сети Ethereum) в качестве актива для максимизации прибыли. Я могу передать ETH смарт-контракту vault и взамен получу share токен vETH. Share токен - это своего рода долговая расписка vault, которая позволяет получить мой ETH обратно.
Переданный мной ETH, внутри vault, объединяется с ETH других пользователей и используется в различных протоколах для извлечения доходности. Vault проверяет доходность по разным протоколам, когда пользователь передает или снимает активы. Это вызывает ребалансировку активов в пуле, если существует более выгодная возможность получения доходности. Ребалансировка - это изменение соотношения активов между различными протоколами заработка или даже стратегиями с целью извлечения максимальной доходности.
Например! Если MakerDao предлагает более высокий доход от вложения ETH в качестве ликвидности, чем Compound, то vault может принять решение о перемещении всего ETH или части из Compound в MakerDao. MakerDao и Compound - это популярные lending протоколы.
Стандарт ERC-4626 был разработан в рамках предложений по улучшению Ethereum. Он был создан в соавторстве с Джоуи Санторо, основателем протокола Fei.
До момента появления стандарта среди vaults отсутствовала стандартизация, что приводило к разнообразию реализаций. В свою очередь, это затрудняло интеграцию протоколов, реализующих приложения поверх vault.
Сам стандарт представляет собой смарт-контракт, который является расширением стандарта ERC-20 и регламентирует:
- Ввод и вывод активов
- Расчет количества токенов для ввода и вывода активов
- Балансы активов
- Отправку событий
Появление стандарта снизило затраты на интеграцию и повысило надежность реализаций.
Техническая реализация наследуется от ERC-20 стандарта. Это позволяет минтить и сжигать share(долевые) токены в обмен на assets(underlying или базовые) токены. Для этого процесса vault предоставляет стандартные функции: deposit(), mint(), redeem(), burn().
Важно! Стандарт может реализовать для vault функционал других стандартов, например ERC-2612: Permit Extension for EIP-20 Signed Approvals.
В стандарте ERC-4626 предусмотрены две функции преобразования:
convertToShares(uint256 assets). Рассчитывает количество share токена, которое можно получить за переданное количество базового токена.convertToAssets(uint256 shares). Рассчитывает количество базового токена, которое можно получить за переданное количество share токена.
Процесс передачи токенов на контракт vault. В процессе передачи, согласно стандарту, необходимо рассчитать количество share токенов, списать базовый токен, сминтить share токен и отправить solidity событие.
Функция deposit() может выглядеть подобно.
function deposit(uint256 assets, address receiver) public virtual returns (uint256 shares) {
/// Получаем доступное количество share токена
/// для внесенного количества базового токена(assets)
/// Под капотом вызывается convertToShares(uint256 assets)
shares = previewDeposit(assets);
if (shares == 0) {
/// Если количество share токена равно нулю, то возвращается ошибка
revert ZeroShares();
}
/// Трансфер базового токена на контракт vault
asset.safeTransferFrom(msg.sender, address(this), assets);
/// Минтинг взамена share токена
_mint(receiver, shares);
/// Отправка события, подтверждающее депозит пользователя
emit Deposit(msg.sender, receiver, assets, shares);
}Процесс передачи share токенов c целью изъятия базового актива, который был вложен через вызов функции deposit(). Согласно стандарту необходимо принять share токен, рассчитать количество базового токена и сжечь переданное количество share токена.
Функция redeem() может выглядеть подобно.
function redeem(
uint256 shares,
address receiver,
address owner
) public virtual returns (uint256 assets) {
/// Проверяется, действительно ли указанный адрес вносил базовый токен на контракт
/// или давал ли он разрешение на управление своим депозитом вызывающему функцию
if (msg.sender != owner) {
uint256 allowed = allowance[owner][msg.sender];
if (allowed != type(uint256).max) allowance[owner][msg.sender] = allowed - shares;
}
/// Получаем доступное количество базового токена(assets)
/// при возврате указанного количества share токена
/// Под капотом вызывается convertToAssets(uint256 assets)
assets = previewRedeem(shares);
if (assets == 0) {
/// Если количество базового токена равно нулю, то возвращается ошибка
revert ZeroAssets();
}
/// Сжигание share токена
_burn(owner, shares);
/// Отправка базового токена до получателя
asset.safeTransfer(receiver, assets);
/// Отправка события об успешном окончании процесса снятия базового актива
/// Согласно стандарту у нас есть только событие Withdraw()
emit Withdraw(msg.sender, receiver, owner, assets, shares);
}Функция mint() реализует процесс предоставления базового токена контракту vault. Отличается этот процесс от deposit() тем, что здесь в аргументах функции указывается не количество базового актива, а количество share токена, которое необходимо получить после вызова функции.
Функция mint() может выглядеть подобно.
function mint(uint256 shares, address receiver) public virtual returns (uint256 assets) {
/// Рассчитывается количество базового актива для передачи на контракт
assets = previewMint(shares);
/// Трансфер базового актива от вызывающего до контракта
asset.safeTransferFrom(msg.sender, address(this), assets);
/// Минтинг share токенов
_mint(receiver, shares);
/// Отправка события об успешности процесса предоставления актива
/// Согласно стандарту у нас есть только событие Deposit
emit Deposit(msg.sender, receiver, assets, shares);
}Функция withdraw() реализует процесс изъятия базового токена из контракта vault. Отличается этот процесс от redeem() тем, что здесь в аргументах функции указывается не количество share токена, а количество базового токена(assets), которое необходимо получить после вызова функции.
Функция withdraw() может выглядеть подобно.
function withdraw(
uint256 assets,
address receiver,
address owner
) public virtual returns (uint256 shares) {
/// Рассчитывается количество share токена для передачи на контракт
shares = previewWithdraw(assets);
/// Проверяется, действительно ли указанный адрес вносил базовый токен на контракт
/// или давал ли он разрешение на управление своим депозитом вызывающему функцию
if (msg.sender != owner) {
uint256 allowed = allowance[owner][msg.sender];
if (allowed != type(uint256).max) allowance[owner][msg.sender] = allowed - shares;
}
/// Сжигание share токена
_burn(owner, shares);
/// Отправка базового токена до получателя
asset.safeTransfer(receiver, assets);
/// Отправка события об успешном окончании процесса снятия базового актива
emit Withdraw(msg.sender, receiver, owner, assets, shares);
}Самые популярные библиотеки уже реализовали минимальный функционал для контракта vault. Можно брать контракты, наследоваться от них и дорабатывать свой собственный контракт vault.
- Минимальная реализация vault в библиотеке solmate.
- Минимальная реализация vault в библиотеке openZeppelin.
Важно! Стандарт полностью обратно совместим со стандартом ERC-20.
- Aave vault
- Minimal ERC4626-style tokenized Vault implementation with ERC1155 accounting
- Rari-Capital vault
- Протокол Fuji V2 Himalaya. Контракт YieldVault. Этот контракт наследуется от BaseVault.
- ERC-4626: Tokenized Vaults
- ERC-2612: Permit Extension for EIP-20 Signed Approvals
- ERC-20: Token Standard
- Про ERC-4626 на ethereum.org
- Прекрасный простой пример vault на solidity-by-example.

