Перейти к основному содержимому
БлокчейнMar 28, 2026

Deep EVM #3: Понимание газа — почему ваш контракт стоит столько

OS
Open Soft Team

Engineering Team

Газ: экономическая модель EVM

Газ в Ethereum — это не просто техническое ограничение. Это экономическая система, которая ценит вычислительные ресурсы, предотвращает злоупотребления и определяет, сколько стоит ваш контракт для пользователей. Каждый опкод, каждое обращение к памяти, каждая запись в хранилище — всё измеряется в газе.

Понимание газа на глубоком уровне — это разница между контрактом, который стоит $2 за транзакцию, и контрактом, который стоит $0.20. В мире DeFi и MEV это разница между прибыльным и убыточным бизнесом.

Анатомия стоимости транзакции

Стоимость транзакции в Ethereum складывается из нескольких компонентов:

1. Внутренний газ (Intrinsic Gas)

Каждая транзакция начинается с базовой стоимости:

КомпонентСтоимость
Базовая стоимость транзакции21000 газа
Ненулевой байт calldata16 газа
Нулевой байт calldata4 газа
Создание контракта (CREATE)32000 газа
Список доступа: адрес2400 газа
Список доступа: слот хранилища1900 газа

Для простого перевода ETH (нет calldata) это ровно 21000 газа. Для вызова transfer(address,uint256) на ERC-20 — 21000 + стоимость calldata + стоимость выполнения.

Calldata для transfer:

// 4 байта селектора + 32 байта адреса + 32 байта суммы = 68 байт
// Типичная стоимость: ~16 * 40 (ненулевых) + 4 * 28 (нулевых) = 752 газа

2. Стоимость выполнения

Это сумма газа всех выполненных опкодов. Для типичного ERC-20 transfer:

// Примерная разбивка стоимости ERC-20 transfer:
// Диспетчеризация функций:     ~100 газа
// Загрузка баланса отправителя: ~2100 газа (холодный SLOAD)
// Проверка достаточности:       ~10 газа
// Обновление баланса отправителя: ~2900 газа (тёплый SSTORE, ненулевое->ненулевое)
// Загрузка баланса получателя:  ~2100 газа (холодный SLOAD)
// Обновление баланса получателя: ~20000 или 2900 газа (зависит от нулевого/ненулевого)
// Эмиссия события Transfer:     ~1500 газа (LOG3 + данные)
// Возврат значения:              ~50 газа
// Итого: ~29000-51000 газа

Обратите внимание на огромную разницу: если получатель уже имел баланс (ненулевое->ненулевое SSTORE: 2900), транзакция на ~17000 газа дешевле, чем если получатель новый (нулевое->ненулевое: 20000).

3. EIP-1559: базовая ставка и приоритет

С обновления London (август 2021) стоимость газа определяется двумя компонентами:

общая стоимость = газ_использовано * (базовая_ставка + приоритетная_ставка)
  • Базовая ставка (base fee) — алгоритмически определяется протоколом. Увеличивается, когда блоки заполнены более чем наполовину; уменьшается, когда менее. Сжигается полностью.
  • Приоритетная ставка (priority fee / tip) — платёж валидатору за включение транзакции. В периоды низкой нагрузки ~1-2 gwei; при высокой ~20-100+ gwei.

Почему SSTORE так дорог

Запись в хранилище — самая дорогая операция в EVM. Вот почему:

Дерево состояний Ethereum

Ethereum хранит всё состояние в модифицированном Merkle Patricia Trie. Каждая запись в хранилище:

  1. Обновляет значение в дереве хранилища контракта.
  2. Пересчитывает хеши от листа до корня (O(log n) хеш-операций).
  3. Обновляет корень хранилища контракта в дереве состояний аккаунтов.
  4. Пересчитывает хеши дерева аккаунтов до корня.

Для записи нового значения (0->ненулевое) нужно создать новый узел-лист — 20000 газа. Для обновления существующего (ненулевое->ненулевое) — 2900 газа (тёплый), потому что лист уже существует.

Практический пример: маппинг vs массив

// Сценарий: хранение 100 адресов

// Вариант A: mapping (каждый адрес = отдельный слот)
mapping(uint256 => address) public addresses;
// Запись 100 адресов: 100 * 22100 = 2 210 000 газа (все холодные, 0->ненулевое)

// Вариант B: массив (упакованные данные)
address[] public addresses;
// Запись 100 адресов: массив уже инициализирован, ~100 * 5000 = 500 000 газа
// Но первоначальная инициализация длины массива: 22100 газа

Паттерны оптимизации газа

1. Кэширование слотов хранилища

Самая частая и эффективная оптимизация — кэширование значений хранилища в локальных переменных:

// ПЛОХО: множественные чтения хранилища
function bad(uint256 amount) external {
    require(balances[msg.sender] >= amount);     // SLOAD #1
    balances[msg.sender] -= amount;               // SLOAD #2 + SSTORE
    totalSupply -= amount;                        // SLOAD #3 + SSTORE
    emit Transfer(msg.sender, address(0), amount);
    require(balances[msg.sender] == 0 || balances[msg.sender] > minBalance); // SLOAD #4
}

