Перейти до основного контенту
Change page

Безпека смартконтракту

Останні оновлення сторінки: 14 лютого 2026 р.

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

Публічні блокчейни, як-от Ethereum, ще більше ускладнюють проблему забезпечення безпеки смарт-контрактів. Розгорнутий код контракту зазвичай неможливо змінити, щоб виправити недоліки безпеки, а активи, викрадені зі смарт-контрактів, надзвичайно важко відстежити і переважно неможливо повернути через їхню незмінність.

Хоча цифри різняться, за оцінками, загальна сума вартості, вкраденої або втраченої через дефекти безпеки в смарт-контрактах, легко перевищує 1 мільярд доларів. Це включає гучні інциденти, як-от злам DAOopens in a new tab (вкрадено 3,6 млн ETH, що за сьогоднішніми цінами становить понад 1 млрд доларів), злам гаманця з мультипідписом Parityopens in a new tab (30 млн доларів, втрачених через хакерів), та проблему замороженого гаманця Parityopens in a new tab (понад 300 млн доларів в ETH, заблокованих назавжди).

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

Передумови

Перш ніж займатися безпекою, переконайтеся, що ви знайомі з основами розробки смарт-контрактів.

Рекомендації щодо створення безпечних смарт-контрактів Ethereum

1. Розробка належних засобів контролю доступу

У смарт-контрактах функції, позначені як public або external, можуть викликатися будь-якими обліковими записами, що належать зовнішнім власникам (EOA), або обліковими записами контрактів. Вказання публічної видимості для функцій є необхідним, якщо ви хочете, щоб інші могли взаємодіяти з вашим контрактом. Проте функції, позначені як private, можуть викликатися лише функціями всередині смарт-контракту, а не зовнішніми обліковими записами. Надання кожному учаснику мережі доступу до функцій контракту може спричинити проблеми, особливо якщо це означає, що будь-хто може виконувати конфіденційні операції (наприклад, карбування нових токенів).

Щоб запобігти несанкціонованому використанню функцій смарт-контракту, необхідно впровадити безпечні засоби контролю доступу. Механізми контролю доступу обмежують можливість використання певних функцій у смарт-контракті для затверджених суб’єктів, таких як облікові записи, відповідальні за керування контрактом. Патерн Ownable та контроль на основі ролей — це два патерни, корисні для реалізації контролю доступу в смарт-контрактах:

Патерн Ownable

У патерні Ownable адреса встановлюється як «власник» контракту під час процесу створення контракту. Захищеним функціям присвоюється модифікатор OnlyOwner, який гарантує, що контракт автентифікує особу адреси, що викликає, перед виконанням функції. Виклики захищених функцій з інших адрес, окрім власника контракту, завжди скасовуються, що запобігає небажаному доступу.

Контроль доступу на основі ролей

Реєстрація однієї адреси як Owner у смарт-контракті створює ризик централізації та є єдиною точкою відмови. Якщо ключі облікового запису власника скомпрометовані, зловмисники можуть атакувати контракт, що йому належить. Ось чому використання патерну контролю доступу на основі ролей із кількома адміністративними обліковими записами може бути кращим варіантом.

При контролі доступу на основі ролей доступ до конфіденційних функцій розподіляється між набором довірених учасників. Наприклад, один обліковий запис може відповідати за карбування токенів, а інший — за оновлення або призупинення контракту. Така децентралізація контролю доступу усуває єдині точки відмови та зменшує припущення щодо довіри для користувачів.

Використання гаманців з мультипідписом

Іншим підходом до реалізації безпечного контролю доступу є використання облікового запису з мультипідписом для керування контрактом. На відміну від звичайного EOA, облікові записи з мультипідписом належать кільком суб’єктам і вимагають підписів від мінімальної кількості облікових записів — скажімо, 3 із 5 — для виконання транзакцій.

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

2. Використовуйте оператори require(), assert() і revert() для захисту операцій контракту

Як уже згадувалося, будь-хто може викликати публічні функції у вашому смарт-контракті після його розгортання в блокчейні. Оскільки ви не можете заздалегідь знати, як зовнішні облікові записи будуть взаємодіяти з контрактом, ідеально впровадити внутрішні заходи захисту від проблемних операцій перед розгортанням. Ви можете забезпечити правильну поведінку в смарт-контрактах, використовуючи оператори require(), assert() і revert(), щоб викликати винятки та скасовувати зміни стану, якщо виконання не відповідає певним вимогам.

require(): require визначаються на початку функцій і гарантують, що попередньо визначені умови виконані до виконання викликаної функції. Оператор require може використовуватися для перевірки введених користувачем даних, перевірки змінних стану або автентифікації особистості облікового запису, що викликає, перш ніж продовжувати роботу з функцією.

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

revert(): revert() можна використовувати в операторі if-else, який викликає виняток, якщо необхідна умова не виконується. Наведений нижче приклад контракту використовує revert() для захисту виконання функцій:

