Вразливість Reentrancy: Як Виявити, Використати та Запобігти

У світі смарт-контрактів, reentrancy вважається однією з найнебезпечніших вразливостей. Ця стаття допоможе вам не лише зрозуміти, що таке атака reentrancy, а й ефективно захиститися від неї. Від базових технік до просунутих рішень — ми дослідимо способи захисту всього вашого проекту.

Як працює Reentrancy: Механізм базової атаки

Щоб зрозуміти reentrancy, спершу потрібно засвоїти основну концепцію: смарт-контракт може викликати інший контракт, і тоді другий контракт може викликати назад перший, поки він ще виконується.

Уявіть, у вас є два контракти: ContractA з 10 Ether і ContractB, який надіслав туди 1 Ether. Коли ContractB викликає функцію зняття, він перевіряє баланс. Якщо достатньо — Ether повертається назад у ContractB. Тут, без належних заходів захисту, саме цей момент стає вразливим для зловмисника.

У типовій атаці reentrancy зловмисник використовує дві функції: attack() для запуску атаки і fallback() — для повторних викликів. Функція fallback — особлива у Solidity: вона не має імені, параметрів і викликається автоматично, коли на контракт надходить Ether без додаткових даних.

Послідовність атаки reentrancy

Розглянемо процес по кроках. Зловмисник викликає attack() у своєму контракті. В цій функції він викликає withdraw() з ContractA.

Коли ContractA отримує цей виклик, він перевіряє баланс ContractB. Оскільки там 1 Ether, перевірка проходить. Тоді ContractA відправляє 1 Ether назад у ContractB, активуючи його fallback(). На цей момент ContractA має 9 Ether, але баланс ContractB у реєстрі ще не оновлений до 0.

Це — вразливий момент: fallback() знову викликає withdraw() з ContractA. Перевірка балансу показує, що там ще 1 Ether! Чому? Тому що рядок balance[msg.sender] = 0 ще не виконався — він йде після відправки Ether.

Цикл повторюється: виклик withdraw() — перевірка балансу — відправка Ether — fallback() — виклик withdraw() знову і так далі, доки всі кошти ContractA не будуть виведені.

Аналіз коду: коли reentrancy стає реальністю

Контракт EtherStore — класичний приклад уразливого контракту. Він має deposit() для внесення балансу і withdrawAll() для зняття всіх коштів. Проблема у тому, що withdrawAll() спочатку перевіряє умови, потім відправляє Ether, і лише потім оновлює баланс.

Зловмисник створює Attack, передаючи адресу EtherStore у конструктор. Його fallback() викликає повторно withdrawAll() щоразу, коли Ether надходить. attack() починає з першого внеску — 1 Ether — щоб пройти початкову перевірку.

В результаті, весь фонд EtherStore знімається за один транзакцій.

Три стратегії захисту від reentrancy

Щоб захистити смарт-контракти, існує три рівні протидії — від простого до комплексного.

Модель noReentrant: базове рішення

Найпростіший спосіб — використання модифікатора noReentrant(). Це особливий тип функції в Solidity, який дозволяє змінювати поведінку інших функцій без їх переписування.

Ідея: коли функція захищена noReentrant(), вона блокує контракт під час виконання. Будь-який повторний виклик цієї функції буде провалений через стан блокування. Після завершення функції і зняття блокування — інші виклики знову можливі.

Це ефективно для захисту однієї функції, але не підходить для складних сценаріїв.

Модель Check-Effect-Interaction: запобігання багатофункціональним reentrancy

Другий, більш потужний метод — застосування шаблону Check-Effect-Interaction. Замість захисту однієї функції, цей підхід змінює логіку її написання.

Принцип: спочатку перевіряємо умови (Check), одразу оновлюємо стан (Effect), і лише потім взаємодіємо з зовнішніми контрактами (Interaction). Це унеможливлює повторний виклик, бо баланс вже оновлений до 0.

Замість оновлення balance[msg.sender] = 0 після відправки Ether, його потрібно зробити перед цим. Тоді, навіть якщо fallback() викликає знову, перевірка балансів не пройде — вони вже оновлені.

Цей підхід захищає контракт від reentrancy у всіх функціях зняття.

GlobalReentrancyGuard: комплексний захист у масштабі проекту

Для великих проектів з багатьма контрактами потрібен більш глобальний підхід — GlobalReentrancyGuard. Це контракт, що зберігає глобальний стан блокування і всі інші контракти в проекті посилаються до нього.

Уявіть сценарій: зловмисник викликає функцію ScheduledTransfer, яка проходить перевірки і надсилає Ether. Потім AttackTransfer викликає fallback() і намагається повторно викликати ScheduledTransfer. Але через глобальний замок, цей виклик блокується.

Такий підхід особливо корисний для великих систем, де reentrancy може виникнути між різними контрактами.

Вибір відповідної техніки для вашого проекту

Вибір залежить від складності вашого проекту. Якщо у вас мало функцій взаємодії — достатньо noReentrant(). Якщо багато функцій зняття — застосовуйте Check-Effect-Interaction. Для масштабних систем з багатьма контрактами — GlobalReentrancyGuard.

Головне — розуміти, як працює reentrancy, щоб вміти його виявляти і запобігати заздалегідь.

Щоб щодня бути в курсі безпеки смарт-контрактів, слідкуйте за джерелами з актуальними кодами та новинами у сфері Web3 і Solidity безпеки.

Переглянути оригінал
Ця сторінка може містити контент третіх осіб, який надається виключно в інформаційних цілях (не в якості запевнень/гарантій) і не повинен розглядатися як схвалення його поглядів компанією Gate, а також як фінансова або професійна консультація. Див. Застереження для отримання детальної інформації.
  • Нагородити
  • Прокоментувати
  • Репост
  • Поділіться
Прокоментувати
Додати коментар
Додати коментар
Немає коментарів
  • Закріпити