// ХОРОШО: кэширование в памяти
function good(uint256 amount) external {
    uint256 senderBalance = balances[msg.sender]; // SLOAD #1 (единственный)
    require(senderBalance >= amount);
    uint256 newBalance = senderBalance - amount;
    balances[msg.sender] = newBalance;            // SSTORE
    totalSupply -= amount;                        // SLOAD #2 + SSTORE
    emit Transfer(msg.sender, address(0), amount);
    require(newBalance == 0 || newBalance > minBalance); // Бесплатно — из стека
}
// Экономия: ~2200 газа (2 холодных SLOAD)

2. Упаковка переменных

Solidity упаковывает переменные меньше 32 байт в один слот:

// ПЛОХО: 3 слота (3 * 2100 = 6300 газа для чтения)
contract Bad {
    uint8 a;      // слот 0
    uint256 b;    // слот 1 (не упаковывается с uint8 из-за размера)
    uint8 c;      // слот 2
}

// ХОРОШО: 2 слота (2 * 2100 = 4200 газа для чтения)
contract Good {
    uint8 a;      // слот 0
    uint8 c;      // слот 0 (упакован с a)
    uint256 b;    // слот 1
}
// Экономия: 2100 газа за каждое чтение

3. Использование calldata вместо memory

// ПЛОХО: копирует весь массив в память
function sumBad(uint256[] memory arr) external pure returns (uint256 total) {
    for (uint256 i; i < arr.length; i++) {
        total += arr[i];
    }
}

// ХОРОШО: читает напрямую из calldata
function sumGood(uint256[] calldata arr) external pure returns (uint256 total) {
    for (uint256 i; i < arr.length; i++) {
        total += arr[i];
    }
}
// Экономия: ~60 газа за элемент + стоимость расширения памяти

4. Использование unchecked для безопасной арифметики

Начиная с Solidity 0.8, арифметические операции проверяют переполнение по умолчанию. Каждая проверка стоит ~100 газа:

// С проверками (по умолчанию в Solidity 0.8+)
function withChecks(uint256 a, uint256 b) external pure returns (uint256) {
    return a + b; // ~130 газа (ADD + проверка переполнения)
}

// Без проверок (когда переполнение невозможно)
function withoutChecks(uint256 a, uint256 b) external pure returns (uint256) {
    unchecked {
        return a + b; // ~30 газа (только ADD)
    }
}

// Типичное применение: счётчик цикла
for (uint256 i; i < length;) {
    // ... тело цикла ...
    unchecked { ++i; } // Экономия ~100 газа за итерацию
}

5. Короткое замыкание условий

Solidity использует ленивое вычисление для && и ||. Порядок условий имеет значение:

// ПЛОХО: дорогое условие проверяется первым
require(expensiveCheck() && cheapCheck());

// ХОРОШО: дешёвое условие проверяется первым
require(cheapCheck() && expensiveCheck());
// Если cheapCheck() ложно, expensiveCheck() не выполняется

Лимит газа блока и его влияние

Лимит газа блока в Ethereum — 30 миллионов газа (целевой — 15 миллионов). Это означает:

  • Максимум ~1500 простых переводов ETH в одном блоке (30M / 21000)
  • Максимум ~500-800 ERC-20 трансферов в одном блоке
  • Один сложный DeFi-своп может потреблять 200000-500000 газа

Для разработчиков MEV-ботов это создаёт прямое давление: чем меньше газа потребляет ваш бот, тем больше прибыли остаётся после оплаты газа. Бот, потребляющий 100000 газа вместо 200000, имеет вдвое большую маржу.

Профилирование газа: инструменты

Для точного анализа газа используйте следующие инструменты:

  1. Foundry gas reportsforge test --gas-report показывает потребление газа для каждой функции.
  2. Hardhat gas reporter — плагин, генерирующий таблицу газа для всех вызовов в тестах.
  3. Tenderly — визуальный дебаггер транзакций с разбивкой газа по опкодам.
  4. EVM Playground — интерактивная среда для экспериментов с опкодами.
# Foundry: запуск тестов с отчётом о газе
forge test --gas-report

# Результат:
# | Contract | Function | Min | Avg  | Max  | # calls |
# |----------|----------|-----|------|------|---------|
# | Token    | transfer | 29k | 45k  | 51k  | 100     |
# | Token    | approve  | 24k | 24k  | 24k  | 50      |

EIP-4844: блобы и газ данных

EIP-4844 (обновление Dencun, март 2024) ввёл новый тип транзакции с блобами — большими фрагментами данных (до 128 КБ), используемыми L2-роллапами для публикации данных. Блобы имеют отдельный рынок газа с собственной базовой ставкой, значительно снижая стоимость данных для роллапов.

Это фундаментально изменило экономику L2: стоимость публикации данных на L1 упала в 10-100 раз, что напрямую снижает стоимость транзакций для пользователей L2.

Заключение

Газ — это сердце экономической модели Ethereum. Понимание его механизмов — от внутреннего газа транзакции до квадратичной стоимости памяти и дифференцированной стоимости хранилища — позволяет писать контракты, которые экономят пользователям реальные деньги. Каждая из описанных оптимизаций — кэширование хранилища, упаковка переменных, использование calldata — это не микрооптимизация, а архитектурное решение.

В следующей статье мы перейдём к безопасности: как msg.sender, контроль доступа и защита от реентрабельности работают на уровне EVM.