Перейти к содержанию
Learning Platform
Глоссарий Troubleshooting
Урок 11.01 · 25 мин
Продвинутый
SecurityBounced MessagesError HandlingTON

Bounced Messages: обработка ошибок

Обработка bounced-сообщений — это первая линия обороны от потери средств в TON. Каждый раз, когда ваш контракт отправляет сообщение с TON другому контракту и тот не может его обработать, средства возвращаются как bounced-сообщение. Если ваш контракт не обрабатывает bounce — деньги пользователя исчезают в «чёрной дыре».

В модуле M02 мы разобрали асинхронную модель сообщений TON: контракты общаются друг с другом через внутренние сообщения, и каждое сообщение обрабатывается в отдельной транзакции. Но что происходит, когда получатель не может обработать сообщение?

TON предусматривает механизм bounced messages — автоматический ответ, сигнализирующий об ошибке. Без правильной обработки этих сообщений контракт может потерять средства или оказаться в несогласованном состоянии.


Когда возникает bounce

Bounced-сообщение отправляется автоматически, когда:

  1. Получатель выбрасывает исключение — функция require() не прошла, вызван throw(), или произошла ошибка выполнения
  2. Недостаточно газа — значение msg_value не покрывает стоимость выполнения у получателя
  3. Контракт не развёрнут — сообщение пришло на адрес без кода (пустой аккаунт)

При этом bounce происходит только если в исходном сообщении установлен флаг bounce: true (по умолчанию в TON это так для внутренних сообщений).

Контракт A                          Контракт B
    |                                    |
    |--- transfer (bounce: true) ------->|
    |                                    | throw("Error!")
    |<--- bounced message ---------------|
    |                                    |
    | bounced handler                    |
    | восстанавливает состояние          |

Уязвимый контракт: Jetton Wallet без bounced-handler

Рассмотрим классическую уязвимость — Jetton Wallet, который отправляет перевод токенов, но не обрабатывает bounce.

// УЯЗВИМЫЙ КОД -- НЕ ИСПОЛЬЗОВАТЬ В ПРОДАКШЕНЕ
contract VulnerableJettonWallet {
    balance: Int as coins;
    owner: Address;

    receive(msg: TokenTransfer) {
        require(sender() == self.owner, "Not owner");
        require(self.balance >= msg.amount, "Insufficient balance");

        // Уменьшаем баланс ПЕРЕД отправкой
        self.balance -= msg.amount;

        // Отправляем перевод получателю
        send(SendParameters{
            to: msg.destination,
            value: 0,
            mode: SendRemainingValue,
            bounce: true,
            body: InternalTransfer{
                amount: msg.amount,
                from: self.owner
            }.toCell(),
        });
    }

    // НЕТ bounced-handler!
    // Если получатель упадёт, баланс не восстановится
}
DANGER

Критическая уязвимость: сгорание токенов

Если Jetton Wallet отправляет transfer и получатель выбрасывает исключение, баланс отправителя уже уменьшен, а получатель токены не получил. Без bounced-handler токены фактически сгорают — они списаны у отправителя, но не зачислены получателю.


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

В Tact bounced-handler объявляется через ключевое слово bounced. Обратите внимание: в модуле M03, когда мы писали первые контракты на Tact, мы уже видели эту конструкцию — теперь разберём её подробно.

contract SecureJettonWallet {
    balance: Int as coins;
    owner: Address;

    receive(msg: TokenTransfer) {
        require(sender() == self.owner, "Not owner");
        require(self.balance >= msg.amount, "Insufficient balance");

        self.balance -= msg.amount;

        send(SendParameters{
            to: msg.destination,
            value: 0,
            mode: SendRemainingValue,
            bounce: true,
            body: InternalTransfer{
                amount: msg.amount,
                from: self.owner
            }.toCell(),
        });
    }

    // Bounced handler: восстанавливаем баланс при ошибке
    bounced(src: bounced<InternalTransfer>) {
        self.balance += src.amount;
    }
}

При получении bounced-сообщения контракт восстанавливает ранее списанный баланс. Это гарантирует, что при любом исходе общий supply токенов остаётся корректным.