1pragma solidity ^0.8.4;
2
3contract VendingMachine {
4 address owner;
5 error Unauthorized();
6 function buy(uint amount) public payable {
7 if (amount > msg.value / 2 ether)
8 revert("Надано недостатньо Ether.");
9 // Виконайте покупку.
10 }
11 function withdraw() public {
12 if (msg.sender != owner)
13 revert Unauthorized();
14
15 payable(msg.sender).transfer(address(this).balance);
16 }
17}
Показати все

3. Тестуйте смарт-контракти та перевіряйте правильність коду

Незмінність коду, що виконується у віртуальній машині Ethereum, означає, що смарт-контракти вимагають вищого рівня оцінки якості на етапі розробки. Ретельне тестування вашого контракту та спостереження за будь-якими неочікуваними результатами значно покращить безпеку та захистить ваших користувачів у довгостроковій перспективі.

Звичайний метод полягає в написанні невеликих модульних тестів з використанням макетних даних, які контракт, як очікується, отримає від користувачів. Модульне тестування добре підходить для тестування функціональності певних функцій і перевірки того, що смарт-контракт працює, як очікувалося.

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

Кращий підхід — поєднати модульне тестування з тестуванням на основі властивостей, що виконується за допомогою статичного та динамічного аналізу. Статичний аналіз покладається на представлення низького рівня, такі як графи потоку керуванняopens in a new tab та абстрактні синтаксичні дереваopens in a new tab, для аналізу досяжних станів програми та шляхів виконання. Тим часом, методи динамічного аналізу, такі як фаззінг смарт-контрактівopens in a new tab, виконують код контракту з випадковими вхідними значеннями для виявлення операцій, що порушують властивості безпеки.

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

4. Попросіть про незалежну перевірку вашого коду

Після тестування вашого контракту, добре попросити інших перевірити вихідний код на наявність будь-яких проблем з безпекою. Тестування не виявить усіх недоліків у смарт-контракті, але отримання незалежної перевірки збільшує ймовірність виявлення вразливостей.

Аудити

Замовлення аудиту смарт-контракту є одним зі способів проведення незалежної перевірки коду. Аудитори відіграють важливу роль у забезпеченні того, щоб смарт-контракти були безпечними та вільними від дефектів якості та помилок у проєктуванні.

Проте, вам слід уникати розгляду аудитів як панацеї. Аудити смарт-контрактів не виявлять усіх помилок і переважно призначені для надання додаткового раунду перевірок, що може допомогти виявити проблеми, пропущені розробниками під час початкової розробки та тестування. Ви також повинні дотримуватися найкращих практик роботи з аудиторами, таких як належне документування коду та додавання вбудованих коментарів, щоб отримати максимальну користь від аудиту смарт-контракту.

Програми винагород за виявлення помилок (баг-баунті)

Створення програми винагород за виявлення помилок — ще один підхід до впровадження зовнішніх перевірок коду. Винагорода за виявлення помилок (баг-баунті) — це фінансова винагорода, що надається особам (зазвичай «білим» хакерам), які виявляють уразливості в застосунку.

При правильному використанні програми винагород за помилки дають членам хакерської спільноти стимул перевіряти ваш код на наявність критичних недоліків. Реальним прикладом є «помилка нескінченних грошей», яка дозволила б зловмиснику створювати необмежену кількість ефіру на Optimismopens in a new tab, протоколі Шару 2, що працює на Ethereum. На щастя, «білий» хакер виявив недолікopens in a new tab і повідомив команду, заробивши при цьому велику винагородуopens in a new tab.

Корисною стратегією є встановлення розміру виплати за програмою винагород за помилки пропорційно до суми коштів, що перебувають під загрозою. Цей підхід, описаний як «програма масштабування винагород за помилкиopens in a new tab», надає фінансові стимули для осіб, щоб вони відповідально розкривали вразливості, а не використовували їх.

5. Дотримуйтесь найкращих практик під час розробки смарт-контрактів

Існування аудитів і програм винагород за помилки не звільняє вас від відповідальності писати високоякісний код. Хороша безпека смарт-контрактів починається з дотримання належних процесів проєктування та розробки:

  • Зберігайте весь код у системі контролю версій, такій як git

  • Робіть усі зміни в коді через pull-запити

  • Переконайтеся, що pull-запити мають принаймні одного незалежного рецензента — якщо ви працюєте над проєктом самостійно, розгляньте можливість знайти інших розробників і обмінюватися перевірками коду

  • Використовуйте середовище розробки для тестування, компіляції та розгортання смарт-контрактів

  • Проганяйте свій код через базові інструменти аналізу коду, такі як Cyfrin Aderynopens in a new tab, Mythril і Slither. В ідеалі, ви повинні робити це перед кожним злиттям pull-запиту та порівнювати відмінності у вихідних даних

  • Переконайтеся, що ваш код компілюється без помилок, і компілятор Solidity не видає попереджень

  • Належним чином документуйте свій код (використовуючи NatSpecopens in a new tab) і описуйте деталі архітектури контракту легкою для розуміння мовою. Це полегшить іншим аудит і перевірку вашого коду.

6. Впроваджуйте надійні плани аварійного відновлення

