ERC-4626 vault: почему maxWithdraw может быть нулевым и как автоматизировать вывод средств из yield-стратегий

Когда вы кладёте USDC в yield-стратегию, интуиция подсказывает простую логику: «мои деньги лежат в пуле, я могу их в любой момент забрать».

ERC-4626 vaults как стандарт формально это подтверждают — у каждого vault есть функция maxWithdraw(address owner), которая возвращает максимальное количество базового актива, доступное для вывода указанному пользователю прямо сейчас.

На практике эта функция регулярно возвращает ноль.

Не потому что vault сломан или средства потеряны. А потому что размещение капитала в стратегии и мгновенный вывод — это две разные операции, между которыми почти всегда есть зазор по ликвидности.

Ниже — разбор того, откуда берётся этот зазор, как он выглядит в ERC-4626/Euler EVK vaults и какой automation layer имеет смысл на случай, когда «просто подождать» перестаёт быть приемлемым вариантом.

Почему maxWithdraw не равен вашему балансу

Если посмотреть на ERC-4626 контракт, maxWithdraw — это не balanceOf. На уровне стандарта она определяется так: максимальное количество базового актива, которое владелец может вывести в текущем состоянии vault.

vault не просто хранит депозиты в виде стейблов на балансе. Он распределяет их по стратегиям:

  • часть средств может быть размещена в lending pool (Aave, Morpho, Compound);
  • часть — в LP позициях на DEX;
  • часть — в restaking или liquid vault более высокого порядка;
  • часть — в ликвидном стейкинге или fixed-income продуктах вроде Pendle PT.

Все эти размещения имеют свою временную шкалу возврата. Lending пул обычно позволяет забрать ликвидность мгновенно (если в пуле есть свободные средства). LP позицию нужно закрыть. Стейкинг — разморозить.

Vault управляет этим «слоёным пирогом» размещений и выдаёт пользователю общий балланс в виде vault shares. Но когда пользователь хочет вывести реальный актив, vault должен:

  1. определить, сколько shares принадлежит пользователю;
  2. оценить текущий запас свободной ликвидности в vault (cash);
  3. если cash хватает — выдать актив сразу;
  4. если cash не хватает — либо запустить withdraw queue, либо отказать с нулевым maxWithdraw.

maxWithdraw возвращает ровно столько, сколько vault может отдать прямо сейчас, не ожидая возврата средств из стратегий.

Когда все средства размещены, maxWithdraw равен нулю. Активы на месте, доходность начисляется, но вывести их нельзя до следующей ребалансировки, возврата ликвидности из стратегии или принудительного отзыва средств.

Где это становится проблемой

Для стратегии, которая держит средства в vault неделями без потребности в выводе, это вообще не проблема. Средства работают, vault делает своё дело.

Проблема возникает в трёх сценариях:

1. Требуется оперативный вывод по условиям risk policy

Допустим, treasury разместила часть средств в yield-стратегии, но по риск-политике должна иметь возможность отозвать их при определённом движении рынка. Если vault в этот момент показывает maxWithdraw = 0, политика формально нарушена — средства есть, но доступа к ним нет.

2. Vault периодически освобождает ликвидность, нужно успеть

Некоторые vaults регулярно проводят ребалансировку — возвращают часть средств из стратегий обратно в cash. В этот момент maxWithdraw становится положительным, но ненадолго: другие пользователи и боты тоже мониторят vault и могут забрать ликвидность быстрее.

Ручной мониторинг здесь не работает. У вас может быть несколько окон в сутки, когда можно вывести средства, и каждое окно длится минуты или даже секунды.

3. Vault с очередью на вывод

В некоторых архитектурах (например, vaults с withdraw queue) вывод не может быть мгновенным по определению. Пользователь встаёт в очередь и ждёт, пока vault аккумулирует достаточно свободной ликвидности. В этом случае maxWithdraw тоже может быть нулевым, пока очередь не дойдёт до этого пользователя.

Что делает maxWithdraw бот

Идея максимально простая: вместо того чтобы вручную проверять vault раз в несколько часов или полагаться на дашборды, ставится бот, который:

  • подписан на новые блоки через WebSocket;
  • на каждом блоке вызывает maxWithdraw для своего адреса;
  • если результат превышает настроенный порог — симулирует транзакцию вывода;
  • если симуляция успешна — отправляет реальную транзакцию с рассчитанными gas параметрами;
  • если ликвидность уже забрали — просто ждёт следующего блока.

Архитектурно это выглядит так:

Monitoring layer:

  • WebSocket-подключение к Ethereum mainnet RPC;
  • подписка на событие block;
  • вызов vault.maxWithdraw(address) на каждый блок;
  • сравнение с порогом minWithdrawUsdc.

Execution layer:

  • применение safety buffer (например, 0.5% от maxWithdraw), чтобы снизить риск конкурентного вывода;
  • staticCall симуляция транзакции — если кто-то уже забрал ликвидность, симуляция упадёт, и бот пропустит этот блок без затрат на gas;
  • EIP-1559 gas management: расчёт maxFeePerGas с мультипликатором от baseFee, настраиваемая priority fee;
  • отправка транзакции с buffer по gas limit (+20% к оценке).

