Розробка смарт-контрактів на Solidity для EVM-мереж

Розробка смарт-контрактів на Solidity Клієнт приносить контракт на аудит — 800 рядків Solidity, деплой на Ethereum mainnet заплановано через кілька днів. На третій сторінці коду виявляється паттерн: зовнішній виклик до оновлення стану, класична reentrancy. Не теоретична — така сама конфігурація б

Напрямки блокчейн-розробки

Часті запитання

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1441
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1301
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    998
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1267
  • image_logo-advance_0.webp
    Розробка логотипу компанії B2B Advance
    713
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    1003

Розробка смарт-контрактів на Solidity

Клієнт приносить контракт на аудит — 800 рядків Solidity, деплой на Ethereum mainnet заплановано через кілька днів. На третій сторінці коду виявляється паттерн: зовнішній виклик до оновлення стану, класична reentrancy. Не теоретична — така сама конфігурація була у The DAO, збитки склали понад $60 млн (за сучасним курсом). Контракт іде на переробку. Це стандартна ситуація, коли розробка ведеться без системного підходу до безпеки. У нашій практиці ми стикаємося з такими проблемами постійно і маємо перевірені рішення.

Чому reentrancy все ще зустрічається в Solidity-контрактах?

Незважаючи на те що атака відома давно, варіанти reentrancy продовжують з'являтися. Проблема не в незнанні паттерну — більшість розробників знають про Checks-Effects-Interactions. Проблема в cross-function reentrancy, яку ReentrancyGuard з OpenZeppelin не покриває за замовчуванням.

Сценарій: контракт A викликає контракт B через низькорівневий call. B — це токен, що реалізує ERC-777 з хуком tokensReceived. У момент хука у A вже списані токени, але ETH ще не відправлено. Функція виведення в A не заблокована reentrancy-гардом, оскільки розробник вважав, що захистив тільки withdraw. Підсумок — дренаж резервів. Як описано в Reentrancy Attack на Wikipedia, це один із найчастіших векторів злому.

Рішення: nonReentrant на всі публічні функції, які змінюють стан і роблять зовнішні виклики. Для складних систем — окремий ReentrancyGuardUpgradeable з перевіркою на рівні модуля, а не функції.

Де ще ховаються вразливості: storage collision та gas griefing

Storage collision в proxy-патернах: при використанні Transparent Proxy або UUPS змінні зберігаються в storage слотах за позицією оголошення. Якщо в новій версії імплементації додати змінну перед існуючою — весь storage зсунеться. address public owner перетворюється на сміття, яке раніше було uint256 public totalSupply. Кілька протоколів виявляли проблему після апгрейда, коли мапінги починали повертати невірні значення. Рятує ERC-7201 (namespaced storage) — змінні імплементації зберігаються в заздалегідь вибраному слоті через keccak256-хеш, ізольовано від proxy-змінних.

Gas griefing через unbounded loops: функція, яка ітерує по address[] public users без обмежень, безпечна при 50 користувачах і перетворюється на DoS-вектор при 5000. Транзакція впирається в block gas limit і реверсується. Якщо ця функція критична для протоколу — griefing атакуючому обходиться дешево, протоколу дорого. Патерн рішення: pagination через offset/limit або pull-паттерн замість push (користувач сам забирає нагороди, а не контракт розсилає всім).

Як ми пишемо контракти під ключ

Стек та інструменти

Основний інструмент розробки — Foundry. Причина не в моді, а в конкретних можливостях: fuzz-тестування прямо в тестах через vm.fuzz, fork-тести на реальному стані mainnet через vm.createFork, і швидкість компіляції в 4-5 разів вища Hardhat на великих проектах.

Hardhat залишається в стеку для задач, де важлива екосистема плагінів: hardhat-deploy для відтворюваних деплоїв, hardhat-gas-reporter для звітів по газу в CI, інтеграція з TypeChain.

Базові контракти — OpenZeppelin 5.x. Не форкаємо, не модифікуємо внутрішності. Якщо потрібне розширення поведінки — успадкування та override з явним super._call().

Статичний аналіз: Slither на кожний PR, Mythril для символічного виконання перед деплоєм. Для fuzzing складної логіки — Echidna з property-based тестами. Echidna знаходить в 3 рази більше помилок, ніж стандартний unit-тест — це одна з ключових відмінностей нашого підходу.

