Learn how to audit which opcodes in your contract lack authorization checks.
This tutorial shows you how to use the Opcode Authorization Checker to discover which message opcodes in your contract can be invoked by anyone — and which are properly restricted.
Unlike other TSA checkers that give a binary "vulnerable / not vulnerable" verdict, opcode-info produces a summary table of every opcode's authorization status. This makes it a great first step when auditing an unfamiliar contract.
By the end, you'll know how to:
We'll build a minimal jetton-minter-style contract that stores admin-only content — but "forget" the authorization check.
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:
OpcodeInfoDemoSimpleMinterAn empty contract (Tolk)Enter the generated directory:
cd OpcodeInfoDemoThe generated empty contract is located at contracts/simple_minter.tolk.
Open this file and replace its contents with the following:
struct (0x00000004) ChangeMinterContent {
queryId: uint64
newContent: cell
}
struct Storage {
adminAddress: address
content: cell
}
fun Storage.load() {
return Storage.fromCell(contract.getData())
}
fun Storage.save(self) {
contract.setData(self.toCell())
}
fun onInternalMessage(in: InMessage) {
if (in.body.isEmpty()) {
return; // accept empty messages (top-up)
}
val msg = lazy ChangeMinterContent.fromSlice(in.body);
// BUG: no check that in.senderAddress == storage.adminAddress
var storage = lazy Storage.load();
storage.content = msg.newContent;
storage.save();
}Security issue: The ChangeMinterContent handler updates the on-chain content without verifying that the sender is the admin. Any user can overwrite the content.
Refer to the TSA Blueprint Plugin Installation Guide for instructions.
Analyze the contract by running:
yarn blueprint tsa opcode-info -c SimpleMinterBy 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.
The checker performs two phases:
After the analysis finishes, you'll see output similar to:
Extracted opcodes: [0x00000004]
Opcode Authorization Analysis:
0x00000004: ⚠️ No authorization checks
Path to reproducing input: tsa/reports/run-[id]/typed-input.yaml
The "No authorization checks" warning tells us that anyone can send a message with opcode 0x00000004 and have it processed — exactly the bug we planted.
The report uses two statuses:
The checker doesn't decide whether an unprotected opcode is a vulnerability — some opcodes (like RequestWalletAddress in a real jetton minter) are intentionally public. It's up to you to review the list and verify that every unprotected opcode is meant to be open.
When the checker flags an opcode, it generates a reproducing input in the tsa/reports/ directory.
tsa
└── reports
└── run-[id]
├── contract-data.boc
├── message-body.boc
├── report.sarif
├── summary.txt
└── typed-input.yaml
The raw BoC files contain the exact contract data and message body for the reproducing call.
typed-input.yaml combines both of them into a single typed representation, with top-level messageBody and contractData sections.
typed-input.yaml is the most convenient file for investigation — you'll see opcode 0x00000004 followed by the fields TSA chose to complete the call successfully.
The fix is straightforward: verify the sender's address before modifying state.
Update contracts/simple_minter.tolk:
const ERR_NOT_FROM_ADMIN: int = 73;
struct (0x00000004) ChangeMinterContent {
queryId: uint64
newContent: cell
}
struct Storage {
adminAddress: address
content: cell
}
fun Storage.load() {
return Storage.fromCell(contract.getData())
}
fun Storage.save(self) {
contract.setData(self.toCell())
}
fun onInternalMessage(in: InMessage) {
if (in.body.isEmpty()) {
return; // accept empty messages (top-up)
}
val msg = lazy ChangeMinterContent.fromSlice(in.body);
var storage = lazy Storage.load();
assert(in.senderAddress == storage.adminAddress) throw ERR_NOT_FROM_ADMIN;
storage.content = msg.newContent;
storage.save();
}Re-run the checker:
yarn blueprint tsa opcode-info -c SimpleMinterExpected output:
Extracted opcodes: [0x00000004]
Opcode Authorization Analysis:
0x00000004: ✅ Has authorization checks
The opcode is now properly protected. TSA confirms that a random sender can no longer invoke it successfully.
In this tutorial you've learned how to:
opcode-info to get a per-opcode authorization summary of your contractThe opcode authorization checker is especially useful as an audit starting point: run it once on any contract to get a quick overview of its access control surface, then dig deeper into any surprising results.