Learn how to check if anyone can drain TONs from your contract.
This tutorial walks you through detecting and preventing one of the most critical smart contract vulnerabilities: unauthorized fund withdrawals.
We'll intentionally create a vulnerable TON smart contract, then use the TSA Blueprint plugin to identify the security flaw. This hands-on approach helps developers understand how vulnerabilities emerge and how tools can catch them before deployment.
By the end, you'll know how to:
To demonstrate how the TON drain checker works, we'll start by deliberately creating a smart contract with a known vulnerability — specifically, one that could allow unauthorized withdrawal of funds.
Follow the steps below to set up the project and write the vulnerable contract.
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:
TonDrainDemoVulnerableContractAn empty contract (Tolk)We'll use this empty contract as a starting point to build our intentionally vulnerable example.
Enter the generated directory:
cd TonDrainDemoThe generated empty contract is located at contracts/vulnerable_contract.tolk.
Open this file and replace its contents with the following:
struct (0x12345678) Withdraw {
amount: coins
to: address
}
fun onInternalMessage(in: InMessage) {
if (in.body.isEmpty()) {
// just receive TONs
return;
}
val msg = lazy Withdraw.fromSlice(in.body);
val outMsg = createMessage({
dest: msg.to,
bounce: false,
value: msg.amount,
});
outMsg.send(SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE);
}Security Issue: This contract contains a critical vulnerability — it lacks authorization checks. Any user can send a Withdraw message to transfer TONs from the contract to any address. There's no verification that the sender is the contract owner or has permission to withdraw funds.
To detect and analyze security vulnerabilities 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 vulnerable contract. Run the TSA drain checker with the following command:
yarn blueprint tsa drain-check -c VulnerableContractBy 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 vulnerabilities.
Here's what the command options mean:
drain-check refers specifically to the TON drain checker — a specialized analyzer that searches only for execution paths where unauthorized fund withdrawal could occur.-c VulnerableContract 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 drain-check --help
The analysis process includes:
If vulnerabilities are detected, the tool will report the findings and provide detailed information about how to reproduce the vulnerable execution paths.
After a moment, you'll receive a vulnerability 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
├── tsa-reproduce-config.json
└── typed-input.yaml
There are two options for proceeding:
We'll explore both approaches starting with manual investigation. The automated reproduction will be described in Section 4 of this tutorial.
Let's break down what each component of the report means:
summary.txt): A concise overview of the vulnerability — 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 vulnerability.report.sarif): A standardized format for static analysis results. This file contains comprehensive details about analyzed execution paths. For most use cases, you won't need to examine this file directly.The checker has successfully identified our missing authorization check — anyone can trigger the withdrawal function.
Before proceeding, ensure you have:
For this tutorial, you'll need at least 1.5 TestNet TONs to cover deployment and transaction fees.
The vulnerability report includes a command to reproduce the issue on-chain:
To reproduce the vulnerability on the blockchain, run:
> yarn blueprint tsa reproduce --config tsa/reports/run-[id]/tsa-reproduce-config.json
This configuration file (generated by tsa drain-check) contains all necessary data for the reproduction process.
yarn blueprint tsa reproduce --config tsa/reports/run-[id]/tsa-reproduce-config.jsonYou'll be prompted to:
testnetNext, you'll deploy the contract to TestNet. When asked:
noAnswer
noissues a fresh deployment of a contract under test that will receive a message that triggers the found vulnerability. This is outside of the scope for this tutorial, but you can also send a reproduction message to an already deployed contract. To do this, answer "yes" to this question and proceed according to prompts. The data of the input contract will be checked, so don't be afraid to type the wrong address — the plugin will report the wrong contract state.
0.5 TONs. This is the money you are about to withdrawYou'll then specify how much you're willing to spend on reproducing the vulnerability:
0.5 TONsTSA will now:
Once confirmed, you'll receive a transaction ID.
You can use the following explorers to view the transaction (TonViewer is recommended for better debugging tools):
TonViewer:
https://testnet.tonviewer.com/transaction/<transaction_id>
TonScan:
https://testnet.tonscan.org/tx/<transaction_id>
You can verify that more TONs were received than sent. This transaction is an evidence of the unauthorized withdrawal.
For deeper analysis, you can use:
Since we already understand this vulnerability (missing authorization), extensive investigation isn't necessary — but these tools are useful for analyzing more complex issues.
The vulnerability stems from missing authorization checks. To fix it, we need to:
Update contracts/vulnerable_contract.tolk with the following corrected code:
struct (0x12345678) Withdraw {
amount: coins
to: address
}
struct Storage {
owner: address
}
fun Storage.load() {
return Storage.fromCell(contract.getData())
}
fun checkOwner(sender: address) {
val storage = lazy Storage.load();
assert(sender == storage.owner) throw 501;
}
fun onInternalMessage(in: InMessage) {
if (in.body.isEmpty()) {
return; // just receive TONs
}
checkOwner(in.senderAddress);
val msg = lazy Withdraw.fromSlice(in.body);
val outMsg = createMessage({
dest: msg.to,
bounce: false,
value: msg.amount,
});
outMsg.send(SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE);
}Re-run the TSA drain checker to confirm the vulnerability is resolved:
yarn blueprint tsa drain-check -c VulnerableContractExpected Result: The tool should report that no vulnerabilities were found, confirming our security fix is effective.
In this tutorial, you've learned how to use the TSA TON drain checker to identify and fix a critical smart contract vulnerability. You've successfully:
The TSA plugin offers additional security checkers beyond drain analysis — explore them with yarn blueprint tsa --help to strengthen your contract security further.
Keep building securely!