Перейти к содержанию
Learning Platform
Глоссарий Troubleshooting
Урок 11.02 · 25 мин
Продвинутый
SecurityGas ManagementFeesTON

Gas Management: оценка и возврат

Gas management в TON — это не просто оптимизация расходов, а вопрос безопасности контракта. Неправильный расчёт газа может привести к тому, что контракт «встанет» (не сможет обработать сообщения), или станет уязвим к drain-атакам через чрезмерное потребление газа. Грамотное управление газом — это баланс между достаточностью ресурсов и защитой от злоупотреблений.

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


Модель газа в TON

В TON газ складывается из трёх компонентов:

  1. Computation fee — стоимость выполнения инструкций TVM
  2. Storage fee — плата за хранение данных контракта (взимается при каждой транзакции)
  3. Forwarding fee — стоимость пересылки сообщений между контрактами

Отправитель сообщения прикладывает msg_value — количество TON, которое покроет все эти расходы. Остаток после выполнения остаётся на балансе получателя, если тот явно его не вернёт.


Валидация msg_value

Уязвимый контракт

// УЯЗВИМЫЙ КОД -- принимает любое msg_value
contract VulnerableHandler {
    receive(msg: ProcessOrder) {
        // Никакой проверки msg_value!
        // Если значение слишком мало, операция упадёт
        // на середине выполнения -- часть состояния уже
        // изменена, часть нет

        self.orders += 1;

        // Эта отправка может не хватить газа
        send(SendParameters{
            to: msg.warehouse,
            value: 0,
            mode: SendRemainingValue,
            body: ShipOrder{orderId: msg.orderId}.toCell(),
        });
    }
}

Исправленный контракт

const MIN_GAS: Int = ton("0.05"); // Минимум для выполнения операции

contract SecureHandler {
    receive(msg: ProcessOrder) {
        // Проверяем, что газа достаточно
        require(context().value >= MIN_GAS, "Insufficient gas");

        self.orders += 1;

        send(SendParameters{
            to: msg.warehouse,
            value: 0,
            mode: SendRemainingValue,
            body: ShipOrder{orderId: msg.orderId}.toCell(),
        });
    }
}
WARNING

Всегда проверяйте msg_value

Без проверки context().value контракт может начать выполнение и упасть на середине — состояние частично изменено, а операция не завершена. Используйте require(context().value >= MIN_GAS, ...) в начале каждого handler.


Gas Excess Return: паттерн 0xd53276db

После выполнения операции контракт должен вернуть неиспользованный газ отправителю. Стандартный способ — сообщение Excesses с опкодом 0xd53276db:

message(0xd53276db) Excesses {}

contract ExampleWithExcessReturn {
    receive(msg: SomeOperation) {
        require(context().value >= ton("0.05"), "Insufficient gas");

        // ... выполняем операцию ...

        // Возвращаем остаток газа отправителю
        send(SendParameters{
            to: sender(),
            value: 0,
            mode: SendRemainingValue,
            body: Excesses{}.toCell(),
        });
    }
}

Опкод 0xd53276db — это стандарт, принятый в экосистеме TON. Кошельки и инструменты распознают это сообщение и показывают его как “возврат газа”, а не как обычный перевод.

Проверка знанийKnowledge check
Why should a contract validate `context().value >= MIN_GAS` before executing any state changes, rather than relying on the transaction to simply fail if gas runs out?
ОтветAnswer
If gas runs out mid-execution, TVM may have already committed partial state changes in the compute phase before hitting the out-of-gas error. The contract ends up in an inconsistent state -- for example, a counter incremented but the follow-up message never sent. Checking upfront ensures the contract either has enough gas for the full operation or rejects the message cleanly before any state mutation.
TIP

Каждый handler должен возвращать excess

Если контракт получает 1 TON на операцию, которая стоит 0.02 TON, остальные 0.98 TON останутся навсегда заблокированы на балансе контракта, если не вернуть их через Excesses. Это прямая потеря средств пользователя.


Оценка стоимости операций