Розробка безпечних засобів контролю доступу, впровадження модифікаторів функцій та інші пропозиції можуть покращити безпеку смарт-контрактів, але вони не можуть виключити можливість зловмисних експлойтів. Створення безпечних смарт-контрактів вимагає «підготовки до невдач» і наявності резервного плану для ефективного реагування на атаки. Належний план аварійного відновлення включатиме деякі або всі з наступних компонентів:

Оновлення контрактів

Хоча смарт-контракти Ethereum за замовчуванням є незмінними, можна досягти певного ступеня змінюваності, використовуючи патерни оновлення. Оновлення контрактів необхідне у випадках, коли критичний недолік робить ваш старий контракт непридатним для використання, і розгортання нової логіки є найбільш доцільним варіантом.

Механізми оновлення контрактів працюють по-різному, але «проксі-патерн» є одним із найпопулярніших підходів до оновлення смарт-контрактів. Проксі-патерниopens in a new tab розділяють стан і логіку застосунку між двома контрактами. Перший контракт (називається «проксі-контракт») зберігає змінні стану (наприклад, баланси користувачів), а другий контракт (називається «логічний контракт») містить код для виконання функцій контракту.

Облікові записи взаємодіють з проксі-контрактом, який перенаправляє всі виклики функцій до логічного контракту за допомогою низькорівневого виклику delegatecall()opens in a new tab. На відміну від звичайного виклику повідомлення, delegatecall() гарантує, що код, який виконується за адресою логічного контракту, виконується в контексті контракту, що викликає. Це означає, що логічний контракт завжди буде записувати дані в сховище проксі (замість власного сховища), і початкові значення msg.sender та msg.value зберігаються.

Делегування викликів логічному контракту вимагає зберігання його адреси в сховищі проксі-контракту. Отже, оновлення логіки контракту — це лише питання розгортання іншого логічного контракту та зберігання нової адреси в проксі-контракті. Оскільки наступні виклики до проксі-контракту автоматично спрямовуються до нового логічного контракту, ви «оновили» б контракт, фактично не змінюючи код.

Детальніше про оновлення контрактів.

Аварійні зупинки

Як уже згадувалося, ретельний аудит і тестування не можуть виявити всі помилки в смарт-контракті. Якщо після розгортання у вашому коді з'являється вразливість, виправити її неможливо, оскільки ви не можете змінити код, що працює за адресою контракту. Крім того, механізми оновлення (наприклад, проксі-патерни) можуть вимагати часу для впровадження (вони часто вимагають схвалення від різних сторін), що лише дає зловмисникам більше часу, щоб завдати більшої шкоди.

Крайній варіант — це реалізувати функцію «аварійної зупинки», яка блокує виклики вразливих функцій у контракті. Аварійні зупинки зазвичай складаються з таких компонентів:

  1. Глобальна булева змінна, яка вказує, чи перебуває смарт-контракт у зупиненому стані. Ця змінна встановлюється у значення false під час налаштування контракту, але повертається до значення true після його зупинки.

  2. Функції, які посилаються на булеву змінну під час свого виконання. Такі функції доступні, коли смарт-контракт не зупинено, і стають недоступними, коли спрацьовує функція аварійної зупинки.

  3. Суб’єкт, що має доступ до функції аварійної зупинки, яка встановлює булеву змінну в значення true. Щоб запобігти зловмисним діям, виклики цієї функції можна обмежити довіреною адресою (наприклад, власником контракту).

Щойно контракт активує аварійну зупинку, певні функції не можна буде викликати. Це досягається шляхом обгортання вибраних функцій у модифікатор, який посилається на глобальну змінну. Нижче наведено прикладopens in a new tab, що описує реалізацію цього патерна в контрактах:

1// Цей код не пройшов професійний аудит і не гарантує безпеку або правильність. Використовуйте на свій страх і ризик.
2
3contract EmergencyStop {
4
5 bool isStopped = false;
6
7 modifier stoppedInEmergency {
8 require(!isStopped);
9 _;
10 }
11
12 modifier onlyWhenStopped {
13 require(isStopped);
14 _;
15 }
16
17 modifier onlyAuthorized {
18 // Перевірка авторизації msg.sender тут
19 _;
20 }
21
22 function stopContract() public onlyAuthorized {
23 isStopped = true;
24 }
25
26 function resumeContract() public onlyAuthorized {
27 isStopped = false;
28 }
29
30 function deposit() public payable stoppedInEmergency {
31 // Логіка депозиту тут
32 }
33
34 function emergencyWithdraw() public onlyWhenStopped {
35 // Аварійне зняття коштів тут
36 }
37}
Показати все

Цей приклад показує основні риси аварійних зупинок:

  • isStopped — це булева змінна, яка на початку має значення false, а коли контракт переходить в аварійний режим, — true.

  • Модифікатори функцій onlyWhenStopped і stoppedInEmergency перевіряють змінну isStopped. stoppedInEmergency використовується для контролю функцій, які мають бути недоступними, коли контракт є вразливим (наприклад, deposit()). Виклики цих функцій буде просто скасовано.

onlyWhenStopped використовується для функцій, які можна викликати під час надзвичайної ситуації (наприклад, emergencyWithdraw()). Такі функції можуть допомогти вирішити ситуацію, тому їх виключають зі списку «обмежених функцій».