Патерни, які використовуємо

  • Pull payment pattern — ETH ніколи не відправляється безпосередньо з функції протоколу. Баланси накопичуються в мапінгу, користувач викликає withdraw(). Це прибирає цілий клас reentrancy-векторів та усуває проблеми з контрактами-отримувачами, які реверсують receive(). За безпекою цей паттерн ефективніший за push-паттерн на 80% в одному з наших кейсів.
  • Multicall — батчинг транзакцій через ERC-2771 або власну реалізацію. Знижує кількість on-chain викликів, особливо критично при високому газі на mainnet.
  • Diamond Pattern (EIP-2535) — для систем, де кількість функцій перевищує ліміт байткоду одного контракту (24 KB). Facet-архітектура дозволяє додавати функціональність без порушення storage. Використовуємо рідко — тільки там, де дійсно потрібно, через складність аудиту.

Як ми оптимізуємо газ в смарт-контрактах?

Патерн Проблема Рішення Економія газу
bool змінна окремо Займає повний slot (32 байти) Упаковка в struct з суміжними типами 15-20k gas на деплой
storage read в loop Кожен SLOAD = 100 gas (EIP-2929) Кеш в memory-змінну перед циклом До 80% на loop
string в storage Дорого та неефективно bytes32 для фіксованих рядків 3-5x економія
Використання require замість if revert Зайві перевірки Inline assembly для частих перевірок 5-10% на транзакцію

Переупорядкування змінних під slot packing — перше, що робимо при аудиті газу. Контракт з uint128 a; uint256 b; uint128 c; займає 3 slot. Переставити в uint128 a; uint128 c; uint256 b; — 2 slot. На деплої різниця 20-40k gas, на кожному SLOAD в гарячих шляхах — відчутно.

В одному проекті ми знизили газ на 40% для стейкінг-контракту: замінили for на while, упакували struct, використовували бітові маски. Підсумковий газ ~150k замість 250k. Протокол з 10 000 користувачів економить суттєві суми на комісіях щомісяця. Ми гарантуємо зниження газу мінімум на 20% у кожному проекті.

Що входить в роботу?

  • Архітектурна документація (діаграми, storage layout, інтерфейси)
  • Вихідний код з тестами (покриття >95%, fuzz-тести)
  • Внутрішній аудит зі звітом за SWC
  • Скрипти деплою та верифікації
  • Доступ до репозиторію та CI/CD (за потреби)
  • Консультація після деплою: 1 місяць підтримки

Типові помилки при розробці

  • Використання tx.origin для аутентифікації замість msg.sender — відкриває фішингові атаки.
  • Відсутність перевірки address(0) в конструкторах та сеттерах — призводить до втрати контролю.
  • Явне приведення типів без перевірки — викликає переповнення або неочікувану поведінку.
  • Виклик send() або transfer() замість call — обмежує газ до 2300, ламає інтеграції з мультисигами.

Як ми працюємо

  1. Аналітика (1-3 дні). Розбираємо архітектуру: які ролі, які права, які інваріанти система повинна дотримуватися завжди. Інваріанти — основа для property-based тестів в Echidna.
  2. Проектування (2-5 днів). Діаграма контрактів, storage layout, інтерфейси. На цьому етапі вирішуємо питання апгрейдаємості: UUPS, Transparent, або immutable. Для DeFi-протоколів з цінністю >1M USD апгрейдаємість — не завжди перевага з точки зору довіри.
  3. Розробка. Контракти + тести в Foundry. Покриття >95% по рядках, fuzz-тести на всі публічні функції з числовими параметрами. Fork-тести на Ethereum/Polygon mainnet для інтеграцій з Uniswap, Aave, Chainlink.
  4. Внутрішній аудит. Slither, Mythril, ручний review з чеклистом SWC. Не замінює зовнішній аудит, але закриває low/medium severity до його початку.
  5. Деплой. Скрипти через Foundry forge script з верифікацією на Etherscan/Polygonscan автоматично. Деплой спочатку на testnet (Sepolia, Mumbai), потім mainnet з мультисиг через Gnosis Safe.

Орієнтири за термінами

Тип контракту Термін
ERC-20 з базовими функціями 3-5 днів
Стейкінг з нагородами та локами 1-2 тижні
DeFi-протокол (AMM, lending) від 6 тижнів
Повний аудит існуючого коду від 5 днів

Оцініть складність вашого проекту — зв'яжіться з нами для безкоштовної консультації. Отримайте детальну оцінку від інженера.