TON Smart Contract Audit: What Auditors Check in FunC/Tact
A TON smart contract audit checklist: bounced message handling, cross-hop gas accounting, and TEP-74/62 compliance in FunC and Tact.
The message-passing model breaks your intuition first
Ethereum auditors think in call stacks. Solana auditors think in account validation. TON auditors have to think in an actor model where every contract is a mailbox, every state change is asynchronous, and a "failed transaction" doesn't revert — it bounces. That single difference accounts for more TON-specific bugs than reentrancy, overflow, and access control combined.
If you're building a jetton, an NFT collection, a DEX router, or a trading bot that signs and routes TON transactions, a TON smart contract audit has to cover ground that EVM checklists simply don't have categories for. Here's what actually gets checked, in the order a competent reviewer works through it.
1. Bounced message handling
On TON, when contract A sends a message to contract B and B's handler throws (or B doesn't exist, or gas runs out mid-execution), the TON VM doesn't roll the whole transaction back the way the EVM does. Instead, if the original message had the bounce flag set (the default), the network sends a bounced message back to A — an empty body prefixed with 0xFFFFFFFF and whatever coins weren't consumed, minus fees.
The bug shows up constantly: a jetton wallet debits its own balance, sends a transfer notification to the recipient, and only credits state changes assuming the send succeeds. If the recipient contract is broken, out of gas, or simply doesn't accept the jetton, the message bounces — and if the sender's recv_internal doesn't have a bounce handler that restores the debited balance, the tokens are gone. Not stolen. Just vaporized, sitting in limbo because nobody wrote the undo path.
A real audit checks, for every outbound message with side effects:
- Is
bounceset correctly for this message type? - Does the contract have an explicit branch for
op == 0combined with the bounced flag in the message header? - Does the bounce handler actually reverse the state mutation, not just log it?
In FunC this means checking the very first lines of recv_internal:
() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
slice cs = in_msg_full.begin_parse();
int flags = cs~load_uint(4);
if (flags & 1) {
;; bounced message — must undo, not just ignore
on_bounce(in_msg_body);
return ();
}
...
}
Skip this and you've built a contract that quietly leaks funds under load. This is the single most common finding we write up in a smart contract audit on TON, more common than any arithmetic bug.
2. Gas accounting across asynchronous hops
A TON transaction is really a chain of transactions — each message hop consumes its own gas, paid from the value attached to that specific message, not from a shared pool. Miscalculate the forward fee on a message that itself triggers a third message, and the chain dies partway through with funds stuck at an intermediate contract.
This matters enormously for jetton transfers, which are a minimum three-hop dance: sender wallet → receiver wallet → (optional) notification to receiver's owner contract. Each hop needs enough TON attached to cover:
- The forward fee for that specific hop (computed from message size and current network config, not a fixed constant)
- Gas for the destination contract's execution
- A buffer for the
send_modeflags being used —SEND_MODE_CARRY_ALL_REMAINING_GAS(128) behaves very differently from a hardcoded value in cases of low balance
Auditors recompute the worst-case gas path by hand against the TonAPI/toncenter gas config the contract will run under, not just eyeball whatever value the reference wallet code shipped with. Underfunding shows up as transactions that succeed on testnet (cheap gas, no congestion) and silently fail on mainnet under load — exactly the failure mode you can't catch by "it worked when I tried it."
3. Jetton and NFT standard compliance
TEP-74 (jettons) and TEP-62 (NFTs) are TON's ERC-20/ERC-721 equivalents, but compliance is checked at the byte level because wallets, DEXs, and explorers all parse these messages assuming exact op-code and layout conformance. A jetton wallet that gets the op codes right but returns the wrong query_id in a transfer notification will break integration with every DEX that indexes it, even though the token "works" in isolation.
What we verify against spec:
transfer(0xf8a7ea5),transfer_notification(0x7362d09c), andinternal_transfer(0x178d4519) op codes match exactly, including for custom extensions layered on top- The minter's
get_wallet_addressmethod returns a deterministic address derivable purely from(owner_address, jetton_master_address, jetton_wallet_code)— if it isn't, DEX routers that precompute the wallet address off-chain will send to the wrong place - Total supply accounting: mint and burn both update
total_supplyin the master contract, and burns are actually irreversible (no code path that lets a "burned" balance be re-credited) min_tons_for_storageis respected so wallets don't get deleted by TON's storage-fee garbage collection while holding a live balance
4. Tact-specific footguns
If the contract is written in Tact rather than FunC, the audit shifts focus. Tact's higher-level syntax hides some of the message-flow explicitness that makes FunC bugs visible, and its automatic getter/receiver generation can create unintended public entry points. We check that receive() handlers exhaustively cover expected message types (an uncovered type falls through to the empty receiver and silently accepts, which is rarely what you want for a contract holding funds), and that any native FunC interop hasn't reintroduced the exact bounce-handling gaps Tact's abstractions are supposed to prevent.
What this means for launch timing
None of this is optional polish. It's the difference between a contract that works in the demo and one that survives real trading volume and adversarial routing. If you're weighing whether you need an audit before mainnet launch, TON's async model is the strongest argument for yes — bugs here don't revert cleanly, they leave stuck funds and orphaned state that no amount of post-launch patching fully recovers. For a broader view of scope across chains, see what a smart contract audit actually checks and how audit costs compare across EVM, Solana, and TON.
If your contract also needs a broader line-by-line pass alongside the security review, pair the audit with a code review focused on gas efficiency and TEP compliance, or bring us in during development so the bounce-handling and gas-accounting patterns are correct from the first commit instead of retrofitted later.
Ready to get your FunC or Tact contract reviewed before it touches mainnet? Book a TON smart contract audit and we'll walk the message flow with you line by line.
Need a bot like this built?
We design, build and run trading bots on Solana, Hyperliquid and Polymarket.
Start a projectMore from the blog
Public vs Paid RPC: How to Run a Fair Benchmark
A fair comparison of public and paid RPC endpoints must account for quotas, workload, freshness and support guarantees.
Read articleRPC Benchmark Percentiles Explained: p50, p95 and p99
Why averages hide the latency spikes that break trading bots, wallets and production blockchain applications.
Read articleHyperliquid REST vs WebSocket Benchmark: What to Measure
A benchmark plan for Hyperliquid market data that separates request latency from streaming freshness and recovery.
Read article