Використання функціональності аварійної зупинки забезпечує ефективний тимчасовий захід для боротьби із серйозними вразливостями у вашому смарт-контракті. Однак це підвищує потребу користувачів довіряти розробникам, що вони не активують її з корисливих міркувань. Для цього можливими рішеннями є децентралізація контролю над аварійною зупинкою шляхом підпорядкування її механізму голосування onchain, timelock або схвалення з гаманця з мультипідписом (multisig).

Моніторинг подій

Подіїopens in a new tab дозволяють відстежувати виклики функцій смарт-контракту та контролювати зміни змінних стану. В ідеалі запрограмувати свій смарт-контракт так, щоб він генерував подію щоразу, коли якась сторона виконує критично важливу для безпеки дію (наприклад, виведення коштів).

Реєстрація подій та їх моніторинг offchain дає уявлення про операції контракту та допомагає швидше виявляти зловмисні дії. Це означає, що ваша команда може швидше реагувати на зломи та вживати заходів для пом'якшення наслідків для користувачів, як-от призупинення функцій або виконання оновлення.

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

7. Проєктуйте безпечні системи управління

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

Децентралізоване управління може бути корисним, особливо тому, що воно узгоджує інтереси розробників і кінцевих користувачів. Тим не менш, механізми управління смарт-контрактами можуть створювати нові ризики, якщо вони реалізовані неправильно. Імовірний сценарій — якщо зловмисник отримує величезну силу голосу (вимірюється кількістю утримуваних токенів), взявши швидку позику, і просуває зловмисну пропозицію.

Одним із способів запобігання проблемам, пов’язаним з ончейн-управлінням, є використання тимчасового блокуванняopens in a new tab. Тимчасове блокування не дозволяє смарт-контракту виконувати певні дії, доки не мине певний проміжок часу. Інші стратегії включають присвоєння «ваги голосу» кожному токену на основі того, як довго він був заблокований, або вимірювання сили голосу адреси в історичний період (наприклад, 2-3 блоки в минулому), а не в поточному блоці. Обидва методи зменшують можливість швидкого накопичення сили голосу для впливу на результати ончейн-голосувань.

Більше про проєктування безпечних систем управлінняopens in a new tab, різні механізми голосування в DAOopens in a new tab та поширені вектори атак на DAO з використанням DeFiopens in a new tab за наведеними посиланнями.

8. Зведіть складність коду до мінімуму

Традиційні розробники програмного забезпечення знайомі з принципом KISS («keep it simple, stupid» — «роби це простіше, дурню»), який радить уникати введення непотрібної складності в дизайн програмного забезпечення. Це випливає з давнього уявлення про те, що «складні системи дають збій у складний спосіб» і є більш схильними до дорогих помилок.

Збереження простоти має особливе значення при написанні смарт-контрактів, враховуючи, що смарт-контракти потенційно контролюють великі обсяги вартості. Порада для досягнення простоти при написанні смарт-контрактів — це повторне використання існуючих бібліотек, таких як OpenZeppelin Contractsopens in a new tab, де це можливо. Оскільки ці бібліотеки були ретельно перевірені та протестовані розробниками, їх використання зменшує шанси на введення помилок шляхом написання нової функціональності з нуля.

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

9. Захист від поширених уразливостей смарт-контрактів

Повторний вхід (Reentrancy)

EVM не допускає паралелізму, що означає, що два контракти, залучені до виклику повідомлення, не можуть виконуватися одночасно. Зовнішній виклик призупиняє виконання та пам’ять контракту, що викликає, доки виклик не повернеться, після чого виконання продовжується в звичайному режимі. Цей процес можна формально описати як передачу потоку керуванняopens in a new tab іншому контракту.

Хоча здебільшого це нешкідливо, передача потоку керування недовіреним контрактам може спричинити проблеми, такі як повторний вхід. Атака повторного входу відбувається, коли зловмисний контракт викликає вразливий контракт до того, як завершиться початковий виклик функції. Цей тип атаки найкраще пояснити на прикладі.

Розглянемо простий смарт-контракт («Victim»), який дозволяє будь-кому вносити та знімати ефір:

1// Цей контракт вразливий. Не використовуйте в робочому середовищі
2
3contract Victim {
4 mapping (address => uint256) public balances;
5
6 function deposit() external payable {
7 balances[msg.sender] += msg.value;
8 }
9
10 function withdraw() external {
11 uint256 amount = balances[msg.sender];
12 (bool success, ) = msg.sender.call.value(amount)("");
13 require(success);
14 balances[msg.sender] = 0;
15 }
16}
Показати все

Цей контракт надає функцію withdraw() для того, щоб користувачі могли знімати ETH, раніше внесені в контракт. Під час обробки зняття коштів контракт виконує такі операції:

  1. Перевіряє баланс ETH користувача
  2. Надсилає кошти на адресу, що викликає
  3. Скидає їхній баланс до 0, запобігаючи додатковим зняттям коштів користувачем

