Газ в Ethereum: почему транзакции дорогие и как снизить расход
Транзакция в Ethereum дорогая по двум причинам: контракт тратит много газа на запись в постоянную память, и в момент отправки высока цена газа из-за спроса на место в блоке. Комиссия равна количеству газа, умноженному на сумму базовой цены и чаевых. Разработчик управляет первым множителем: запись в пустую ячейку storage стоит 20 000 газа, и именно её сокращают в первую очередь. Второй множитель снижают выбором времени и сети.
Как считается комиссия
С обновления London в 2021 году в Ethereum действует механизм EIP-1559. У каждого блока есть базовая цена газа (base fee), которая растёт, когда блоки переполнены, и падает, когда они полупустые. Базовая часть сжигается. Сверху пользователь добавляет чаевые валидатору (priority fee). Подробное объяснение есть на странице ethereum.org о газе.
| Составляющая | Кто определяет | Что с ней можно сделать |
|---|---|---|
| Количество газа (gas used) | Код контракта и данные вызова | Оптимизировать контракт |
| Базовая цена (base fee) | Загрузка сети | Выбрать время или другую сеть |
| Чаевые (priority fee) | Пользователь или кошелёк | Не переплачивать, если транзакция не срочная |
| Лимит газа (gas limit) | Пользователь, обычно кошелёк оценивает сам | Неизрасходованный газ возвращается |
Пример расчёта. Простой перевод эфира тратит ровно 21 000 газа. При базовой цене 1 gwei и чаевых 0,1 gwei комиссия составит 21 000 × 1,1 = 23 100 gwei, то есть 0,0000231 ETH. Вызов контракта, который меняет несколько ячеек storage, тратит в разы больше. Цифры цены газа здесь условные: актуальные значения показывают трекеры, например Etherscan Gas Tracker.
Какие операции съедают газ
| Операция | Стоимость в газе | Комментарий |
|---|---|---|
| Базовая стоимость транзакции | 21 000 | Платится за любую транзакцию |
| Запись в пустую ячейку storage (0 → не 0) | 20 000 плюс 2 100 за холодный доступ | Самое дорогое в типичном контракте |
| Изменение уже занятой ячейки | 2 900 плюс холодный доступ | Обнуление частично возвращается |
| Чтение холодной ячейки storage | 2 100 | Первое обращение в транзакции (EIP-2929) |
| Повторное чтение той же ячейки | 100 | Тёплый доступ |
| Байт данных вызова: ненулевой / нулевой | 16 / 4 | Дорогие длинные аргументы |
| Transient storage (TSTORE/TLOAD) | 100 | Живёт только в рамках транзакции, EIP-1153 |
Цифры взяты из спецификаций EIP-2200, EIP-2929 и EIP-1153. Видно главное: любая работа с постоянной памятью на порядки дороже вычислений. Поэтому оптимизация газа — это прежде всего оптимизация storage.
9 приёмов оптимизации в Solidity
- Упаковывайте переменные. Ячейка storage — 32 байта. Переменные
uint128,uint64,addressиbool, объявленные подряд, компилятор кладёт в одну ячейку. Порядок объявления имеет значение. - Читайте storage один раз. Сохраните значение в локальную переменную, работайте с ней и запишите результат одной операцией.
- **Используйте
constantиimmutable.** Параметры, которые не меняются после деплоя, не должны занимать storage. - Собственные ошибки вместо строк.
error InsufficientBalance();дешевле в деплое и при откате, чем длинная строка вrequire. - **
calldataвместоmemoryдля внешних аргументов.** Массивы и строки только для чтения не нужно копировать в память. - Не храните то, что можно вычислить или прочитать из событий. История операций для интерфейса живёт в логах, а не в массивах storage.
- Избегайте неограниченных циклов. Цикл по растущему массиву однажды перестанет помещаться в лимит блока. Используйте схему, где пользователь сам забирает своё (pull вместо push).
- Transient storage для временных флагов. Блокировка от повторного входа в
ReentrancyGuardTransientиз OpenZeppelin стоит дешевле классической. - Включите оптимизатор и измеряйте.
forge snapshotиforge test --gas-reportв Foundry показывают расход по функциям до и после изменений.
Пример упаковки переменных
Учебный пример. Три переменные, объявленные в порядке uint256 amount; address owner; bool active;, займут две ячейки: amount — целую, а owner (20 байт) и active (1 байт) поместятся во вторую. Если вместо uint256 хватит uint96, то amount и owner вместе займут 32 байта, а active уйдёт в следующую ячейку. Выигрыш появляется, только когда переменные читаются и пишутся вместе. Иначе упаковка может даже добавить расход на маскирование битов.
Газ на уровне архитектуры
Самая большая экономия обычно достигается не в коде, а в решениях. Нужно ли хранить данные в блокчейне или достаточно их хеша? Можно ли объединить несколько операций пользователя в одну транзакцию? Можно ли подписывать намерения вне сети и исполнять пачкой? Подписи EIP-712 и permit убирают отдельную транзакцию approve, но требуют аккуратной защиты, о ней — в статье о повторе подписи.
| Решение | Экономия | Цена решения |
|---|---|---|
| Хранить хеш вместо данных | Большая | Данные нужно хранить и отдавать отдельно |
| Merkle-дерево для списка адресов (вайтлист, раздача) | Большая при длинных списках | Сложнее генерация доказательств на бэкенде |
| Перенос в L2 | Комиссия для пользователя ниже в разы | Другая модель безопасности и вывода средств |
| Пакетная обработка | Средняя | Риск упереться в лимит газа блока |
| Подписи вне сети (permit) | Минус одна транзакция | Защита от повтора подписи |
После обновления Dencun в марте 2024 года (EIP-4844) публикация данных роллапов в Ethereum подешевела, и комиссии в L2 заметно упали. Если пользователи платят много за каждое действие, честно рассмотрите другую сеть. Сравнение — в статье о выборе сети.
Из чего состоит комиссия в L2
В роллапах вроде Arbitrum и Base комиссия состоит из двух частей. Первая — плата за исполнение в самой сети L2, она считается так же, как в Ethereum, но цена газа там обычно ниже. Вторая — доля расходов на публикацию данных транзакции в Ethereum. Поэтому в L2 особенно важно экономить не только storage, но и размер данных вызова: длинные массивы аргументов и лишние байты обходятся дороже, чем кажется по одной лишь цене исполнения.
Практический вывод для разработчика: измеряйте расход газа в той сети, куда реально деплоите, а не только в локальной. Для этого Foundry умеет запускать тесты на форке нужной сети с --fork-url, а трекеры и обозреватели каждой сети показывают актуальные цены.
Почему транзакция падает с ошибкой «out of gas»
- Кошелёк неверно оценил лимит, потому что состояние контракта изменилось между оценкой и исполнением.
- Функция содержит цикл по массиву, который вырос с момента тестов.
- Внешний вызов передаёт фиксированный объём газа, а получателю его не хватает.
- Контракт вызывает другой контракт, который сам стал дороже после обновления.
Оптимизацию газа мы делаем вместе с тестами и ревью, а не отдельно от них. Если ваш контракт стал слишком дорогим для пользователей, опишите задачу на странице разработки смарт-контрактов. Перед изменениями в работающем контракте стоит пройти аудит смарт-контракта.
Вопросы и ответы
Почему газ в Ethereum то дорогой, то дешёвый?
Базовая цена газа автоматически растёт, когда блоки переполнены, и падает, когда спрос низкий. Поэтому одна и та же операция стоит по-разному в разное время суток и в дни ажиотажа.
Сколько газа тратит перевод ERC-20?
Обычно несколько десятков тысяч газа: базовые 21 000 плюс чтение и запись балансов. Точная цифра зависит от реализации токена и от того, был ли у получателя ненулевой баланс.
Возвращается ли неизрасходованный газ?
Да. Пользователь платит только за фактически потраченный газ, а лимит — это верхняя граница. Если транзакция откатилась, газ, потраченный до отката, не возвращается.
Что такое gwei?
Это единица цены газа: одна миллиардная доля эфира. Когда кошелёк показывает цену газа 1 gwei, это значит, что каждая единица газа стоит 0,000000001 ETH.
Стоит ли переходить на L2 ради экономии газа?
Если пользователи совершают много мелких операций, обычно да. Но у L2 другая модель безопасности, вывод средств в Ethereum может занимать время, а ликвидность и аудитория отличаются.