TON Smart Contract Audit: What FunC and Tact Auditors Check
A TON smart contract audit checks bounce handling, message gas flow, and TVM quirks that generic EVM checklists miss.
Why TON audits don't look like Solidity audits
A FunC or Tact auditor opens a contract and the first thing they check isn't reentrancy — it's whether the contract can lose a message. TON's actor model means every contract is its own isolated shard, talking to other contracts exclusively through asynchronous messages that can arrive out of order, get delayed, or bounce back undelivered. None of that exists on EVM chains, where a call either succeeds atomically or the whole transaction reverts. Bring an Ethereum checklist to a TON audit and you'll miss the bugs that actually drain funds.
We've audited jetton contracts, NFT collections, and DeFi routers on TON, and the failure patterns repeat. Here's what an experienced auditor actually walks through, in the order it matters.
Bounce handling: the number one killer
Every outbound message in TON can bounce. If the receiving contract doesn't exist, runs out of gas, or throws during execution, the network sends a bounce message back to the sender — but only if the original message had bounce = true (the default) and enough gas was attached to cover the return trip.
The bug we see constantly: a contract sends TON or jettons to a recipient, updates internal state assuming success, and never implements bounced() handling. If that message bounces, the funds return to the contract's balance, but the internal ledger still thinks they left. Now you've got phantom accounting — a user's recorded balance says zero but the contract's TON balance says otherwise, or worse, a double-spend path opens up because state was optimistically decremented twice.
Auditors specifically test:
- Does every message that carries value have a corresponding
bounced()branch, or explicit justification forbounce = false? - Is state reverted correctly when a bounce is processed, including partial reverts for multi-step operations?
- Bounced messages arrive with only the first 224 bits of the original payload (per the current TVM spec) — does the handler try to read fields beyond that and silently misparse?
A minimal jetton wallet transfer in FunC should look something like this, with the bounce path treated as a first-class code branch, not an afterthought:
() 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) {
;; this is a bounced message — reverse the state change
;; we optimistically applied when we sent the original transfer
on_bounce(in_msg_body);
return ();
}
;; normal message handling continues
}
Skip that branch and you have a contract that silently leaks accounting integrity under network conditions that happen routinely, not edge cases.
Message flow and the "who pays for gas" problem
TON has no global gas auction the way Ethereum does. Every message carries its own attached TON to cover the gas of processing it on the destination contract, and if that contract needs to forward a message onward — say, a jetton wallet notifying a DEX contract of an incoming transfer — it needs to have reserved enough for that next hop too.
Auditors trace the entire message chain for multi-hop operations, not just the entry point. A typical jetton transfer touches at minimum three contracts: sender's jetton wallet, receiver's jetton wallet, and often a notification callback to a receiving contract like a DEX pool. Each hop needs its own gas budget carved out of the original msg_value, and if the auditor finds a fixed gas constant instead of a computed reserve, that's a flag — gas costs on TON shift with masterchain congestion and contract complexity, and hardcoded values that were fine at deployment become insufficient after a network fee adjustment.
This is also where send_raw_message mode flags matter more than people expect. Mode 64 (carry remaining value from the incoming message) versus mode 128 (carry the entire contract balance) versus mode 0 (exact amount) produce very different behavior under load, and using mode 128 in the wrong place is a straightforward way to drain a contract's balance to whoever triggers the code path.
Storage rent and the abandoned-contract problem
TON contracts pay rent from their own balance for the storage they occupy. Let a contract's balance run to zero and it gets frozen; recovering it requires an unfreeze transaction with proof of the last state, which most users never do. Auditors check whether contracts that are supposed to be long-lived — jetton minters, NFT collections, escrow contracts — have any mechanism guaranteeing minimum balance, and whether fee-collection logic accidentally sweeps the contract down to a level where rent starvation becomes a real risk within months, not years.
TVM quirks that don't map to any EVM opcode
A few TVM-specific behaviors show up in nearly every finding list we write:
- Cell overflow and depth limits. Cells hold a maximum of 1023 bits and 4 references. Auditors check serialization logic for user-controlled data that could grow a cell past that limit, causing a contract to permanently reject valid input rather than gracefully failing.
- Integer over/underflow in FunC. FunC integers are 257-bit signed by default with no automatic overflow checks like Solidity 0.8+. Every arithmetic operation on user-supplied amounts needs an explicit bounds check.
- Exit codes as the only error signal. There's no revert string. Auditors verify that meaningful exit codes are used consistently (not everything dumped into a generic 0xffff) so that off-chain systems and other contracts calling in can distinguish failure modes.
- Tact's safety nets versus FunC's bare metal. Tact adds compile-time checks — message receiver exhaustiveness, typed structs — that eliminate whole bug classes FunC lets you write freely. We still audit Tact contracts as if those guarantees might be wrong, because the compiler is younger and less battle-tested than FunC's toolchain.
What this means for anyone launching on TON
If you're comparing this to what an audit costs on Solana or EVM, TON audits generally run a similar range but take longer per contract because of the message-flow tracing — see our breakdown in smart contract audit cost across EVM, Solana, and TON for real numbers. If you're building jettons or NFTs specifically, the standard's own quirks around wallet code hashes and metadata are worth reading before you write a line of code — we cover those in jetton and NFT standard security pitfalls. And if you're still deciding whether an audit is necessary before mainnet, we laid out the actual risk math in do you need a smart contract audit before launch.
None of this replaces a scoped review by someone who's read the TVM whitepaper more than once. If you want a second set of eyes before you ship, our TON smart contract audit process walks the bounce paths, gas budgets, and TVM edge cases described here line by line against your actual code.
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