Skip to content

Bounced messages

When a contract sends a message with the flag bounce set to true, and if the message isn’t processed properly, it will bounce back to the sender. This is useful when you want to ensure that the message has been processed properly and, if not, revert the changes.

Yet, bouncing a message back incurs the same forward fees as sending a new message. If the received message does not have any value attached to it, e.g., it was sent with an optional flag SendPayFwdFeesSeparately and the value set to 0, it will not have any funds to bounce back upon errors in this transaction.

Caveats

Currently, bounced messages in TON can have at most 256 bits of payload and no references, but the amount of useful data bits when handling messages in bounced receivers is at most 224, since the first 32 bits are occupied by the message opcode. This means that you can’t recover much of the data from the bounced message, for instance you cannot fit an address into a bounced message, since regular internal addresses need 267 bits to be represented.

Tact helps you to check if your message fits the limit, and if it doesn’t, suggests using a special type constructor bounced<M> for the bounced message receiver, which constructs a partial representation of the message that fits within the required limits.

Note that the inner message struct of the bounced<M> type constructor cannot be optional.

contract Bounce() {
// COMPILATION ERROR! Only named type can be bounced<>
bounced(msg: bounced<BB?>) {}
// ~~~~~~~~~~~~
}
message BB {}

Since message fields are always laid out sequentially and no automatic rearrangements are made, the partial representation made by the bounced<M> constructor would only contain the first fields that fit entirely within the 224-bit limit.

All subsequent fields that exceed the limit will not be available in bounced message receivers. This also includes fields that fit only partially.

If the first field does not wholly conform to the 224-bit limit, none of the bounced message fields would be available. Therefore, to ensure access, prefer arranging the important fields first.

contract Bounce() {
bounced(msg: bounced<TwoFields>) {
// COMPILATION ERROR! Maximum size of the bounced message is 224 bits...
msg.f2;
// ~~
}
}
message TwoFields {
f1: Int as uint224; // only this field will fit in a bounced message body
f2: Int;
}

For gas optimization reasons, unrecognized bounced messages are ignored by Tact contracts and do not cause erroneous, non-zero exit codes. That is, if there is no relevant bounced() message receiver or no fallback bounced receiver, messages sent from the contract, rejected by their intended recipient and bounced back to the original contract will not be processed, apart from collecting their value and paying any related fees.

This behavior is unlike the non-bounced messages such as regular internal or external messages, where if the contract does not handle the message, an error with exit code 130 is thrown: Invalid incoming message. Not throwing such errors for bounced messages is a common pattern on TON Blockchain, to which Tact adheres.

Bounced message receiver

To receive a bounced message, define a bounced() receiver function in your contract or a trait:

contract MyContract {
bounced(msg: bounced<MyMessage>) {
// ...
}
}

Fallback bounced message receiver

To process bounced messages manually, you can use a fallback catch-all definition that handles a raw Slice directly. Note that such a receiver handles all bounced messages that are not handled by specific bounced message receivers.

contract MyContract() {
// Specific bounced message receiver
bounced(msg: bounced<MyMessage>) {
// ...
}
// Fallback catch-all bounced message receiver
bounced(rawMsg: Slice) {
// Here, rawMsg can be anything, except for the bounced<MyMessage>
}
}

On TON, bounced message bodies have a 32 bits prefix, where all bits are set, i.e., with 0xFFFFFFFF. However, since that prefix is always the same for all bounced messages, Tact cuts it off for your convenience, keeping only the remaining 256 bits of the payload, which usually starts with a 32-bit message opcode.

Contract storage handling

Bounced message receivers handle contract storage just as internal message receivers do. In addition, the empty return statement and the throw(0) patterns work the same.