Реентрансабельность — атака, которая обманывает смарт-контракт — что это простыми словами | IceAI

Реентрансабельность — атака, которая обманывает смарт-контракт

Суть reentrancy: хакер вызывает функцию контракта до того, как баланс обновится. Логика и защита.

Обновлено: 2026-08-13

В 2016 году атакующий унёс из проекта The DAO 3,6 миллиона ETH — примерно $50 млн по курсу того дня. Способ не был сложным и не требовал взлома ключей. Контракт честно отдал деньги. Просто атакующий заставил его отдать их дважды. Речь о реентрансабельности (reentrancy) — одном из старейших и до сих пор живущих классов багов в смарт-контрактах.

Реентрансабельность: повторный вход в смарт-контракт Жертва balance = 10 ETH balance = 0 ⚠ обновится позже withdraw() в работе 1. выведи 1 ETH 4. ещё раз, до update Атакующий fallback() ловит и снова зовёт withdraw контракт ещё не успел списать баланс всего: 10 звонков факт: баланс списан только один раз

Как это работает

В нормальном сценарии контракт «Жертва» хранит баланс пользователя, получает запрос на вывод и делает две вещи по очереди: пересылает эфир на кошелёк вызывающего и списывает сумму с внутреннего баланса. Реентрансабельность возникает, когда атакующий вклинивается между этими шагами.

У ethereum-кошельков есть «функция приёма денег» — fallback() (в новом синтаксисе receive()). Когда Жертва пересылает эфир на контракт-атакующий, этот контракт получает управление. Его fallback тут же снова зовёт withdraw у Жертвы. А та к этому моменту ещё не списала баланс — деньги ушли, но учётная запись об этом не знает. Кошелёк отдаёт ещё один эфир. И ещё. Пока стек вызовов не упрётся в лимит газа.

The DAO работал именно так: атакующий разворачивал контракт, вкладывал туда ETH, получал «долю» в DAO и дёргал функцию разделения, которая переводила ETH обратно. Перевод запускал fallback, тот звал разделение снова. Так повторялось, пока не кончался газ — за каждый проход атакующий получал сумму депозита заново, а внутренний баланс у Жертвы так и не доходил до обновления.

Какими бывают атаки

Перечисленный механизм — самый известный, но разновидностей больше. Если злоумышленник не зовёт ту же функцию, а зовёт другую публичную функцию до завершения первой — это cross-function reentrancy. Если он попадает в логику стороннего протокола через токен-перевод — cross-contract. Если повторно входит в constructor контракта при развёртывании — read-only, которая не крадёт деньги напрямую, но искривляет состояние.

Цифра, которую стоит держать в голове: до сих пор в рамках аудита на приличных проектах находят от одной до пяти reentrancy-уязвимостей в год. Баг жив, хотя паттерн известен с 2016-го.

Как защищаться

Симпатичного универсального решения нет, но практики устоялись.

Checks-Effects-Interactions — основной паттерн. Сначала проверить условия (достаточно ли баланса, разрешён ли вывод), потом сразу изменить внутреннее состояние контракта (уменьшить баланс в учётной системе), и только в самом конце — взаимодействовать с внешним контрактом. Если атакующий вклинится на этом последнем шаге, внутренний баланс уже списан, и повторный вызов уйдёт в отказ.

Эффект усилит ReentrancyGuard — обёртка, популярная в OpenZeppelin. Базируется на локальной переменной-замке: функция ставит флаг в начале, снимает в конце. Повторный вход видит флаг занятым и откатывает. Добавляет пару строк кода и немного газа на хранение состояния.

Для пулов, где логика хитрее баланса, применяют pull over push: вместо того чтобы контракт сам пересылал деньги контрагентам, он «помечает» их к выводу, а пользователи сами забирают свои токены отдельной транзакцией. Взять обратно пометку второй раз не получится — её уже нет.

В эпоху автоматических аудитов (Slither, Mythril, Echidna) тривиальную реентрансабельность ловят статически. Но cross-function и read-only требуют ручного разбора — автомат плохо отличает «плохой» повторный вход от легитимного колбэка.

Почему это всё ещё актуально

После The DAO разработка стала осторожнее, и «голый» повторный вход на простом контракте-копилке — редкость. Но протоколы усложнились. Несколько контрактов, связанных через токены и оракулы, часто наследуют чужие зависимости. В июле 2023 года платформа Curve потеряла около $70 млн из-за бага в компиляторе Vyper — ошибка в блокировке реентрансабельности в конкретной версии языка позволила повторно войти в пул. Источник реентрансабельности сместился с «контракт-копилка» на «протокол, использующий другой протокол» — и закрыть его простой блокировкой уже нельзя.

Коротко

  • Реентрансабельность — повторный вызов функции контракта до того, как внутренний баланс обновится.
  • Классический пример — The DAO, 3,6 млн ETH ушли за ~$50 млн по курсу того дня.
  • Лечится паттерном Checks-Effects-Interactions, ReentrancyGuard-замком или pull-платежами.
  • Современные случаи — cross-contract и read-only; простой статический анализ их не видит.
  • Лучшая защита — комбинированная: guard + разделение проверки и перевода + аудит.

Остались вопросы? Спросите ИИ-ассистента — объяснит другими словами.

Спросить