К основному содержимому

Введите слово из вопроса или названия статьи.

3 · Под капотом

Оптимизация газа в Solidity: 12 приёмов и когда они уместны

Практические приёмы экономии газа в смарт-контрактах: упаковка хранилища, calldata, кэширование, immutable, unchecked и правило - не жертвовать безопасностью.

3 мин чтенияОбновлено:
В этой статье

Каждая операция 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. Если читаете несколько раз, сохраните в локальную переменную:

solidity
// дорого: два чтения
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 не нужно копировать в память:

solidity
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 убирает лишние проверки:

solidity
unchecked { ++i; }

Применяйте с осторожностью: ошибка здесь возвращает уязвимости переполнения (целые числа).

7. Избегайте неограниченных циклов

Цикл по массиву растущего размера рано или поздно перестанет влезать в лимит газа и заблокирует функцию (DoS). Используйте mapping и «забери сам» (реентерабельность и pull).

8. События вместо хранения

Если данные нужны только внешним наблюдателям, пишите событие, а не слот: событие на порядок дешевле (события). Не подходит, если контракт сам должен читать эти данные позже.

9. Обнуление и возврат газа

Обнуление слота возвращает часть газа. Не злоупотребляйте: возврат ограничен долей от транзакции.

10. Короткая цепочка вызовов

Каждый внешний вызов - это накладные расходы, плюс риск реентерабельности. Пакуйте связанные действия в одну транзакцию (в том числе на уровне кошелька: абстракция аккаунтов).

11. Ранний отказ

Дешёвые проверки (аргументы, права) - в начале функции, дорогие (чтение хранилища, внешние вызовы) - позже. Тогда неудачная транзакция откатится дёшево.

12. Настройки оптимизатора

Компилятор Solidity умеет оптимизировать код. Параметр optimizer_runs показывает, на сколько вызовов в жизни контракта рассчитан код: больше - дешевле вызов, но больше размер. Эскроу собран с 200 проходами (параметры).

Чего делать не стоит

  • Жертвовать читаемостью ради мелочей. Экономия 20 газа не стоит риска ошибки.
  • Ломать безопасность. Не убирайте nonReentrant или проверки ради экономии (каталог уязвимостей).
  • Использовать ассемблер без необходимости. Инструмент мощный, но обходит защиты компилятора.
  • Оптимизировать до тестов. Сначала корректность, затем измерения.

Оптимизация с точки зрения пользователя

Пользователю влиять на устройство контракта нельзя, но можно уменьшить расходы:

  • выбрать сеть с низкими комиссиями (выбор сети, Layer 2);
  • отправлять транзакции в менее загруженное время (рынок комиссий);
  • не создавать лишних операций - например, не повторять approve, если подходящее разрешение уже есть;
  • сверить стоимость в кошельке до подтверждения.

Ознакомьтесь с составляющими комиссий сервиса в статье комиссии на примерах.

Что почитать дальше

Вернитесь к основам и закрепите путь: словарь терминов. Продолжайте изучать безопасность: аудиты и баг-баунти.

Упоминается в статьях

Готовы попробовать?

Создайте первую сделку за пару минут. Деньги блокируются в смарт-контракте и выплачиваются по правилам, которые видны заранее.