All articles
Smart Contracts·March 27, 2026·7 min read

TON's Asynchronous Contracts vs EVM: Why Bugs Look Different

TON's async actor model vs EVM's synchronous call stack: why TON smart contracts trade reentrancy bugs for race conditions auditors must hunt manually.

TON does not have a call stack. That single fact explains more about the bug reports we've written on TON projects than anything else in the codebase. On Ethereum, when contract A calls contract B, the EVM pushes a new frame, B runs to completion or reverts, and control returns to A with a result sitting right there in the same transaction. On TON, contract A sends a message to contract B and moves on. B might process that message in the next block, three blocks later, or on a completely different shard. A never gets a return value the way a Solidity developer expects one. It gets another incoming message, later, if at all.

That gap — a synchronous call stack versus asynchronous actor mailboxes — is the biggest reason TON audits and EVM audits catch different bug families, even when the underlying business logic (escrow, vault, marketplace, staking) is nearly identical.

The EVM's synchronous guarantee

Solidity engineers get atomicity for free. A transaction either fully commits or fully reverts, and every external call inside it happens on the same stack, in the same block, with state changes visible to every subsequent line. This is why checks-effects-interactions works as a pattern at all: you can reason about "before the external call" and "after the external call" because both live inside one execution frame. The DAO hack in 2016 happened precisely because a contract made an external call before updating its balance, and that class of bug, reentrancy, is now something every competent Solidity reviewer checks for on reflex, usually with a nonReentrant modifier or a manual reorder of state writes.

The tradeoff is throughput. Every cross-contract interaction on Ethereum is a synchronous hop on the same execution thread, which is part of why complex multi-contract flows get expensive and why ordering within a single block matters so much to MEV searchers and sequencers — a related ordering problem shows up in our piece on Jito bundles versus the public mempool on Solana, a different chain but the same underlying tension between atomicity and throughput.

TON's actor model: no call stack, just mailboxes

TON was built on a different premise: every smart contract is an actor with its own persistent storage, and actors communicate exclusively by sending messages. A "transaction" on TON is one contract processing one incoming message and producing zero or more outgoing messages. Those outgoing messages get queued, routed, possibly across shardchains, and processed in a later block by the recipient. There is no atomic multi-contract transaction in the Ethereum sense. If you want contract A to update its own state, tell contract B to update its state, and have B tell contract C to do something, that's three separate transactions spread across an indeterminate number of blocks, and any of them can fail independently.

This is why TON added the bounce mechanism: if a message fails on the receiving contract (out of gas, throws an exception, wrong opcode), and the bounce flag was set, the funds and a bounced version of the message get sent back to the sender. It's TON's crude substitute for a revert, except the sender already committed its own state changes before it knew whether the downstream message would succeed. That's the crux of the whole problem.

Where the bugs actually hide

On EVM, the classic bug is reentrancy: an external call re-enters your contract before your own state update lands. On TON, the classic bug is a time-of-check-to-time-of-use race — your contract's state changes between when it sends a message and when it learns the outcome of that message, because other messages can be processed against that same contract in the meantime.

A jetton wallet (TON's fungible token standard) is the textbook case. Suppose a vault contract does this:

;; simplified vault contract, receiving a "withdraw" message
() recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {
    slice sender = in_msg_body~load_msg_addr();
    int amount = in_msg_body~load_coins();

    ;; optimistic: mark the withdrawal as settled immediately
    balances~udict_set(256, sender_hash, pack_amount(get_balance(sender) - amount));

    ;; then fire off the jetton transfer message
    send_message(sender, amount, op::transfer);
}

This looks like a direct port of a Solidity withdraw() that follows checks-effects-interactions correctly: debit the balance, then send funds. On EVM that ordering is exactly right and prevents reentrancy. On TON it's wrong in a different way. The balance debit is final and synchronous, but the transfer message is asynchronous and can still fail downstream — insufficient gas forwarded, a malformed recipient contract, a bounce. If it bounces, you need explicit bounce-handling logic to credit the balance back, and if you forgot that handler, or another message mutated balances in the window before the bounce came back, you end up with a state that no longer matches on-chain funds. We've seen exactly this shape of bug in real jetton wallet forks: developers copy the "debit-then-send" pattern from Solidity muscle memory and never write the bounce-recovery path, because nothing in FunC or Tact forces you to.

The other common TON-specific bug is a message-ordering assumption. Two messages sent to the same contract in one logical operation — say, a "reserve seat" message followed by a "confirm payment" message — are not guaranteed to arrive in send order if they take different routing paths or one gets delayed by shard congestion. Code that assumes confirm always arrives after reserve will occasionally process them out of order, and that's a race condition with no EVM equivalent, because there's no such thing as message reordering within a single Ethereum transaction: there's no messaging, just direct calls.

Side by side

Dimension EVM (Ethereum, L2s) TON
Execution model Synchronous call stack Asynchronous actor mailboxes
Cross-contract call Same transaction, same block Separate transactions, possibly different blocks/shards
Atomicity Whole tx reverts on failure Per-message; failure needs explicit bounce handling
Classic bug Reentrancy (call before state update) TOCTOU race between send and message outcome
Failure signal revert unwinds everything Bounced message, funds return but state may already have changed
Ordering guarantees Total order within a block No guaranteed order across messages/shards
Mature tooling Foundry, Hardhat, Slither, Mythril Blueprint, Sandbox — younger, fewer static analyzers
Gas model Single gas budget per tx Gas estimated and forwarded per hop; misestimate stalls the message

The tooling gap makes this worse, not just different

Foundry and Hardhat simulate a full multi-contract call chain in one deterministic run, and Slither or Mythril flag reentrancy patterns automatically. TON's equivalent stack, Blueprint and the Sandbox emulator, is genuinely good for unit-testing a single contract's message handler, but simulating a realistic multi-hop, multi-shard interaction with adversarial message timing is still mostly manual work. That's not a knock on the ecosystem, it's just younger — it means more of the race-condition hunting on TON happens by manually tracing message graphs and asking "what if this arrives out of order or bounces here" for every send, rather than leaning on an automated detector the way you can for reentrancy on EVM. That's exactly the kind of gap our smart contract audit work closes, and the same discipline applies during a broader code review and audit pass on any async system, including oracle resolution logic — see the similar "state committed before outcome is known" shape in Polymarket's UMA oracle versus Chainlink for market resolution.

Which to pick when

If your product needs strict, single-transaction atomicity across several contracts — a flash loan, a multi-step DeFi composability play, anything where "all of this happens or none of it does" is a hard requirement — build on EVM. That guarantee is baked into the execution model, not bolted on, and the tooling for catching the resulting bug class is mature enough that a competent audit will reliably catch reentrancy before mainnet.

If your product needs to scale to genuinely high throughput with predictable per-user latency — a game, a high-frequency trading front end, millions of low-value interactions where horizontal sharding matters more than cross-contract atomicity — TON's actor model is the right call. It's the same reason chains like Hyperliquid split execution into specialized engines instead of forcing everything through one synchronous VM, a tradeoff we broke down in our comparison of HyperCore and HyperEVM. Just budget real time for bounce-path testing and message-ordering review, because that's where TON contracts actually fail in production, not in the parts that look like a Solidity port.

Either way, the bugs that get missed are rarely in the happy path. They're in the failure and reordering cases nobody wrote a test for, and that's exactly what we design against from day one rather than bolting on after a mainnet incident.

If you're shipping on TON and want someone who actually traces message graphs instead of just reading the happy path, talk to us about a smart contract audit.

Need a bot like this built?

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

Start a project
#TON#EVM#Smart Contract Security#FunC#Blockchain Architecture