История безопасности Ethereum делится на два этапа: до взлома The DAO в 2016 году и после. Тогда атака повторного входа (Reentrancy) привела к хардфорку сети и появлению Ethereum Classic.
Спустя годы разработчики успокоились. Считается, что модификатор nonReentrant и паттерн Checks-Effects-Interactions полностью решили проблему. Это ошибка. Reentrancy эволюционировала. Сегодня мы видим атаки через стандарты токенов (ERC-777) и манипуляции с чтением данных (Read-only Reentrancy), которые обходят стандартные защиты.
1. Классика: Как это работает Механика базовой атаки проста. Смарт-контракт сначала отправляет ETH пользователю, и только потом обновляет его баланс.
Атакующий создает вредоносный контракт. В момент получения средств срабатывает функция fallback или receive, которая снова вызывает функцию вывода средств жертвы. Поскольку баланс еще не обновлен (средства не списаны), контракт жертвы снова отправляет деньги. Цикл повторяется до полного опустошения казны.
Решение: Паттерн Checks-Effects-Interactions (сначала меняем состояние, потом отправляем деньги) и модификатор nonReentrant.
2. Атака через токены: Угроза ERC-777 Многие думают, что Reentrancy возможна только с нативным ETH. Однако стандарты токенов ERC-777 и ERC-1155 содержат хуки (hooks) — функции, которые вызываются при получении токенов.
Сценарий: Вы пишете функцию депозита токенов в свой протокол. Вы используете transferFrom. В стандарте ERC-777 функция transferFrom вызывает tokensReceived на адресе получателя и отправителя. Если атакующий отправляет токены на ваш контракт, управление передается ему до завершения транзакции. Он может снова вызвать функцию депозита или вывода, нарушая логику учета долей.
Вывод: Даже если вы используете ERC-20, всегда проверяйте, не является ли токен прокси для ERC-777. Используйте nonReentrant на всех функциях, меняющих состояние.
3. Read-only Reentrancy: Невидимый враг Это самый современный и опасный вектор, который стоил протоколам миллионы долларов (кейсы Curve, Balancer). Проблема возникает, когда nonReentrant защищает функции записи (write), но оставляет открытыми функции чтения (view).
Механика атаки:
Как защититься Обычный nonReentrant здесь бесполезен, так как он не блокирует функции view. Решение:
Reentrancy — это архитектурная уязвимость, а не баг кода. Она эксплуатирует саму природу внешних вызовов в Ethereum. Если ваш протокол взаимодействует с другими контрактами или использует сложные стандарты токенов, полагаться только на nonReentrant недостаточно. Безопасность требует полного контроля над потоком исполнения транзакции.
Спустя годы разработчики успокоились. Считается, что модификатор nonReentrant и паттерн Checks-Effects-Interactions полностью решили проблему. Это ошибка. Reentrancy эволюционировала. Сегодня мы видим атаки через стандарты токенов (ERC-777) и манипуляции с чтением данных (Read-only Reentrancy), которые обходят стандартные защиты.
1. Классика: Как это работает Механика базовой атаки проста. Смарт-контракт сначала отправляет ETH пользователю, и только потом обновляет его баланс.
Атакующий создает вредоносный контракт. В момент получения средств срабатывает функция fallback или receive, которая снова вызывает функцию вывода средств жертвы. Поскольку баланс еще не обновлен (средства не списаны), контракт жертвы снова отправляет деньги. Цикл повторяется до полного опустошения казны.
Решение: Паттерн Checks-Effects-Interactions (сначала меняем состояние, потом отправляем деньги) и модификатор nonReentrant.
2. Атака через токены: Угроза ERC-777 Многие думают, что Reentrancy возможна только с нативным ETH. Однако стандарты токенов ERC-777 и ERC-1155 содержат хуки (hooks) — функции, которые вызываются при получении токенов.
Сценарий: Вы пишете функцию депозита токенов в свой протокол. Вы используете transferFrom. В стандарте ERC-777 функция transferFrom вызывает tokensReceived на адресе получателя и отправителя. Если атакующий отправляет токены на ваш контракт, управление передается ему до завершения транзакции. Он может снова вызвать функцию депозита или вывода, нарушая логику учета долей.
Вывод: Даже если вы используете ERC-20, всегда проверяйте, не является ли токен прокси для ERC-777. Используйте nonReentrant на всех функциях, меняющих состояние.
3. Read-only Reentrancy: Невидимый враг Это самый современный и опасный вектор, который стоил протоколам миллионы долларов (кейсы Curve, Balancer). Проблема возникает, когда nonReentrant защищает функции записи (write), но оставляет открытыми функции чтения (view).
Механика атаки:
- Атакующий входит в пул ликвидности и начинает обмен (swap) или удаление ликвидности.
- В процессе обмена балансы токенов в пуле уже изменились, но финализация транзакции еще не прошла.
- Атакующий использует fallback, чтобы перехватить управление. В этот момент он не входит обратно в тот же контракт (там стоит защита).
- Вместо этого он идет в другой протокол (например, лендинг), который использует уязвимый пул как оракул цены.
- Лендинг-протокол вызывает getVirtualPrice у пула. Так как пул находится в промежуточном состоянии (деньги ушли, но пересчет не окончен), он возвращает некорректную, заниженную или завышенную цену.
- Атакующий берет огромный займ под залог «дешевого» актива или ликвидирует чужие позиции по искусственной цене.
Как защититься Обычный nonReentrant здесь бесполезен, так как он не блокирует функции view. Решение:
- Использовать паттерн «Reentrancy Guard» и для view-функций (хотя это повышает gas costs).
- Внедрять проверку входа в самой архитектуре: если контракт находится в процессе изменения состояния, он должен запрещать чтение критических данных (цена, резервы).
Reentrancy — это архитектурная уязвимость, а не баг кода. Она эксплуатирует саму природу внешних вызовов в Ethereum. Если ваш протокол взаимодействует с другими контрактами или использует сложные стандарты токенов, полагаться только на nonReentrant недостаточно. Безопасность требует полного контроля над потоком исполнения транзакции.