All articles
Strategies·April 6, 2026·6 min read

TON Smart Contract Audits: Jetton, NFT & FunC/Tact Pitfalls

A TON smart contract audit checklist: FunC vs Tact pitfalls, jetton/NFT standard gaps, and bounced-message handling on the async TVM.

Why TON audits look nothing like EVM audits

Most of what the security industry knows about smart contract auditing was built on Solidity and a single-threaded, synchronous EVM. TON throws that model out. Every contract is an actor with its own persistent storage, and every cross-contract interaction is an asynchronous message that might land on a different shard, in a different block, seconds later — or not at all. A TON smart contract audit that treats FunC or Tact code like "Solidity with different syntax" will miss the bugs that actually drain funds on this chain: broken bounce handling, jetton wallets that trust the wrong sender, and gas math that assumes atomicity where none exists.

We've pulled apart enough TON codebases now to have a short list of failure patterns that show up over and over. None of them are exotic. All of them are expensive.

The actor model changes what "reentrancy" even means

On Ethereum, reentrancy is about a callback re-entering your function before state updates finish. On TON there's no callback stack — there's message passing between independent contracts, each with its own balance and storage. The equivalent danger is a contract that sends an outbound message assuming a downstream step succeeded, when TVM message delivery gives you no such guarantee.

Two mechanics matter more than anything else here:

  • Bounced messages. If a message you send fails on the receiving contract (out of gas, thrown exception, malformed payload) and the bounce flag was set, TON returns a truncated version of it to your contract with bounced = true and only the first 32 bits of the original op intact. If your contract doesn't explicitly check msg_flags for the bounce bit and handle it, a failed jetton transfer can leave your internal accounting showing tokens sent that never actually left, or — worse — tokens debited twice because the bounce handler re-runs the same debit logic.
  • Carry-value transfers. TON's gas model requires you to attach enough Toncoin to a message to cover the receiving contract's execution and its own onward messages. Jetton transfers routinely chain three or four hops (wallet → wallet → notification → excess return). Underestimate forward fees anywhere in that chain and a transfer silently fails partway through, sometimes leaving value stuck in an intermediate contract with no clean recovery path.

Here's the pattern we flag constantly in FunC jetton wallets:

() 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 be handled, this project just returns
        return ();
    }
    int op = in_msg_body~load_uint(32);
    if (op == op::transfer_notification) {
        ;; BUG: never verifies in_msg_full's source address
        ;; against the expected jetton-wallet address derived
        ;; from (jetton_master, owner) — accepts a notification
        ;; from ANY contract claiming to be a jetton wallet
        handle_incoming_transfer(in_msg_body);
    }
}

That missing sender check is the single most common critical finding in TON jetton integrations. Because jetton wallets are deployed per-owner via StateInit, any contract can forge a transfer_notification message claiming an arbitrary jetton and amount arrived, unless the receiving contract independently recomputes the expected wallet address from the jetton master's code and checks it against in_msg_full's source. Skip that, and you've built a fake-deposit exploit into a DEX, a vault, or a bot's payment listener.

FunC vs Tact: different footguns, not fewer of them

Tact was built to remove whole categories of FunC mistakes — it has real types, structured messages, and compiler-enforced exhaustive receiver matching instead of hand-rolled opcode dispatch. That genuinely eliminates a class of bugs: mismatched cell schemas, forgotten end_parse() calls that let malformed payloads through, and typos in manually maintained op-code constants.

But Tact introduces its own risk surface. Its default send() semantics and message mode constants (SendRemainingValue, SendIgnoreErrors, and so on) are easy to combine incorrectly, and SendIgnoreErrors in particular will silently swallow a failed forward that a FunC developer, writing the bounce logic by hand, would have been forced to think about. We've seen Tact contracts pass every unit test because the test harness never simulates a bounce, while the same contract loses funds in production the first time a downstream wallet is uninitialized. The language changed the failure mode, it didn't remove the need to reason about async delivery.

Where jetton and NFT standards actually get violated

TEP-74 (jettons) and TEP-62 (NFTs) are specifications, not runtimes — nothing stops a team from deploying code that claims compliance and fails it in ways that only surface under specific conditions:

  • Jetton wallets that don't preserve the forward_ton_amount / forward_payload contract, breaking any indexer or DEX router that depends on the notification format being exact.
  • NFT collections where get_nft_address_by_index uses a different derivation than the actual deployed item's StateInit, so marketplaces resolve to addresses that don't exist or don't match on-chain state.
  • Missing excesses message handling, which either strands gas in a contract or, in the worst case we've seen, lets a wallet accumulate unaccounted Toncoin that a later "cleanup" transaction sends to the wrong recipient because the excess-return logic was never actually tested against a real bounce.
  • Minter contracts that don't validate total_supply overflow on mint, which is a rounding error away from being an inflation bug once volume gets real.

None of this shows up by re-reading the spec side by side with the code once. It shows up by simulating the actual message graph — sender, intermediate wallets, notification, excess return — under adversarial inputs, including deliberately triggered bounces and deliberately underfunded gas.

What a TON-specific engagement should actually cover

If you're scoping this out, the checklist we'd want to see matches the general framework in our guide to choosing a smart contract auditor, with TON-specific line items layered on: bounce-path coverage for every outbound message, gas/forward-fee accounting across the full multi-hop transfer, sender-address verification against derived StateInit for every jetton and NFT interaction, and replay/seqno protection on external messages. If your project hasn't shipped yet, it's also worth reading through why pre-launch audits catch different things than post-incident reviews — TON's async model makes pre-deployment simulation far more valuable than it is on synchronous chains, because the bugs that matter are timing- and ordering-dependent in ways a single manual read-through won't surface.

Before any of that, most teams are better served by a short scoping call than by jumping straight into a fixed-price audit quote — a strategy consultation is where we map out which contracts touch jettons or NFTs, which ones handle carry-value chains, and where the real risk concentration sits before committing to a review timeline. And if the product includes a live trading or execution layer sitting on top of these contracts, get eyes on the trading dashboard integration too — a clean contract audit doesn't help if the off-chain layer misreads bounced or delayed messages as confirmed state.

Get a proper TON smart contract audit before mainnet, not after your jetton wallet forges its first fake deposit.

Need a bot like this built?

We design, build and run trading bots on Solana, Hyperliquid and Polymarket.

Start a project
#TON blockchain#smart contract audit#Jetton standard#FunC#Tact