Learn how to check if your contract processes bounced messages correctly.
This tutorial walks you through using the Bounced Messages Processing Checker to identify issues in how your contract deals with failed outgoing messages.
Before running the checker, you need a contract that contains flaws in its bounced message processing logic. This will help demonstrate what the tool detects and how to interpret its findings.
Before you begin, make sure you meet all the requirements for working with Blueprint. You can find the details in the Blueprint documentation.
Once everything is set up, create a new project by running:
npm create ton@latestYou'll be prompted to enter:
BounceDemoFlawedContractAn empty contract (Tolk)Enter the generated directory:
cd BounceDemoThe generated empty contract is located at contracts/flawed_contract.tolk.
Open this file and replace its contents with the following:
struct (0x12345678) ChangeValue {
amount: int64
}
struct (0x00000001) NotifyIncrease {
amount: uint64
}
struct (0x00000002) NotifyDecrease {
amount: uint64
}
struct Storage {
value: int64
notifyAddress: address
}
fun Storage.load() {
return Storage.fromCell(contract.getData())
}
fun Storage.save(self) {
contract.setData(self.toCell())
}
fun onInternalMessage(in: InMessage) {
val msg = lazy ChangeValue.fromSlice(in.body);
var storage = lazy Storage.load();
storage.value += msg.amount;
storage.save();
if (msg.amount > 0) {
val outMsg = createMessage({
bounce: BounceMode.Only256BitsOfBody,
dest: storage.notifyAddress,
value: 0,
body: NotifyIncrease {
amount: msg.amount as uint64
}
});
outMsg.send(SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE);
}
if (msg.amount < 0) {
val outMsg = createMessage({
bounce: BounceMode.Only256BitsOfBody,
dest: storage.notifyAddress,
value: 0,
body: NotifyDecrease {
amount: -msg.amount as uint64
}
});
outMsg.send(SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE);
}
}
fun onBouncedMessage(in: InMessageBounced) {
in.bouncedBody.skipBouncedPrefix();
val msg = lazy NotifyIncrease.fromSlice(in.bouncedBody);
var storage = lazy Storage.load();
storage.value -= msg.amount;
storage.save();
}The contract has an issue in its bounced message handling.
While it correctly processes bounced NotifyIncrease messages (reverting the value increase),
it only expects NotifyIncrease in the onBouncedMessage handler. If a NotifyDecrease message bounces back,
the contract attempts to parse it as NotifyIncrease, which causes a parsing error and makes the transaction fail.
This means that any failed NotifyDecrease notification will lead to a contract failure instead of proper state recovery.
To detect and analyze weaknesses like the one in our contract, we'll use the TSA plugin.
Refer to TSA Blueprint Plugin Installation Guide for instructions.
Now let's analyze our contract. Run the TSA bounce checker with the following command:
yarn blueprint tsa bounce-check -c FlawedContractBy default, built-in checkers are interactive: TSA asks for confirmation twice — first before preparation and timeout calculation, and then again before the analysis starts.
If you want to skip both prompts, add --no-interactive.
When you execute this command, TSA performs symbolic analysis of your contract. This means it systematically explores different execution paths to identify any that could lead to incorrect behavior when handling bounced messages.
First, the analyzer identifies which bounceable messages the contract can send. It then constructs the corresponding bounced messages and feeds them back to the contract to observe how it responds.
Here's what the command options mean:
bounce-check refers specifically to the bounced messages handling checker-c FlawedContract specifies the name of the contract to analyze.You can customize the analysis with additional options. To see all available parameters, run:
yarn blueprint tsa bounce-check --help
If incorrect behavior was detected, the tool will report the findings.
After a moment, you'll receive a report similar to this:
⚠️ Vulnerability found!
Summary path: tsa/reports/run-<id>/summary.txt
Typed input: tsa/reports/run-<id>/typed-input.yaml
SARIF with full information: tsa/reports/run-<id>/report.sarif
The generated files are stored in tsa/reports/run-<id>:
tsa
└── reports
└── run-<id>
├── contract-data.boc
├── message-body.boc
├── report.sarif
├── summary.txt
└── typed-input.yaml
Let's break down what each component of the report means:
summary.txt): A concise overview of the weakness — this is what was displayed in your terminal.typed-input.yaml): The recommended file for investigation. It contains the typed representation of the reproducing message body and contract data in one place.message-body.boc, contract-data.boc): The exact message body and contract data that reproduce the findings.report.sarif): A standardized format for static analysis results. This file contains comprehensive details about analyzed execution paths.Important: the generated inputs are those that trigger the contract to send a bounceable message, not the bounced message itself.
To understand the details about what happened in that execution, you can investigate SARIF report.
Update contracts/flawed_contract.tolk with the following corrected code:
struct (0x12345678) ChangeValue {
amount: int64
}
struct (0x00000001) NotifyIncrease {
amount: uint64
}
struct (0x00000002) NotifyDecrease {
amount: uint64
}
struct Storage {
value: int64
notifyAddress: address
}
fun Storage.load() {
return Storage.fromCell(contract.getData())
}
fun Storage.save(self) {
contract.setData(self.toCell())
}
fun onInternalMessage(in: InMessage) {
val msg = lazy ChangeValue.fromSlice(in.body);
var storage = lazy Storage.load();
storage.value += msg.amount;
storage.save();
if (msg.amount > 0) {
val outMsg = createMessage({
bounce: BounceMode.Only256BitsOfBody,
dest: storage.notifyAddress,
value: 0,
body: NotifyIncrease {
amount: msg.amount as uint64
}
});
outMsg.send(SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE);
}
if (msg.amount < 0) {
val outMsg = createMessage({
bounce: BounceMode.Only256BitsOfBody,
dest: storage.notifyAddress,
value: 0,
body: NotifyDecrease {
amount: -msg.amount as uint64
}
});
outMsg.send(SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE);
}
}
type BounceOpToHandle = NotifyIncrease | NotifyDecrease
fun onBouncedMessage(in: InMessageBounced) {
in.bouncedBody.skipBouncedPrefix();
val msg = BounceOpToHandle.fromSlice(in.bouncedBody);
val restoreAmount = match (msg) {
NotifyIncrease => msg.amount as int64,
NotifyDecrease => -msg.amount as int64,
};
var storage = lazy Storage.load();
storage.value -= restoreAmount;
storage.save();
}Re-run the checker to confirm the weakness is resolved:
yarn blueprint tsa bounce-check -c FlawedContractExpected Result: The tool should report that the weakness was not found, confirming our fix is effective.
In this tutorial, you've learned how to use the TSA Bounced Messages Handling Checker to identify and fix a critical smart contract weakness. You've successfully:
The TSA plugin offers additional security checkers beyond this one — explore them with yarn blueprint tsa --help to strengthen your contract security further.
Build with confidence!