ton-vulnerability-scanner — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited ton-vulnerability-scanner (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Systematically scan TON blockchain smart contracts written in FunC for platform-specific security vulnerabilities related to boolean logic, Jetton token handling, and gas management. This skill encodes 3 critical vulnerability patterns unique to TON's architecture.
.fc, .func;; FunC contract indicators
#include "imports/stdlib.fc";
() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
;; Contract logic
}
() recv_external(slice in_msg) impure {
;; External message handler
}
;; Common patterns
send_raw_message()
load_uint(), load_msg_addr(), load_coins()
begin_cell(), end_cell(), store_*()
transfer_notification operation
op::transfer, op::transfer_notification
.store_uint().store_slice().store_coins()contracts/*.fc - FunC contract sourcewrappers/*.ts - TypeScript wrapperstests/*.spec.ts - Contract testston.config.ts or wasm.config.ts - TON project configWhen invoked, I will:
When vulnerabilities are found, you'll get a report like this:
=== TON VULNERABILITY SCAN RESULTS ===
Project: my-ton-contract
Files Scanned: 3 (.fc, .tact)
Vulnerabilities Found: 2
Coverage: 3/3 patterns reported
1 Integer as Boolean .............. found contracts/wallet.fc:45
2 Fake Jetton Contract ............ found contracts/staking.fc:85
3 Forward TON Without Gas Check ... clear forward amounts fixed at 0.05 TON
---
[CRITICAL] Fake Jetton Contract - Missing Sender Validation
File: contracts/staking.fc:85
Pattern: transfer_notification sender not checked against the stored Jetton walletI check for 3 critical vulnerability patterns unique to TON. For detailed detection patterns, code examples, mitigations, and testing strategies, see VULNERABILITY_PATTERNS.md.
transfer_notification sender not validatedFor complete vulnerability patterns with code examples, see VULNERABILITY_PATTERNS.md.
.fc or .func files)# Find boolean-like variables
rg "int.*is_|int.*has_|int.*flag|int.*enabled" contracts/
# Check for positive integers used as booleans
rg "= 1;|return 1;" contracts/ | grep -E "is_|has_|flag|enabled|valid"
# Look for NOT operations on boolean-like values
rg "~.*\(|~ " contracts/For each boolean:
# Find transfer_notification handlers
rg "transfer_notification|op::transfer_notification" contracts/For each Jetton handler:
# Find forward amount usage
rg "forward_ton_amount|forward_amount" contracts/
rg "load_coins\(\)" contracts/
# Find send_raw_message calls
rg "send_raw_message" contracts/For each outgoing message:
TON contracts require thorough manual review:
~, &, | operatorsReport on every pattern in §6, whether or not it turned anything up. Emit this table above the findings, with all 3 rows present:
| # | Pattern | Verdict | Evidence |
|---|---|---|---|
| 1 | Integer as Boolean | clear | searched is_/has_/flag; all set to -1 |
| 2 | Fake Jetton Contract | ||
| 3 | Forward TON Without Gas Check |
Each verdict is one of:
file:line and write the finding up in full below.address, or check you searched for, so a reader can repeat the search.
transfer notifications"). Not having looked is not n/a.
Three patterns is a short list, which makes an incomplete table harder to excuse rather than easier: a report covering one pattern and silent on the other two reads exactly like a clean contract. Emit all three rows even when all three are clear. A row whose Verdict cell is empty is incomplete in the same way: row 1 above is filled in to show the shape, and every row is filled in the same way before the report is done.
## [CRITICAL] Fake Jetton Contract - Missing Sender Validation
**Location**: `contracts/staking.fc:85-95` (recv_internal, transfer_notification handler)
**Description**:
The `transfer_notification` operation handler does not validate that the sender is the expected Jetton wallet contract. Any attacker can send a fake `transfer_notification` message claiming to have transferred tokens, crediting themselves without actually depositing any Jettons.
**Vulnerable Code**:// staking.fc, line 85 if (op == op::transfer_notification) { int jetton_amount = in_msg_body~load_coins(); slice from_user = in_msg_body~load_msg_addr();
;; WRONG: No validation of sender_address! ;; Attacker can claim any jetton_amount
credit_user(from_user, jetton_amount); }
**Attack Scenario**:
1. Attacker deploys malicious contract
2. Malicious contract sends `transfer_notification` message to staking contract
3. Message claims attacker transferred 1,000,000 Jettons
4. Staking contract credits attacker without checking sender
5. Attacker can now withdraw from contract or gain benefits without depositing
**Proof of Concept**:// Attacker sends fake transfer_notification const attackerContract = await blockchain.treasury("attacker");
await stakingContract.sendInternalMessage(attackerContract.getSender(), { op: OP_CODES.TRANSFER_NOTIFICATION, jettonAmount: toNano("1000000"), // Fake amount fromUser: attackerContract.address, });
// Attacker successfully credited without sending real Jettons const balance = await stakingContract.getUserBalance(attackerContract.address); expect(balance).toEqual(toNano("1000000")); // Attack succeeded
**Recommendation**:
Store expected Jetton wallet address and validate sender:global slice jetton_wallet_address;
() recv_internal(...) impure { load_data(); ;; Load jetton_wallet_address from storage
slice cs = in_msg_full.begin_parse(); int flags = cs~load_uint(4); slice sender_address = cs~load_msg_addr();
int op = in_msg_body~load_uint(32);
if (op == op::transfer_notification) { ;; CRITICAL: Validate sender throw_unless(error::wrong_jetton_wallet, equal_slices(sender_address, jetton_wallet_address));
int jetton_amount = in_msg_body~load_coins(); slice from_user = in_msg_body~load_msg_addr();
;; Safe to credit user credit_user(from_user, jetton_amount); } }
**References**:
- building-secure-contracts/not-so-smart-contracts/ton/fake_jetton_contractimport { Blockchain } from "@ton/sandbox";
import { toNano } from "ton-core";
describe("Security tests", () => {
let blockchain: Blockchain;
let contract: Contract;
beforeEach(async () => {
blockchain = await Blockchain.create();
contract = blockchain.openContract(await Contract.fromInit());
});
it("should use correct boolean values", async () => {
// Test that TRUE = -1, FALSE = 0
const result = await contract.getFlag();
expect(result).toEqual(-1n); // True
expect(result).not.toEqual(1n); // Not 1!
});
it("should reject fake jetton transfer", async () => {
const attacker = await blockchain.treasury("attacker");
const result = await contract.send(
attacker.getSender(),
{ value: toNano("0.05") },
{
$$type: "TransferNotification",
query_id: 0n,
amount: toNano("1000"),
from: attacker.address,
}
);
expect(result.transactions).toHaveTransaction({
success: false, // Should reject
});
});
it("should validate gas for forward amount", async () => {
const result = await contract.send(
user.getSender(),
{ value: toNano("0.01") }, // Insufficient gas
{
$$type: "Transfer",
to: recipient.address,
forward_ton_amount: toNano("1"), // Trying to forward 1 TON
}
);
expect(result.transactions).toHaveTransaction({
success: false,
});
});
});// Test with real Jetton wallet
it("should accept transfer from real jetton wallet", async () => {
// Deploy actual Jetton minter and wallet
const jettonMinter = await blockchain.openContract(JettonMinter.create());
const userJettonWallet = await jettonMinter.getWalletAddress(user.address);
// Set jetton wallet in contract
await contract.setJettonWallet(userJettonWallet);
// Real transfer from Jetton wallet
const result = await userJettonWallet.sendTransfer(
user.getSender(),
contract.address,
toNano("100"),
{}
);
expect(result.transactions).toHaveTransaction({
to: contract.address,
success: true,
});
});building-secure-contracts/not-so-smart-contracts/ton/Before completing TON contract audit:
Boolean Logic (HIGH):
~, &, | uses correct valuesJetton Security (CRITICAL):
transfer_notification handler validates sender addressGas & Forward Amounts (HIGH):
msg_value >= tx_fee + forward_amountsend_raw_message flags usedTesting:
found, clear or n/a with a reasonn/a costs oneclause and makes the judgment reviewable. Silence records nothing, and a reader cannot tell it apart from not having checked. With only three patterns, there is no version of this scan too large to complete.
the reason the table matters, not an excuse for skipping it. Manual review is the method here; the table is what makes it auditable.
not a precondition for starting it. Rank by severity after the table is complete, not by leaving rows out.
That table is the deliverable: it is what distinguishes a contract that was examined from one that was glanced at.
contract can send a transfer notification; the check is a comparison against an address the contract itself computed, and its absence is the fake-Jetton bug.
enough for the remaining execution strands the contract mid-operation regardless of bounce behavior.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.