Оптимизация газа в Solidity: 12 приёмов и когда они уместны
Практические приёмы экономии газа в смарт-контрактах: упаковка хранилища, calldata, кэширование, immutable, unchecked и правило - не жертвовать безопасностью.
В этой статье
Каждая операция EVM стоит газа, а газ - деньги (газ и комиссии). Для сервиса, который делают тысячи пользователей, экономия даже нескольких тысяч единиц газа на операцию - реальная выгода. Но оптимизация без осторожности опасна: чем «умнее» код, тем легче в нём ошибиться. Сначала правильность и безопасность, потом газ.
Правило первое: измеряйте
Не оптимизируйте «на глаз». Используйте forge test --gas-report и forge snapshot (Foundry): они покажут, где реально уходит газ. Типично 80% расхода - это пара операций с хранилищем.
Откуда берётся стоимость
| Операция | Газ (порядок) |
|---|---|
| Простая арифметика | 3-5 |
| Чтение из памяти | 3 |
Чтение хранилища (SLOAD), холодное | ~2 100 |
| Чтение хранилища, тёплое | ~100 |
| Запись в пустой слот | ~20 000 |
| Запись в непустой слот | ~2 900 - 5 000 |
| Вызов другого контракта (холодный адрес) | ~2 600+ |
Событие (LOG) | ~375 + 375 за тему + 8 за байт |
Вывод: самые дорогие операции - работа с хранилищем и внешние вызовы (EVM изнутри).
12 приёмов
1. Упаковка переменных хранилища
Несколько маленьких переменных в одном слоте вместо отдельных слотов. В структуре сделки Coin-Trust 17 полей уложены в 5 слотов, экономия порядка 240 000 газа при создании (раскладка).
2. Кэширование чтений хранилища
Каждое обращение к переменной состояния - SLOAD. Если читаете несколько раз, сохраните в локальную переменную:
// дорого: два чтения
if (d.status != Status.Active) revert ...;
emit E(d.status);
// дешевле: одно чтение
Status s = d.status;
if (s != Status.Active) revert ...;
emit E(s);То же для структур: Deal storage d = _deals[id]; даёт ссылку и не копирует весь объект.
3. calldata вместо memory
Для параметров внешних функций массив или структуру calldata не нужно копировать в память:
function createOffer(OfferParams calldata p) external payable { ... }4. constant и immutable
Константы времени компиляции (constant) и значения, задаваемые в конструкторе (immutable), встраиваются в байткод и не читаются из хранилища. Для эскроу PLATFORM_FEE_BPS - constant.
5. Пользовательские ошибки
revert NotAuthorized() дешевле, чем require(cond, "длинное сообщение"), потому что не хранит строку в байткоде и возвращает всего 4 байта селектора (ABI).
6. unchecked там, где доказано
С версии 0.8 арифметика проверяется. Если вы доказали, что переполнения нет (например, счётчик, увеличенный всего на 1 после проверки), блок unchecked убирает лишние проверки:
unchecked { ++i; }Применяйте с осторожностью: ошибка здесь возвращает уязвимости переполнения (целые числа).
7. Избегайте неограниченных циклов
Цикл по массиву растущего размера рано или поздно перестанет влезать в лимит газа и заблокирует функцию (DoS). Используйте mapping и «забери сам» (реентерабельность и pull).
8. События вместо хранения
Если данные нужны только внешним наблюдателям, пишите событие, а не слот: событие на порядок дешевле (события). Не подходит, если контракт сам должен читать эти данные позже.
9. Обнуление и возврат газа
Обнуление слота возвращает часть газа. Не злоупотребляйте: возврат ограничен долей от транзакции.
10. Короткая цепочка вызовов
Каждый внешний вызов - это накладные расходы, плюс риск реентерабельности. Пакуйте связанные действия в одну транзакцию (в том числе на уровне кошелька: абстракция аккаунтов).
11. Ранний отказ
Дешёвые проверки (аргументы, права) - в начале функции, дорогие (чтение хранилища, внешние вызовы) - позже. Тогда неудачная транзакция откатится дёшево.
12. Настройки оптимизатора
Компилятор Solidity умеет оптимизировать код. Параметр optimizer_runs показывает, на сколько вызовов в жизни контракта рассчитан код: больше - дешевле вызов, но больше размер. Эскроу собран с 200 проходами (параметры).
Чего делать не стоит
- Жертвовать читаемостью ради мелочей. Экономия 20 газа не стоит риска ошибки.
- Ломать безопасность. Не убирайте
nonReentrantили проверки ради экономии (каталог уязвимостей). - Использовать ассемблер без необходимости. Инструмент мощный, но обходит защиты компилятора.
- Оптимизировать до тестов. Сначала корректность, затем измерения.
Оптимизация с точки зрения пользователя
Пользователю влиять на устройство контракта нельзя, но можно уменьшить расходы:
- выбрать сеть с низкими комиссиями (выбор сети, Layer 2);
- отправлять транзакции в менее загруженное время (рынок комиссий);
- не создавать лишних операций - например, не повторять
approve, если подходящее разрешение уже есть; - сверить стоимость в кошельке до подтверждения.
Ознакомьтесь с составляющими комиссий сервиса в статье комиссии на примерах.
Что почитать дальше
Вернитесь к основам и закрепите путь: словарь терминов. Продолжайте изучать безопасность: аудиты и баг-баунти.
Читайте также
- 3 · Под капотом
Раскладка хранилища и упаковка переменных в Solidity
Как контракт хранит данные в слотах по 32 байта, как упаковываются переменные, где лежат mapping и массивы и как устроена структура сделки Coin-Trust.
4 мин чтения - 3 · Под капотом
EIP-1559: как устроен рынок комиссий Ethereum (base fee, чаевые и сжигание)
Как работает комиссия после обновления London: базовая плата, чаевые, maxFeePerGas, подстройка на 12,5% и сжигание ETH. С расчётами и примерами.
3 мин чтения
Упоминается в статьях
- Целочисленная математика и округление в смарт-контрактах
- Раскладка хранилища и упаковка переменных в Solidity
- CREATE2: как один и тот же адрес контракта появляется сразу в четырёх сетях
- Оракулы и проблема оракулов: как блокчейн узнаёт о внешнем мире
- EIP-1559: как устроен рынок комиссий Ethereum (base fee, чаевые и сжигание)
- MEV и фронтраннинг: кто и как зарабатывает на порядке транзакций
- Foundry для начинающих: компиляция, тесты, фаззинг и локальная сеть
- Аудиты и баг-баунти: как проверяют смарт-контракты и как читать отчёт
Готовы попробовать?
Создайте первую сделку за пару минут. Деньги блокируются в смарт-контракте и выплачиваются по правилам, которые видны заранее.

