Оптимизация газа в EVM: шаблоны и практические приёмы

Шаблоны оптимизации газа (gas patterns) — это практические приёмы, которые помогают снижать стоимость выполнения смарт-контрактов под EVM. В высоконагруженных протоколах (DEX, лендинги, агрегаторы) правильная работа с газом влияет и на UX пользователей, и на экономику протокола.

Эта страница — обзор типовых паттернов. За теорией газа см. газ в Ethereum, за деталями хранения — storage layout в Solidity.

Оптимизация газа в EVM: шаблоны и практические приёмы

Общие принципы оптимизации газа

  • Storage дороже всего

Операции SSTORE и SLOAD — самые дорогие. Любой паттерн, который:

  • уменьшает количество обращений к storage;
  • заменяет их вычислениями в memory/calldata;

даёт существенную экономию.

  • Чем меньше байткод — тем дешевле деплой

Большие библиотеки и дублирование логики увеличивают стоимость развёртывания контракта.

  • Газ против читаемости и безопасности

Агрессивная оптимизация делает код сложнее. Важно искать баланс: сначала безопасность и предсказуемость, потом — микроген.

  • Профилирование важнее «магии»

Реальный выигрыш видно только по результатам профилирования и тестов, а не по «чувству». Сначала измеряем, потом оптимизируем.

Паттерны работы со storage

  • Кэширование чтений из storage в память

Вместо многократного обращения:

  ```solidity
  function f() external {
      doSomething(balances[msg.sender]);
      doAnotherThing(balances[msg.sender]);
  }
  ```

лучше:

  ```solidity
  function f() external {
      uint256 balance = balances[msg.sender];
      doSomething(balance);
      doAnotherThing(balance);
  }
  ```

Один SLOAD вместо двух и более.

  • Обновление storage один раз в конце

Если в функции несколько раз изменяется одно и то же поле:

  ```solidity
  user.position += size;
  user.position -= fee;
  user.position += bonus;
  ```

выгоднее считать в локальной переменной, а в storage записать итоговое значение один раз.

  • Минимизация «случайных» структур

Сложные nested-struct с множеством полей часто означают лишние SLOAD/SSTORE. Иногда выгоднее нормализовать данные или разделить одну сущность на несколько, если это упрощает доступ к данным.

Calldata, memory и параметры функций

  • Использовать calldata для входных параметров

Внешние функции (external) с большими массивами или строками выгоднее объявлять с параметрами в calldata:

  ```solidity
  function batchTransfer(address[] calldata recipients, uint256 amount) external;
  ```

— вместо memory. Это экономит копирование данных и уменьшает газ.

  • Передавать ссылки, а не копировать

Там, где возможно, лучше передавать массивы и структуры по ссылке (через storage/calldata), а не копировать их в новые memory-объекты без нужды.

  • Структуры в памяти, если не нужно хранить в storage

Временные объекты удобно держать в memory: это дешевле, чем создавать «вспомогательные» поля в storage.

Циклы и обходы массивов

  • Минимизировать длину и количество циклов

Каждый шаг цикла — это дополнительные инструкции и газ. Типовая оптимизация:

  • избегать линейных обходов, если можно обратиться по индексу/маппингу;
  • разбивать большие batch-операции на несколько транзакций.
  • Убирать лишние проверки внутри цикла

Инварианты, которые не зависят от индекса, можно вынести за цикл, чтобы не проверять их на каждой итерации.

  • Batch-операции вместо множества транзакций

Иногда дешевле сделать одну batch-функцию с циклом, чем десятки отдельных транзакций, особенно если часть газа платит вызывающий контракт/протокол.

События и логирование (events)

  • Логировать только необходимое

События нужны для аналитики и индексации, но каждый эмит — это LOG-инструкции и газ за данные и indexed-поля. Полезные паттерны:

  • не дублировать в событиях то, что легко восстановить по другим данным;
  • аккуратно выбирать, что делать indexed, а что нет;
  • объединять несколько близких событий, если это не ухудшает DX аналитиков.
  • Избегать лишних событий в горячих путях

В функциях, которые вызываются чаще всего (например, свопы), каждое лишнее событие стоит денег пользователю.

Арифметика и типы данных

  • Использовать минимально достаточные типы

В places, где значения точно укладываются, uint64/uint128:

  • помогают с packing в слоты storage;
  • уменьшают размер байткода (но нужно внимательно следить за переполнением).
  • Переиспользовать вычисления

Если формула повторяется, лучше вычислить её один раз и сохранить в локальной переменной, а не пересчитывать каждый раз.

  • Не злоупотреблять сложной математикой на ончейне

Если можно перенести часть вычислений off-chain, а на ончейне только верифицировать результат (подпись, proof), это почти всегда выгоднее.

Внешние вызовы и архитектурные решения

  • Минимизировать количество внешних вызовов

Каждый call/delegatecall/staticcall — это дополнительный расход газа + риск по безопасности (см. риски DELEGATECALL и reentrancy). Нередко выгоднее:

  • агрегировать несколько операций в один внешний вызов;
  • хранить часть состояния локально, а не дергать внешний контракт на каждый шаг.
  • Продумать архитектуру протокола

Иногда самая большая экономия достигается не микропаттернами, а изменением архитектуры:

  • хранить меньше данных ончейн;
  • внедрять off-chain исполнительные слои;
  • использовать rollup- или zk-подходы, если это вписывается в модель протокола.

Баланс между оптимизацией и безопасностью

  • Не жертвовать безопасностью ради экономии газа

Удаление проверок, усложнение логики и нестандартные конструкции ради пары процентов экономии газа часто приводят к уязвимостям, которые обходятся гораздо дороже.

  • Сначала корректность, затем оптимизация

Практический порядок:

  1. написать чистый и понятный код (на Solidity / Vyper);
  2. покрыть тестами;
  3. провести аудит;
  4. после этого точечно оптимизировать горячие участки, при необходимости используя Yul.
  • Документировать нестандартные паттерны

Всякий раз, когда вы отходите от очевидной реализации ради газа, фиксируйте это в комментариях и документации: зачем так сделано, какой выигрыш и какие риски.

Мини-чеклист gas patterns для ревью

Перед ревью/аудитом смарт-контракта полезно пройтись по чеклисту:

  • не дублируются ли SLOAD/SSTORE в рамках одной функции;
  • используются ли calldata для массивов/строк во внешних функциях;
  • нет ли тяжёлых циклов по неограниченным массивам;
  • не логируются ли лишние данные в событиях;
  • нет ли избыточных внешних вызовов одного и того же контракта;
  • не усложнён ли код ради микроскопической экономии газа.

Если контракт выдержал этот чеклист и при этом остаётся понятным и безопасным — значит, базовый уровень оптимизации газа уже достигнут.

См. также

Task Runner