All articles
Comparisons·June 9, 2026·6 min read

FunC vs Tact: Which TON Language Is Easier to Audit

FunC vs Tact security audit: comparing bounce handling, gas fee mistakes, and message-flow bugs to see which TON language cuts audit findings and turnaround.

A FunC contract and a Tact contract can implement the exact same jetton transfer logic and still hand an auditor two completely different jobs. We've run both through review at TierZero, and the finding count is not close. FunC audits average more findings per KLOC on message-handling and gas accounting; Tact audits skew toward logic and business-rule bugs because the language already closed off a chunk of the low-level failure modes. That difference is the whole story of this comparison, and it matters if you're deciding what to build in before you ever get to smart contract audit time.

Why TON auditing is different from EVM or Solana

TON is actor-model, asynchronous, and every contract-to-contract call is a message that can bounce, get delayed, or arrive out of order relative to what you assumed. There's no atomic multi-contract transaction like on EVM. A jetton transfer touches three or four contracts across separate transactions, each with its own gas budget, and if one of those messages fails partway through, funds can get stuck or duplicated depending on how carefully the contract handles bounces and exit codes. This is the terrain both FunC and Tact sit on. The language doesn't change TON's execution model — it changes how many ways you have to shoot yourself in the foot while working within it.

FunC: full control, full responsibility

FunC compiles close to TVM assembly. You manage the cell/slice/builder API by hand, you write your own message parsing with begin_parse() and load_uint(), and you decide manually whether every incoming message is bounceable and what happens if it bounces back. That's powerful — you can hand-tune gas costs cell by cell — but it means every contract effectively reinvents message safety from scratch.

The recurring findings we see in FunC audits:

  • Missing bounce checks. A contract accepts an inbound message without checking the bounced flag, so a failed downstream call gets processed as if it succeeded, double-crediting a balance.
  • Unhandled exit codes. Custom throw_unless codes that overlap with reserved TVM exit codes (values below 128 are reserved for TVM itself), causing a contract to silently misinterpret an error.
  • Storage fee starvation. Forgetting to reserve enough TON for storage rent via raw_reserve, so a contract that looks solvent on deploy gets garbage-collected months later because nobody topped it up.
  • Manual cell layout mistakes. Off-by-one bit or ref count when hand-packing a cell, which doesn't fail at compile time — it fails at runtime with a cryptic cell underflow exception, sometimes only on specific input sizes.

None of this is exotic. It's mechanical, repetitive, and exactly the kind of thing a checklist and a careful pass catch — which is also why FunC audits take longer per line. You're verifying discipline, not just logic.

;; classic FunC gap: no bounce check, no minimum gas check
() recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {
    slice cs = in_msg.begin_parse();
    int flags = cs~load_uint(4);
    slice sender = cs~load_msg_addr();
    ;; missing: if (flags & 1) { return (); } to skip bounced messages
    int op = in_msg_body~load_uint(32);
    if (op == 0x0f8a7ea5) {
        ;; jetton transfer logic runs even on a bounced message
    }
}

Tact: guardrails baked into the compiler

Tact is a newer, higher-level language that compiles to FunC/TVM under the hood but adds structure: typed messages via receive() handlers, automatic serialization for structs, and — critically — automatic bounce handling unless you explicitly opt out. The compiler generates a lot of the boilerplate that FunC developers write by hand, and it generates it consistently, which is the actual audit benefit. Consistency is auditable; ad hoc correctness isn't.

What Tact removes from the finding list:

  • Message parsing errors. Typed message structs with the Receiver pattern mean the compiler generates the cell parsing. You don't hand-roll load_uint calls, so there's no off-by-one bit bug to find.
  • Default bounce safety. Tact contracts bounce by default on unhandled exceptions unless you mark a receiver non-bounceable, which flips the FunC failure mode (forgot to add safety) into a Tact failure mode (explicitly and visibly turned safety off) — much easier to flag in review.
  • Type confusion between message types. Op-code dispatch is generated from the type system rather than matched by hand against magic numbers, so the "wrong branch for this op" class of bug mostly disappears.

What it does NOT remove:

  • Gas and fee math still has to be checked by hand. Tact doesn't know your business logic, so it can't tell you that you under-priced a forward fee for a three-hop message chain.
  • Storage fee reserves are still a manual decision. SendRemainingValue vs a hardcoded amount is still a judgment call the compiler won't flag.
  • Access control and reentrancy-adjacent logic are unchanged. Tact's type safety doesn't touch the actual state machine of your contract; a bad admin-check or a missing sequence-number guard is just as invisible to the compiler as it is in FunC.

Side-by-side on audit criteria

Criterion FunC Tact
Message parsing safety Manual, error-prone Compiler-generated, typed
Bounce handling Opt-in, easy to forget Opt-out, default-safe
Gas/fee estimation Fully manual either way Fully manual either way
Storage rent handling Manual raw_reserve calls Manual, same burden
Exit code collisions Real risk (<128 reserved range) Lower, compiler manages more codes
Typical findings per audit Higher, more mechanical bugs Lower, more logic-level bugs
Audit turnaround Longer per KLOC Shorter per KLOC, same depth
Ecosystem maturity / tooling Older, more precedent, more auditors fluent Younger, fewer historical incidents to reference

Which to pick when

If you're building something where every unit of gas and every cell layout matters — a high-frequency router, a custom DEX core, anything where you're squeezing TVM performance — FunC still gives you control Tact abstracts away, and a competent team can write it safely. But budget more audit time and more review cycles for it; the mechanical bug surface is real and it costs findings.

If you're building a standard jetton, an NFT collection, a vault, or anything where correctness matters more than shaving compute units, Tact is the better default in 2026. The guardrails remove a whole category of the bugs we catch in manual security review against automated scanners, and that translates directly into fewer rounds between our team and yours before sign-off.

Either way, language choice doesn't replace review. We've flagged serious logic bugs in Tact contracts that compiled clean and passed every type check, because the type system has nothing to say about whether your admin function checks the right address. If you're weighing where review should happen in your build process, our breakdown of in-house review versus third-party audit for TON contracts covers the tradeoff in more depth, and for teams also shipping on EVM or Solana, our comparison of formal verification against manual audit approaches is worth reading before you lock in a review process across chains.

Before you pick a language for your next TON deployment, get a second opinion from someone who's read the bytecode both ways — our strategy consultation call covers language choice, architecture, and audit scope in one session, or you can go straight to a full code review and audit if the contract's already written.

If you want the finding count down and the deployment date real, start with a smart contract audit scoped to whichever language you actually shipped.

Need a bot like this built?

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

Start a project
#TON#FunC#Tact#smart contract audit#comparisons