Resilience layer:

  • обработка WebSocket разрывов с exponential backoff переподключением (до 10 попыток);
  • флаг txPending, предотвращающий отправку второй транзакции, пока первая не подтверждена или не отклонена.

Почему buffer Bps — не просто перестраховка

Если два withdraw-бота мониторят один и тот же vault и оба видят положительный maxWithdraw, первый успешный withdraw может исчерпать доступную ликвидность. Второй бот отправит транзакцию, которая revert-нется, и потеряет gas.

Safety buffer в 0.5% (50 bps) означает, что бот выводит не всё, что доступно, а на 0.5% меньше. Это не защищает от конкуренции полностью, но:

  • снижает вероятность, что withdraw упрётся в точный лимит пула;
  • даёт небольшой запас на изменение состояния между симуляцией и исполнением;
  • уменьшает число неудачных транзакций.

Если buffer слишком большой, бот будет оставлять ликвидность, которую заберут другие. Если слишком маленький — возрастёт число revert-ов. 50 bps — рабочий компромисс, но значение зависит от волатильности пула и числа конкурентов.

Когда такой бот нужен, а когда нет

Бот для автоматического вывода оправдан, когда:

  • vault работает с ограниченной свободной ликвидностью (большая часть средств в стратегиях);
  • окна доступности вывода короткие и нерегулярные;
  • вручную отслеживать их невозможно или неэффективно;
  • есть несколько vaults или адресов, которые нужно мониторить одновременно.

Бот избыточен, когда:

  • vault поддерживает мгновенный вывод в любой момент (всегда достаточно cash);
  • вывод средств — редкая операция без привязки к времени;
  • размер позиции настолько мал, что gas за мониторинг превышает потенциальную выгоду от своевременного вывода.

Практический пример: Euler EVK vault

В качестве конкретного примера — vault на базе Euler EVK, работающий по стандарту ERC-4626. vault размещает USDC в набор стратегий, и большая часть средств постоянно находится в работе. Свободный cash появляется периодически — при ребалансировках, погашении позиций или при выводе крупных пользователей.

Бот:

  1. подключается к vault через WebSocket;
  2. на каждый блок вызывает maxWithdraw(vaultAddress);
  3. при превышении порога 1 000 USDC симулирует withdraw через staticCall;
  4. применяет буфер 50 bps к сумме;
  5. оценивает gas, рассчитывает EIP-1559 параметры (2x от baseFee, priority fee 1.5 Gwei);
  6. отправляет транзакцию, логирует результат.

После вывода бот продолжает мониторинг на случай, если появится ещё ликвидность.

Dry-run режим позволяет сначала протестировать логику без отправки реальных транзакций — просто наблюдать, как часто maxWithdraw превышает порог, сколько времени занимает симуляция, какие gas параметры актуальны.

Что даёт такой подход команде или бизнесу

Вместо ручного мониторинга vault дашбордов и угадывания «когда же появится ликвидность» команда получает предсказуемый процесс:

  • средства выводятся автоматически, как только это становится возможным;
  • окна ликвидности не пропускаются;
  • каждая попытка вывода логируется с причиной успеха или отказа;
  • газ тратится только на реальные транзакции, симуляции бесплатны;
  • параметры (порог, буфер, gas) настраиваются под конкретный vault и риск-профиль.

Для treasury-отдела или DeFi-продукта, где vaults — часть операционной инфраструктуры, это не «ещё один бот», а недостающий элемент между размещением капитала и контролем над ликвидностью.

Что можно построить вокруг этой механики

Как отдельный сервис или automation слой, поверх базового maxWithdraw-бота можно реализовать:

  • мониторинг нескольких vaults из одного экземпляра с роутингом по порогам;
  • интеграцию с alerting (Telegram, Slack, PagerDuty) при успешном выводе или при превышении лимита неудачных попыток;
  • policy engine, который решает, выводить ли средства сейчас или подождать более выгодных gas условий;
  • reconciliation между ожидаемым и фактически выведенным объёмом;
  • multi-wallet execution для treasury с несколькими стратегиями;
  • исторические логи с данными по каждому блоку (maxWithdraw, результат симуляции, причина пропуска) для аналитики.

Итог

maxWithdraw, равный нулю, — это не баг и не поломка. Это штатное поведение vault, у которого большая часть капитала работает в стратегиях, а не лежит без дела.

Проблема возникает, когда такой vault становится частью операционной инфраструктуры, а не просто «положил и забыл». Для treasury, DeFi-продукта или сервиса с регулярными движениями средств нулевой maxWithdraw — это операционный лимит, который нужно отслеживать и обрабатывать.

Бот, который блок за блоком проверяет доступность вывода и исполняет его при появлении ликвидности, превращает эту неопределённость в предсказуемый процесс. Без ручного мониторинга, без пропущенных окон, без лишнего газа на неудачные попытки.

Если вы управляете vault-позициями, treasury или DeFi-продуктом, где своевременный вывод средств — часть операционного процесса, и хотите автоматизировать этот слой без потери контроля над ликвидностью — свяжитесь со мной. Разберём вашу архитектуру vaults, настроим мониторинг, execution и policy layer под конкретные стратегии и риск-профиль.