Для точной оценки стоимости операции используйте getComputeFee():

// Получаем стоимость текущей транзакции
let gasCost: Int = getComputeFee(gasConsumed, false);

На практике для определения MIN_GAS константы:

  1. Тестируйте контракт в testnet и замеряйте реальный расход газа
  2. Добавляйте запас — обычно 2x-3x от измеренного значения
  3. Учитывайте forwarding fee для сообщений, которые контракт отправляет дальше

Send Modes: безопасное использование

TON предоставляет несколько режимов отправки сообщений. Выбор неправильного режима — распространённая уязвимость:

ModeКонстанта в TactЗначениеКогда использовать
0Отправить указанный valueОтправка фиксированной суммы
1SendPayGasSeparatelyОплатить forwarding fee отдельно от valueПеревод точной суммы
2SendIgnoreErrorsИгнорировать ошибки отправкиНекритичные уведомления
64SendRemainingValueПереслать весь оставшийся msg_valueВозврат excess, chain forwarding
128SendRemainingBalanceОтправить весь баланс контрактаТолько при уничтожении контракта

Частые ошибки

  1. Не используют msg_value - gas_consumed для расчёта оставшегося газа: без этого контракт не знает, сколько газа можно передать дальше по цепочке.
  2. Жёстко кодируют gas_limit вместо динамического расчёта: при изменении газовых цен контракт перестанет работать корректно.
  3. Не возвращают неиспользованный газ (excess) пользователю: газ накапливается на контракте, а пользователь переплачивает.
  4. Забывают резервировать газ на storage fees: контракт может обработать все сообщения, но остаться без средств на хранение и замёрзнуть.

DANGER

Mode 128 (SendRemainingBalance) — опасен!

Режим 128 отправляет весь баланс контракта, а не только msg_value текущего сообщения. Если контракт хранит средства других пользователей, mode 128 отправит их все. Используйте mode 128 только при уничтожении контракта (с флагом SendDestroyIfZero).

Правильное использование mode 64 (SendRemainingValue)

Большинство handler-ов должны использовать mode 64 для возврата газа:

// Возвращаем весь оставшийся msg_value (за вычетом computation fee)
send(SendParameters{
    to: sender(),
    value: 0,       // value = 0, потому что mode 64 подставит оставшееся
    mode: SendRemainingValue,
    body: Excesses{}.toCell(),
});

Когда mode 64 опасен

Если контракт получает несколько сообщений одновременно и использует mode 64 в каждом — только первый handler получит полный remaining value, остальные получат значительно меньше. В таких случаях лучше отправлять фиксированный value.


Forwarding Fees в цепочках сообщений

В TON сообщения часто проходят через несколько контрактов. Каждый hop вычитает forwarding fee из оставшегося msg_value:

Пользователь (1 TON)
    |
    | -0.01 TON (forwarding fee)
    v
Контракт A (0.99 TON)
    |
    | -0.02 TON (computation) -0.01 TON (forwarding fee)
    v
Контракт B (0.96 TON)
    |
    | -0.01 TON (computation) -0.01 TON (forwarding fee)
    v
Контракт C (0.94 TON)

Правило: чем длиннее цепочка сообщений, тем больше газа нужно закладывать в начальное сообщение. Для 3-hop цепочки (типичной для Jetton transfer: wallet -> wallet -> notification) закладывайте как минимум 0.05-0.1 TON.


Ключевые правила

  1. Всегда проверяйте context().value в начале каждого handler
  2. Возвращайте excess через сообщение Excesses (0xd53276db) — это стандарт экосистемы
  3. Используйте mode 64 (SendRemainingValue) для возврата газа — но не в контрактах с параллельными handler-ами
  4. Никогда не используйте mode 128 (SendRemainingBalance) в обычных операциях — только при уничтожении контракта
  5. Закладывайте запас газа для цепочек из нескольких сообщений
  6. Тестируйте расход газа в testnet перед деплоем

В следующем уроке мы разберём другой класс ошибок — целочисленные ловушки, связанные со знаковыми числами в Tact.

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 6