Функція withdraw() у контракті Victim дотримується патерну «перевірки-взаємодії-ефекти». Він перевіряє, чи виконані умови, необхідні для виконання (тобто користувач має позитивний баланс ETH), і виконує взаємодію, надсилаючи ETH на адресу викликача, перш ніж застосовувати ефекти транзакції (тобто зменшувати баланс користувача).

Якщо withdraw() викликається з облікового запису, що належить зовнішньому власнику (EOA), функція виконується, як очікувалося: msg.sender.call.value() надсилає ETH викликачу. Однак, якщо msg.sender — це обліковий запис смарт-контракту, що викликає withdraw(), надсилання коштів за допомогою msg.sender.call.value() також запустить код, що зберігається за цією адресою.

Уявіть, що це код, розгорнутий за адресою контракту:

1 contract Attacker {
2 function beginAttack() external payable {
3 Victim(victim_address).deposit.value(1 ether)();
4 Victim(victim_address).withdraw();
5 }
6
7 function() external payable {
8 if (gasleft() > 40000) {
9 Victim(victim_address).withdraw();
10 }
11 }
12}
Показати все

Цей контракт призначений для виконання трьох дій:

  1. Приймати депозит з іншого облікового запису (ймовірно, EOA зловмисника)
  2. Вносити 1 ETH у контракт Victim
  3. Знімати 1 ETH, що зберігається в смарт-контракті

Тут немає нічого поганого, за винятком того, що Attacker має іншу функцію, яка знову викликає withdraw() у Victim, якщо залишок газу від вхідного msg.sender.call.value перевищує 40 000. Це дає Attacker можливість повторно увійти в Victim і зняти більше коштів до завершення першого виклику withdraw. Цикл виглядає так:

1- EOA зловмисника викликає `Attacker.beginAttack()` з 1 ETH
2- `Attacker.beginAttack()` вносить 1 ETH у `Victim`
3- `Attacker` викликає `withdraw()` в `Victim`
4- `Victim` перевіряє баланс `Attacker` (1 ETH)
5- `Victim` надсилає 1 ETH до `Attacker` (що запускає функцію за замовчуванням)
6- `Attacker` знову викликає `Victim.withdraw()` (зверніть увагу, що `Victim` не зменшив баланс `Attacker` після першого зняття коштів)
7- `Victim` перевіряє баланс `Attacker` (який все ще становить 1 ETH, оскільки він не застосував наслідки першого виклику)
8- `Victim` надсилає 1 ETH до `Attacker` (що запускає функцію за замовчуванням і дозволяє `Attacker` повторно увійти до функції `withdraw`)
9- Процес повторюється, доки у `Attacker` не закінчиться газ, після чого `msg.sender.call.value` повертається без запуску додаткових знять коштів
10- `Victim` нарешті застосовує результати першої транзакції (і наступних) до свого стану, тому баланс `Attacker` встановлюється на 0
Показати все

Підсумок полягає в тому, що оскільки баланс викликача не встановлюється на 0 до завершення виконання функції, наступні виклики будуть успішними і дозволять викликачу зняти свій баланс кілька разів. Цей вид атаки може бути використаний для викачування коштів зі смарт-контракту, як це сталося під час зламу DAO у 2016 роціopens in a new tab. Атаки повторного входу все ще є критичною проблемою для смарт-контрактів сьогодні, як показують публічні списки експлойтів повторного входуopens in a new tab.

Як запобігти атакам повторного входу

Одним із підходів до боротьби з повторним входом є дотримання патерну перевірки-ефекти-взаємодіїopens in a new tab. Цей патерн впорядковує виконання функцій таким чином, що код, який виконує необхідні перевірки перед продовженням виконання, йде першим, за ним іде код, який маніпулює станом контракту, а код, який взаємодіє з іншими контрактами або EOA, йде останнім.

Патерн перевірки-ефекти-взаємодії використовується в переглянутій версії контракту Victim, показаній нижче:

1contract NoLongerAVictim {
2 function withdraw() external {
3 uint256 amount = balances[msg.sender];
4 balances[msg.sender] = 0;
5 (bool success, ) = msg.sender.call.value(amount)("");
6 require(success);
7 }
8}

Цей контракт виконує перевірку балансу користувача, застосовує ефекти функції withdraw() (скидаючи баланс користувача до 0), і переходить до виконання взаємодії (надсилання ETH на адресу користувача). Це гарантує, що контракт оновлює своє сховище перед зовнішнім викликом, усуваючи умову повторного входу, яка уможливила першу атаку. Контракт Attacker все ще може викликати NoLongerAVictim, але оскільки balances[msg.sender] було встановлено на 0, додаткові зняття коштів спричинять помилку.

