Розробка смарт-контрактів на Solidity
Клієнт приносить контракт на аудит — 800 рядків Solidity, розгортання на Ethereum mainnet через тиждень. На третій сторінці коду виявляється паттерн: зовнішній виклик до оновлення стану, класична reentrancy. Не теоретична — така ж конфігурація була у The DAO у 2016-му, 60 мільйонів доларів. Контракт йде на переробку. Це стандартна ситуація, коли Solidity-розробка йде без системного підходу до безпеки.
Де частіше ломається Solidity-контракт
Reentrancy — все ще живе у production
Попри те що атака відома з 2016 року, варіанти reentrancy продовжують з'являтись. Проблема не в невіданні паттерну — більшість розробників знають про Checks-Effects-Interactions. Проблема у cross-function reentrancy, яку ReentrancyGuard з OpenZeppelin не покриває за замовчуванням.
Сценарій: контракт A викликає контракт B через низькорівневий call. B — це токен, що реалізує ERC-777 з гаком tokensReceived. У момент гака у A вже списані токени, але ETH ще не відправлений. Функція виводу у A не заблокована reentrancy-гардом, тому що розробник вважав, що захистив тільки withdraw. Результат — дренаж резервів.
Рішення: nonReentrant на всі публічні функції, які змінюють стан та роблять зовнішні виклики. Для складних систем — окремий ReentrancyGuardUpgradeable з перевіркою на рівні модулю, а не функції.
Storage collision у proxy-паттернах
При використанні Transparent Proxy або UUPS змінні зберігаються у storage слотах за позицією оголошення. Якщо у новій версії імплементації додати змінну перед існуючою — весь storage зміщується. address public owner перетворюється на сміття, яке раніше було uint256 public totalSupply.
Це не гіпотетика: кілька протоколів у 2022-2023 роках виявляли проблему після апгрейду, коли маппінги починали повертати невірні значення. Спасає 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 (користувач сам забирає награди, а не контракт розсилає всім).
Як ми пишемо Solidity-контракти
Стек та інструменти
Основний інструмент розробки — 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 для символічного виконання перед розгортанням. Для фаззингу складної логіки — Echidna з property-based тестами.
Паттерни, які використовуємо
Diamond Pattern (EIP-2535) — для систем, де кількість функцій перевищує ліміт байткоду одного контракту (24 KB). Facet-архітектура дозволяє додавати функціональність без порушення storage. Використовуємо рідко — тільки де дійсно потрібно, через складність аудиту.
Pull payment pattern — ETH ніколи не відправляється напрямик з функцій протоколу. Баланси накопичуються у маппінгу, користувач викликає withdraw(). Це видаляє весь клас reentrancy-векторів та усуває проблеми з контрактами-отримувачами, які реверсують receive().
Multicall — батчинг трансакцій через ERC-2771 або власну реалізацію. Зменшує кількість on-chain викликів, критично при високому газі на mainnet.
Оптимізація газу
Типові місця, де газ йде впустую:
| Паттерн | Проблема | Рішення | Економія |
|---|---|---|---|
bool змінна окремо |
Займає повний slot (32 байти) | Упаковка в struct зі суміжними типами | 15-20k газу на розгортання |
storage read у loop |
Кожен SLOAD = 100 газу (EIP-2929) | Кеш у memory-змінну перед циклом | До 80% на loop |
emit Event без індексації |
Неможливо фільтрувати через The Graph | indexed на ключові поля |
Без економії газу, але критично для DX |
string у storage |
Дорого та неефективно | bytes32 для фіксованих строк |
3-5x економія |
Переупорядкування змінних під slot packing — перше, що робимо при аудиті газу. Контракт з uint128 a; uint256 b; uint128 c; займає 3 слоти. Переставити у uint128 a; uint128 c; uint256 b; — 2 слоти. На розгортанні різниця 20-40k газу, на кожному SLOAD у гарячих контурах — помітна.
Процес роботи
Аналітика (1-3 дні). Розбираємо архітектуру: які ролі, які права, які інваріанти система повинна завжди дотримуватись. Інваріанти — основа для property-based тестів у Echidna.
Проектування (2-5 днів). Діаграма контрактів, storage layout, інтерфейси. На цьому етапі вирішуємо питання апгрейдності: UUPS, Transparent, або immutable. Для DeFi-протоколів з вартістю >1M USD апгрейдність — не завжди перевага з точки зору довіри.
Розробка. Контракти + тести у Foundry. Покриття >95% по рядках, fuzz-тести на всі публічні функції з числовими параметрами. Fork-тести на Ethereum/Polygon mainnet для інтеграцій з Uniswap, Aave, Chainlink.
Внутрішній аудит. Slither, Mythril, ручний review з чеклістом SWC (Smart Contract Weakness Classification). Не замінює зовнішній аудит, але закриває low/medium severity до його початку.
Розгортання. Скрипти через Foundry forge script з автоматичною верифікацією на Etherscan/Polygonscan. Розгортання спочатку на testnet (Sepolia, Mumbai), потім mainnet з мультисигом через Gnosis Safe.
Орієнтири за строками
Простий токен ERC-20 з базовими функціями — 3-5 днів включаючи тести. Стейкинг-контракт з награма та часовими локами — 1-2 тижні. Повноцінний DeFi-протокол з AMM-логікою або ленінгом — від 6 тижнів. Строки залежать від складності логіки та вимог до покриття тестами.
Вартість розраховується після аналізу технічного завдання.