Проверка знанийKnowledge check
If a contract sends a message with `bounce: true` but does not implement a bounced handler, and the recipient throws an error, what happens to the funds or state changes that were already applied before sending?
ОтветAnswer
The state changes (such as balance deductions) persist because they were committed in the sender's transaction. The bounced message arrives back but is silently ignored since there is no handler. This results in permanent inconsistency -- for example, tokens are deducted from the sender but never credited to the recipient, effectively burning them.

Ограничения bounced-сообщений

Только первые 256 бит тела

Bounced-сообщение содержит только первые 256 бит оригинального тела сообщения (плюс 32-битный флаг 0xFFFFFFFF). Это значит, что сложные данные из оригинального сообщения будут потеряны.

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

  1. Полностью игнорируют bounced-сообщения: это самая опасная ошибка, приводящая к необратимой потере средств пользователей.
  2. Обрабатывают bounce, но не откатывают состояние (например, не возвращают internal balance после неудачного transfer).
  3. Забывают, что тело bounced-сообщения обрезано до 256 бит: восстановить полную информацию оригинала невозможно.
  4. Отправляют сообщения с флагом non-bounceable без крайней необходимости, что лишает контракт возможности узнать о неудаче.

WARNING

Ограничение тела bounced-сообщения

Если ваше сообщение содержит данные за пределами первых 256 бит (ссылки на другие ячейки, длинные строки), эти данные не попадут в bounced-handler. Проектируйте сообщения так, чтобы ключевые поля для восстановления (amount, id) находились в начале тела.

В Tact это отражено в типе bounced<T> — компилятор автоматически ограничивает доступные поля.

Цепные bounce-сообщения не поддерживаются

Bounced-сообщение само отправляется с флагом bounce: false. Это значит, что если обработка bounced-сообщения тоже упадёт, повторного bounce не будет — сообщение будет потеряно.

A ---(bounce: true)---> B     B выбрасывает ошибку
A <---(bounce: false)--- B    Bounced message
   |
   | Если обработка bounce у A
   | тоже упадёт -- сообщение
   | потеряно навсегда!

Поэтому bounced-handler должен быть максимально простым — только восстановление состояния, без сложной логики.


Gas для bounce

Bounced-сообщение оплачивается из msg_value оригинального сообщения. Если отправитель приложил слишком мало газа, bounce-сообщению может не хватить средств для доставки.

Правило: при отправке сообщений, которые могут быть bounced, закладывайте достаточно газа не только на выполнение, но и на обратный путь bounce-сообщения.


Паттерны обработки bounced-сообщений

Паттерн 1: Восстановление баланса (Jetton 2.0)

Стандартный паттерн из Jetton 2.0 — восстановить баланс при неудачном переводе:

bounced(src: bounced<InternalTransfer>) {
    // Восстанавливаем баланс, списанный при отправке
    self.balance += src.amount;
}

Паттерн 2: Отмена операции (NFT Transfer)

При передаче NFT — вернуть владение обратно:

bounced(src: bounced<NftTransfer>) {
    // Передача не состоялась -- возвращаем владельца
    self.owner = src.prev_owner;
}

Паттерн 3: Маркировка транзакции как неудачной

Для сложных операций — пометить транзакцию как failed для последующей обработки:

bounced(src: bounced<ProcessPayment>) {
    // Помечаем платёж как неудавшийся
    self.pendingPayments.set(src.paymentId, false);
}

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

  1. Всегда пишите bounced-handler для операций, которые изменяют состояние перед отправкой сообщения
  2. Держите bounced-handler простым — только восстановление состояния, никакой сложной логики
  3. Критичные данные — в начале тела сообщения (первые 256 бит)
  4. Закладывайте газ на обратный путь bounce-сообщения
  5. Тестируйте сценарий отказа — убедитесь, что после bounce контракт возвращается в корректное состояние

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

Bounce-обработка — частный случай более общей проблемы: безопасность и upgradability контракта на TON. Системный взгляд на security-модель (модель угроз, права доступа, upgrade-стратегии, операционные риски) — в курсе по System Design TON.

System Design: security и upgradability

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

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

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

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