Інший варіант — використовувати блокування взаємного виключення (зазвичай описується як «м'ютекс»), яке блокує частину стану контракту до завершення виклику функції. Це реалізується за допомогою логічної змінної, яка встановлюється в true перед виконанням функції та повертається до false після завершення виклику. Як видно з прикладу нижче, використання м'ютекса захищає функцію від рекурсивних викликів, поки початковий виклик ще обробляється, ефективно зупиняючи повторний вхід.

1pragma solidity ^0.7.0;
2
3contract MutexPattern {
4 bool locked = false;
5 mapping(address => uint256) public balances;
6
7 modifier noReentrancy() {
8 require(!locked, "Заблоковано від повторного входу.");
9 locked = true;
10 _;
11 locked = false;
12 }
13 // Ця функція захищена м'ютексом, тому рекурсивні виклики з `msg.sender.call` не можуть знову викликати `withdraw`.
14 // Оператор `return` повертає `true`, але все ще виконує оператор `locked = false` у модифікаторі
15 function withdraw(uint _amount) public payable noReentrancy returns(bool) {
16 require(balances[msg.sender] >= _amount, "Немає балансу для зняття.");
17
18 balances[msg.sender] -= _amount;
19 (bool success, ) = msg.sender.call{value: _amount}("");
20 require(success);
21
22 return true;
23 }
24}
Показати все

Ви також можете використовувати систему pull-платежівopens in a new tab, яка вимагає від користувачів знімати кошти зі смарт-контрактів, замість системи «push-платежів», яка надсилає кошти на облікові записи. Це усуває можливість ненавмисного запуску коду за невідомими адресами (і може також запобігти певним атакам типу «відмова в обслуговуванні»).

Цілочисельні недоповнення та переповнення

Цілочисельне переповнення відбувається, коли результати арифметичної операції виходять за межі допустимого діапазону значень, змушуючи його «перекрутитися» до найменшого представленого значення. Наприклад, uint8 може зберігати значення лише до 2^8-1=255. Арифметичні операції, що призводять до значень, вищих за 255, переповняться і скинуть uint до 0, подібно до того, як одометр на автомобілі скидається до 0, досягнувши максимального пробігу (999999).

Цілочисельні недоповнення відбуваються з подібних причин: результати арифметичної операції падають нижче допустимого діапазону. Скажімо, ви спробували зменшити 0 у uint8, результат просто перекрутився б до максимального представленого значення (255).

Як цілочисельні переповнення, так і недоповнення можуть призвести до несподіваних змін змінних стану контракту та призвести до незапланованого виконання. Нижче наведено приклад, що показує, як зловмисник може використати арифметичне переповнення в смарт-контракті для виконання недійсної операції:

1pragma solidity ^0.7.6;
2
3// Цей контракт призначений для роботи як сховище часу.
4// Користувач може вносити кошти в цей контракт, але не може зняти їх принаймні протягом тижня.
5// Користувач також може продовжити час очікування понад 1-тижневий період очікування.
6
7/*
81. Розгорніть TimeLock
92. Розгорніть Attack з адресою TimeLock
103. Викличте Attack.attack, відправивши 1 ether. Ви зможете негайно
11 зняти свій ефір.
12
13Що сталося?
14Атака спричинила переповнення TimeLock.lockTime, і зловмисник зміг зняти кошти
15до закінчення 1-тижневого періоду очікування.
16*/
17
18contract TimeLock {
19 mapping(address => uint) public balances;
20 mapping(address => uint) public lockTime;
21
22 function deposit() external payable {
23 balances[msg.sender] += msg.value;
24 lockTime[msg.sender] = block.timestamp + 1 weeks;
25 }
26
27 function increaseLockTime(uint _secondsToIncrease) public {
28 lockTime[msg.sender] += _secondsToIncrease;
29 }
30
31 function withdraw() public {
32 require(balances[msg.sender] > 0, "Недостатньо коштів");
33 require(block.timestamp > lockTime[msg.sender], "Час блокування ще не минув");
34
35 uint amount = balances[msg.sender];
36 balances[msg.sender] = 0;
37
38 (bool sent, ) = msg.sender.call{value: amount}("");
39 require(sent, "Не вдалося надіслати Ether");
40 }
41}
42
43contract Attack {
44 TimeLock timeLock;
45
46 constructor(TimeLock _timeLock) {
47 timeLock = TimeLock(_timeLock);
48 }
49
50 fallback() external payable {}
51
52 function attack() public payable {
53 timeLock.deposit{value: msg.value}();
54 /*
55 якщо t = поточний час блокування, то нам потрібно знайти x, такий що
56 x + t = 2**256 = 0
57 отже x = -t
58 2**256 = type(uint).max + 1
59 отже x = type(uint).max + 1 - t
60 */
61 timeLock.increaseLockTime(
62 type(uint).max + 1 - timeLock.lockTime(address(this))
63 );
64 timeLock.withdraw();
65 }
66}
Показати все
Як запобігти цілочисельним недоповненням і переповненням

Починаючи з версії 0.8.0, компілятор Solidity відхиляє код, який призводить до цілочисельних недоповнень і переповнень. Однак контракти, скомпільовані за допомогою старішої версії компілятора, повинні або виконувати перевірки функцій, що включають арифметичні операції, або використовувати бібліотеку (наприклад, SafeMathopens in a new tab), яка перевіряє на недоповнення/переповнення.

Маніпуляція оракулами

Оракули отримують інформацію поза ланцюгом і надсилають її в ланцюг для використання смарт-контрактами. За допомогою оракулів ви можете створювати смарт-контракти, які взаємодіють із системами поза ланцюгом, такими як ринки капіталу, що значно розширює їх застосування.

Але якщо оракул пошкоджений і надсилає неправильну інформацію в ланцюг, смарт-контракти виконуватимуться на основі помилкових вхідних даних, що може спричинити проблеми. Це є основою «проблеми оракула», яка стосується завдання переконатися, що інформація від блокчейн-оракула є точною, актуальною та своєчасною.

Пов'язана проблема безпеки полягає у використанні ончейн-оракула, такого як децентралізована біржа, для отримання спотової ціни на актив. Платформи кредитування в галузі децентралізованих фінансів (DeFi) часто роблять це для визначення вартості застави користувача, щоб визначити, скільки він може позичити.

Ціни DEX часто є точними, значною мірою завдяки арбітражерам, які відновлюють паритет на ринках. Однак вони відкриті для маніпуляцій, особливо якщо ончейн-оракул розраховує ціни на активи на основі історичних торгових патернів (як це зазвичай буває).

Наприклад, зловмисник може штучно підняти спотову ціну активу, взявши швидку позику безпосередньо перед взаємодією з вашим контрактом кредитування. Запит до DEX щодо ціни активу поверне вищу, ніж зазвичай, вартість (через великий «ордер на купівлю» зловмисника, що спотворює попит на актив), що дозволить їм позичити більше, ніж вони повинні. Такі «атаки зі швидкими позиками» використовувалися для експлуатації залежності від цінових оракулів серед застосунків DeFi, що коштувало протоколам мільйони втрачених коштів.

Як запобігти маніпуляціям оракулами

Мінімальною вимогою для уникнення маніпуляцій оракуламиopens in a new tab є використання децентралізованої мережі оракулів, яка запитує інформацію з кількох джерел, щоб уникнути єдиних точок відмови. У більшості випадків децентралізовані оракули мають вбудовані криптоекономічні стимули, щоб заохочувати вузли оракулів повідомляти правильну інформацію, що робить їх більш безпечними, ніж централізовані оракули.

Якщо ви плануєте запитувати ончейн-оракул щодо цін на активи, розгляньте можливість використання такого, що реалізує механізм середньозваженої за часом ціни (TWAP). Оракул TWAPopens in a new tab запитує ціну активу в два різні моменти часу (які ви можете змінити) і розраховує спотову ціну на основі отриманого середнього значення. Вибір довших періодів часу захищає ваш протокол від маніпуляцій цінами, оскільки великі ордери, виконані нещодавно, не можуть вплинути на ціни активів.

Ресурси з безпеки смарт-контрактів для розробників

Інструменти для аналізу смарт-контрактів і перевірки правильності коду

  • Інструменти та бібліотеки для тестування - Збірка стандартних інструментів і бібліотек для виконання модульних тестів, статичного та динамічного аналізу смарт-контрактів.

  • Інструменти формальної верифікації - Інструменти для перевірки функціональної правильності в смарт-контрактах і перевірки інваріантів.

  • Послуги з аудиту смарт-контрактів - Список організацій, що надають послуги з аудиту смарт-контрактів для проєктів розробки Ethereum.

  • Платформи баг-баунті - Платформи для координації програм винагород за помилки та винагороди за відповідальне розкриття критичних уразливостей у смарт-контрактах.

  • Fork Checkeropens in a new tab - Безкоштовний онлайн-інструмент для перевірки всієї доступної інформації щодо форкнутого контракту.

  • ABI Encoderopens in a new tab - Безкоштовний онлайн-сервіс для кодування функцій вашого контракту Solidity та аргументів конструктора.

  • Aderynopens in a new tab - Статичний аналізатор Solidity, що обходить абстрактні синтаксичні дерева (AST) для виявлення підозрілих вразливостей і виведення проблем у легкому для сприйняття форматі markdown.

Інструменти для моніторингу смарт-контрактів

  • Tenderly Real-Time Alertingopens in a new tab - Інструмент для отримання сповіщень у реальному часі, коли на ваших смарт-контрактах або гаманцях відбуваються незвичайні або несподівані події.

Інструменти для безпечного адміністрування смарт-контрактів

  • Safeopens in a new tab - Гаманець на базі смарт-контракту, що працює на Ethereum і вимагає, щоб мінімальна кількість людей схвалила транзакцію, перш ніж вона відбудеться (M з N).

  • OpenZeppelin Contractsopens in a new tab - Бібліотеки контрактів для реалізації адміністративних функцій, включаючи володіння контрактом, оновлення, контроль доступу, управління, можливість призупинення тощо.

Послуги з аудиту смарт-контрактів

  • ConsenSys Diligenceopens in a new tab - Сервіс аудиту смарт-контрактів, що допомагає проєктам у всій блокчейн-екосистемі переконатися, що їхні протоколи готові до запуску та створені для захисту користувачів.

  • CertiKopens in a new tab - Фірма з безпеки блокчейну, що є піонером у використанні передових технологій формальної верифікації для смарт-контрактів і блокчейн-мереж.

  • Trail of Bitsopens in a new tab - Компанія з кібербезпеки, яка поєднує дослідження безпеки з мисленням зловмисника для зменшення ризиків і зміцнення коду.

  • PeckShieldopens in a new tab - Компанія з безпеки блокчейну, що пропонує продукти та послуги для безпеки, конфіденційності та зручності використання всієї екосистеми блокчейну.

  • QuantStampopens in a new tab - Сервіс аудиту, що сприяє масовому впровадженню технології блокчейн через послуги з оцінки безпеки та ризиків.

  • OpenZeppelinopens in a new tab - Компанія з безпеки смарт-контрактів, що надає аудити безпеки для розподілених систем.

  • Runtime Verificationopens in a new tab - Компанія з безпеки, що спеціалізується на формальному моделюванні та верифікації смарт-контрактів.

  • Hackenopens in a new tab - Аудитор кібербезпеки Web3, що пропонує 360-градусний підхід до безпеки блокчейну.

  • Nethermindopens in a new tab - Послуги аудиту Solidity та Cairo, що забезпечують цілісність смарт-контрактів та безпеку користувачів на Ethereum та Starknet.

  • HashExopens in a new tab - HashEx зосереджується на аудиті блокчейну та смарт-контрактів для забезпечення безпеки криптовалют, надаючи такі послуги, як розробка смарт-контрактів, тестування на проникнення, консалтинг у сфері блокчейну.

  • Code4renaopens in a new tab - Конкурентна платформа для аудитів, яка стимулює експертів з безпеки смарт-контрактів знаходити вразливості та допомагає зробити web3 більш безпечним.

  • CodeHawksopens in a new tab - Платформа для конкурентних аудитів, що проводить змагання з аудиту смарт-контрактів для дослідників безпеки.

  • Cyfrinopens in a new tab - Лідер у сфері безпеки Web3, що розвиває криптобезпеку через продукти та послуги з аудиту смарт-контрактів.

  • ImmuneBytesopens in a new tab - Фірма з безпеки Web3, що пропонує аудити безпеки для блокчейн-систем за допомогою команди досвідчених аудиторів та найкращих інструментів.

  • Oxorioopens in a new tab - Аудити смарт-контрактів та послуги з безпеки блокчейну з експертизою в EVM, Solidity, ZK, крос-чейн технологіях для криптофірм та проєктів DeFi.

  • Inferenceopens in a new tab - Компанія з аудиту безпеки, що спеціалізується на аудиті смарт-контрактів для блокчейнів на базі EVM. Завдяки своїм експертним аудиторам вони виявляють потенційні проблеми та пропонують дієві рішення для їх усунення перед розгортанням.

Платформи баг-баунті

  • Immunefiopens in a new tab - Платформа баг-баунті для смарт-контрактів і проєктів DeFi, де дослідники безпеки перевіряють код, розкривають уразливості, отримують винагороду та роблять криптосвіт безпечнішим.

  • HackerOneopens in a new tab - Платформа для координації вразливостей і баг-баунті, що з'єднує бізнеси з пентестерами та дослідниками кібербезпеки.

  • HackenProofopens in a new tab - Експертна платформа баг-баунті для криптопроєктів (DeFi, смарт-контракти, гаманці, CEX та інше), де фахівці з безпеки надають послуги тріажу, а дослідники отримують винагороду за релевантні, перевірені звіти про помилки.

  • Sherlockopens in a new tab - Андеррайтер у Web3 для безпеки смарт-контрактів, з виплатами для аудиторів, що керуються через смарт-контракти, щоб забезпечити справедливу оплату за відповідні помилки.

  • CodeHawksopens in a new tab - Конкурентна платформа баг-баунті, де аудитори беруть участь у конкурсах та змаганнях з безпеки, а (незабаром) і у власних приватних аудитах.

Публікації відомих уразливостей та експлойтів смарт-контрактів

  • ConsenSys: відомі атаки на смарт-контрактиopens in a new tab - Зрозуміле для початківців пояснення найзначніших уразливостей контрактів, з прикладами коду для більшості випадків.

  • Реєстр SWCopens in a new tab - Курований список елементів Common Weakness Enumeration (CWE), які застосовуються до смарт-контрактів Ethereum.

  • Rektopens in a new tab - Регулярно оновлювана публікація про гучні криптозлами та експлойти, разом із детальними звітами після інцидентів.

Завдання для вивчення безпеки смарт-контрактів

  • Awesome BlockSec CTFopens in a new tab - Курований список воєнних ігор з безпеки блокчейну, завдань та змагань Capture The Flagopens in a new tab та описів рішень.

  • Damn Vulnerable DeFiopens in a new tab - Воєнна гра для вивчення наступальної безпеки смарт-контрактів DeFi та розвитку навичок полювання на помилки та аудиту безпеки.

  • Ethernautopens in a new tab - Воєнна гра на базі Web3/Solidity, де кожен рівень — це смарт-контракт, який потрібно «зламати».

  • HackenProof x HackTheBoxopens in a new tab - Завдання зі зламу смарт-контрактів у стилі фентезійної пригоди. Успішне завершення завдання також дає доступ до приватної програми баг-баунті.

Найкращі практики для захисту смарт-контрактів

Посібники з безпеки смарт-контрактів

Чи була ця стаття корисною?