Статичний аналіз смарт-контрактів (Slither, Mythril)

Проєктуємо та розробляємо блокчейн-рішення повного циклу: від архітектури смарт-контрактів до запуску DeFi-протоколів, NFT-маркетплейсів та криптобірж. Аудит безпеки, токеноміка, інтеграція з наявною інфраструктурою.
Показано 1 з 1Усі 1306 послуг
Статичний аналіз смарт-контрактів (Slither, Mythril)
Середній
~1 день
Часті запитання

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

Етапи блокчейн-розробки

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

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

Оптимізація газу смарт-контрактів

Газ — це не абстракція, це гроші. При ціні газу 30 gwei та ETH по $3000, одна транзакція з 200,000 gas коштує $18. Помножте на тисячі користувачів в день — та різниця між оптимізованим і неоптимізованим контрактом вимірюється в сотнях тисяч доларів на рік. На L2 газ дешевше, але calldata все ще дорогий та оптимізація залишається важливою.

Оптимізація газу — це не «зробити код красивіше». Це знання EVM opcodes та їхньої вартості, паттерни зберігання даних та вміння читати gas report у Foundry. Розберемо ключові техніки.

Storage: головне джерело витрат

SSTORE та SLOAD вартість

Запис у storage (SSTORE) — один з найдорожчих opcodes: 20,000 gas для запису у новий слот, 5,000 gas для зміни існуючого (холодний), 100 gas для повторної зміни у межах однієї транзакції (теплий, EIP-2929).

Читання (SLOAD) — 2,100 gas холодний, 100 gas теплий. Якщо читаєте той же слот storage кілька разів у функції — кешуйте у змінну memory:

// Погано: два SLOADs
function bad() external view returns (uint256) {
    return balances[msg.sender] + balances[msg.sender] / 100;
}

// Добре: один SLOAD
function good() external view returns (uint256) {
    uint256 balance = balances[msg.sender]; // один SLOAD
    return balance + balance / 100;
}

Пакування Storage

EVM слот — 32 байти. Якщо у вас кілька змінних менше 32 байт — Solidity упакує їх в один слот, якщо вони оголошені поряд. Це зменшує кількість SLOAD/SSTORE.

// Погано: три окремих слота
uint256 a; // слот 0
uint128 b; // слот 1 (неефективно — займає весь слот)
uint128 c; // слот 2 (неефективно)

// Добре: b та c упаковані в один слот
uint256 a; // слот 0
uint128 b; // слот 1 (перші 16 байт)
uint128 c; // слот 1 (наступні 16 байт)

Для struct це особливо важливо: порядок полів у struct впливає на кількість слотів storage. Групуйте маленькі типи разом.

Mapping vs Array

Mapping дешевше для довільного доступу: O(1) та один SLOAD. Array з пошуком — O(N) та N SLOADs. Якщо вам потрібно «чи є X у списку» — використовуйте mapping(address => bool), не array.

Ітерація по mapping on-chain неможлива нативно (немає способу отримати всі ключі). Якщо потрібна ітерація — EnumerableSet з OpenZeppelin (зберігає обидва: mapping для O(1) пошуку та array для ітерації).

Calldata та функції

Calldata vs Memory

Параметри функції позначені calldata не копіюються у memory — читаються безпосередньо з calldata. Для масивів та рядків це значна економія:

// memory: копіює весь масив — дорого
function processMemory(uint256[] memory data) external { ... }

// calldata: без копіювання — дешевше
function processCalldata(uint256[] calldata data) external { ... }

Різниця зростає з розміром даних. Для великого масиву — економія сотень тисяч gas.

Користувацькі помилки vs Require Strings

До Solidity 0.8.4 помилки передавалися як рядок, кодовані у calldata та зберігалися у bytecode. Користувацькі помилки — дешевше як при розгортанні, так і при revert:

// Старий спосіб: дорого
require(amount > 0, "Amount must be positive");

// Користувацька помилка: дешевше
error InvalidAmount(uint256 amount);
if (amount == 0) revert InvalidAmount(amount);

Користувацька помилка економить ~200-500 gas на кожному revert та зменшує розмір bytecode (менше string literals).

Короткі функції та вбудовування

Кожний внутрішній виклик функції — додатковий overhead (JUMP opcodes, управління стеком). Компілятор Solidity з --via-ir (Yul IR) краще оптимізує вбудовування малих функцій. Увімкнення viaIR: true у foundry.toml/hardhat.config може зменшити gas на 5-15% без змін коду.

Паттерни оптимізації

Упаковані struct для масових операцій

При обробці масиву об'єктів, структура даних впливає на слоти storage на елемент:

Структура Слотів на елемент Gas на елемент
Три поля uint256 3 слота ~60,000 gas запису
uint128 + uint64 + uint64 1 слот ~20,000 gas запису
Bitmap флаги 1 слот / 256 елементів ~78 gas на флаг

Bitmap для булевих флагів: uint256 flags зберігає 256 флагів в одному слоті. Операція flags |= (1 << bitIndex) — 100 gas замість 20,000 для окремого mapping(uint => bool).

Ледача ініціалізація

Не ініціалізуйте змінні значенням за замовчуванням — це марна трата gas:

// Непотрібний SSTORE з нулем (Solidity робить це)
uint256 public counter = 0; // не потрібно

// Просто
uint256 public counter;

Невідмічена арифметика

Solidity 0.8+ додав переповнення/недоповнення перевірки до кожної арифметичної операції (+100-200 gas). Якщо ви впевнені що переповнення неможливо — використовуйте unchecked:

// Стандартний цикл з відміченою арифметикою
for (uint256 i = 0; i < arr.length; i++) { ... }

// Оптимізований з невідміченим збільшенням
for (uint256 i = 0; i < arr.length; ) {
    // ... логіка
    unchecked { ++i; } // префіксний ++ теж дешевше ніж постфіксний
}

Економія на типовому циклі з 100 елементами — 5,000-10,000 gas.

Інструменти вимірювання

Снимки Foundry gasforge snapshot створює файл .gas-snapshot з використанням gas на тест. forge snapshot --diff показує зміну після виправлень. Необхідне для ітеративної оптимізації.

forge test --gas-report — таблиця з середнім/мінімальним/максимальним gas на функцію.

ETH Gas Station / Tenderly — симуляція транзакції з розбором по opcodes. Корисно для розуміння де саме витрачається gas.

Процес аудиту та оптимізації

  1. Базова вимірювання: forge snapshot до змін
  2. Профілювання: визначити найдорожчі функції
  3. Аналіз storage: перевірити упаковку структури, непотрібні SLOADs
  4. Застосувати оптимізації: за пріоритетом (storage → calldata → арифметика)
  5. Верифікація: forge snapshot --diff, перевірити що функціональність не зламана

Типовий результат: 20-40% зменшення gas для неоптимізованого контракту, 10-20% для вже «розумного» коду. Для DeFi з високим навантаженням транзакцій це прямої економії користувачам.

Часова шкала: аудит + звіт + реалізація оптимізацій — 2-4 тижні залежно від розміру кодової бази.