Gas Management: оценка и возврат
Gas management в TON — это не просто оптимизация расходов, а вопрос безопасности контракта. Неправильный расчёт газа может привести к тому, что контракт «встанет» (не сможет обработать сообщения), или станет уязвим к drain-атакам через чрезмерное потребление газа. Грамотное управление газом — это баланс между достаточностью ресурсов и защитой от злоупотреблений.
Каждая операция в TON стоит газ. В отличие от Ethereum, где пользователь оплачивает весь газ транзакции заранее, в TON газ передаётся между контрактами внутри сообщений через msg_value. Это создаёт уникальные проблемы безопасности: если контракт не проверяет входящий газ или не возвращает излишки, пользователи теряют средства.
Модель газа в TON
В TON газ складывается из трёх компонентов:
- Computation fee — стоимость выполнения инструкций TVM
- Storage fee — плата за хранение данных контракта (взимается при каждой транзакции)
- 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(),
});
}
}
Всегда проверяйте 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. Кошельки и инструменты распознают это сообщение и показывают его как “возврат газа”, а не как обычный перевод.
Проверка знаний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?
Каждый handler должен возвращать excess
Если контракт получает 1 TON на операцию, которая стоит 0.02 TON, остальные 0.98 TON останутся навсегда заблокированы на балансе контракта, если не вернуть их через Excesses. Это прямая потеря средств пользователя.
Оценка стоимости операций
Для точной оценки стоимости операции используйте getComputeFee():
// Получаем стоимость текущей транзакции
let gasCost: Int = getComputeFee(gasConsumed, false);
На практике для определения MIN_GAS константы:
- Тестируйте контракт в testnet и замеряйте реальный расход газа
- Добавляйте запас — обычно 2x-3x от измеренного значения
- Учитывайте forwarding fee для сообщений, которые контракт отправляет дальше
Send Modes: безопасное использование
TON предоставляет несколько режимов отправки сообщений. Выбор неправильного режима — распространённая уязвимость:
| Mode | Константа в Tact | Значение | Когда использовать |
|---|---|---|---|
| 0 | — | Отправить указанный value | Отправка фиксированной суммы |
| 1 | SendPayGasSeparately | Оплатить forwarding fee отдельно от value | Перевод точной суммы |
| 2 | SendIgnoreErrors | Игнорировать ошибки отправки | Некритичные уведомления |
| 64 | SendRemainingValue | Переслать весь оставшийся msg_value | Возврат excess, chain forwarding |
| 128 | SendRemainingBalance | Отправить весь баланс контракта | Только при уничтожении контракта |
Частые ошибки
- Не используют msg_value - gas_consumed для расчёта оставшегося газа: без этого контракт не знает, сколько газа можно передать дальше по цепочке.
- Жёстко кодируют gas_limit вместо динамического расчёта: при изменении газовых цен контракт перестанет работать корректно.
- Не возвращают неиспользованный газ (excess) пользователю: газ накапливается на контракте, а пользователь переплачивает.
- Забывают резервировать газ на storage fees: контракт может обработать все сообщения, но остаться без средств на хранение и замёрзнуть.
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.
Ключевые правила
- Всегда проверяйте
context().valueв начале каждого handler - Возвращайте excess через сообщение
Excesses(0xd53276db) — это стандарт экосистемы - Используйте mode 64 (
SendRemainingValue) для возврата газа — но не в контрактах с параллельными handler-ами - Никогда не используйте mode 128 (
SendRemainingBalance) в обычных операциях — только при уничтожении контракта - Закладывайте запас газа для цепочек из нескольких сообщений
- Тестируйте расход газа в testnet перед деплоем
В следующем уроке мы разберём другой класс ошибок — целочисленные ловушки, связанные со знаковыми числами в Tact.
Проверьте понимание
Закончили урок?
Отметьте его как пройденный, чтобы отслеживать свой прогресс
Войдите чтобы оценить урок