TON's Actor Model vs EVM's Account Model: Why Bugs Differ
Delivered a 1,299-word publication-ready markdown article on TON's actor model vs EVM's account model, including a comparison table, worked Solidity/FunC code example, 3 service links, 2 blog links, and a clear verdict.
{
"excerpt": "TON's async actor model vs EVM's account model: why message-chain bugs replace reentrancy, and which architecture fits your protocol.",
"tags": ["TON", "EVM", "Smart Contract Security", "Blockchain Architecture", "Audits"],
"cover": "nodes",
"content": "A reentrancy guard that saves you on Ethereum does nothing useful on TON, because TON doesn't really have reentrancy in the Solidity sense. It has something that trips up experienced auditors even harder: every external interaction is a message sent into an outbox, delivered on its own schedule, to a contract that might not exist yet. That single design choice is why the bug classes we find during a code review and audit on a TON contract look almost nothing like what we find on an EVM chain, even when the business logic is identical.\n\n## The account model you already know\n\nEVM contracts run inside one atomic transaction. Alice calls the router, the router calls the pool, the pool calls the token, the token updates a mapping, and if any single step reverts, the whole chain unwinds back to Alice's original call as if nothing happened. State reads are synchronous: balanceOf returns the truth right now, in the same execution context as everything else in that transaction. This is why reentrancy is a solved-but-recurring problem — an external call in the middle of your function can jump back into your contract before your state update lands, and the whole class of bugs (the 2016 DAO hack, dozens of vault exploits since) comes from that one property. Checks-effects-interactions, reentrancy guards, and pull-over-push payment patterns all exist to fight synchronous composability.\n\nThe tradeoff is that EVM contracts are easy to reason about in isolation. Read a Solidity function top to bottom and you know the exact state transition it produces, because nothing else can touch storage mid-execution except through a call you explicitly made.\n\n## The actor model TON actually runs on\n\nTON contracts are actors. Each smart contract is its own account with its own persistent state, and the only way to talk to another contract is by sending it a message that gets queued, routed across shards, and processed at some later point, possibly the next block, possibly several blocks later if the destination shard is congested. There is no synchronous cross-contract call. When your FunC contract does send_raw_message, you are not calling a function, you are mailing a letter and moving on. Your function returns, your gas is spent, and the recipient processes that message independently, with its own execution context and its own chance to fail, bounce, or simply take longer than you assumed.\n\nThis is why the canonical TON bug is not reentrancy, it's inconsistent intermediate state across an unfinished conversation. A jetton transfer isn't one call, it's a chain of messages: transfer → internal_transfer → transfer_notification, each hopping to a different contract, each capable of independently succeeding or bouncing. If your contract mutates state assuming the next message will definitely arrive, and it doesn't (out of gas, a malformed payload, a contract that rejects unknown messages), you're left with a wallet that thinks it sent tokens it still holds, or a vault that credited a deposit that never actually settled.\n\n### A concrete example\n\nCompare a same-chain swap credit in Solidity versus the equivalent as a TON message chain.\n\nsolidity\n// EVM: atomic, either both happen or neither does\nfunction deposit(uint amount) external {\n token.transferFrom(msg.sender, address(this), amount);\n balances[msg.sender] += amount; // safe: same tx, reverts together\n}\n\n\nfunc\n;; TON: the deposit and the credit are two DIFFERENT contracts,\n;; two different messages, no shared atomicity\n() recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {\n ;; this handler runs when the jetton wallet notifies us of a transfer\n ;; if we crash or bounce AFTER the jettons already moved, the sender\n ;; has no on-chain guarantee the credit will ever be recorded\n balances::set(sender, balances::get(sender) + amount);\n}\n\n\nThe Solidity version is correct because transferFrom and the balance update share one transaction. The FunC version is only correct if you've also handled three things: what happens if recv_internal throws after the jettons already arrived (the sender expects a bounce crediting them back, and if you didn't reserve enough TON for that bounce, it silently fails); what happens if two notifications arrive out of order after routing through different shards; and whether an attacker can spam malformed transfer_notification payloads to a contract that trusts the message body without verifying the sender is the actual jetton wallet it expects — a mistake we see constantly, and one of the highest-severity findings on TON audits.\n\n## The bugs, side by side\n\n| Dimension | EVM (account model) | TON (actor model) |\n|---|---|---|\n| Cross-contract calls | Synchronous, same transaction | Async messages, arrive later or not at all |\n| Atomicity | Whole tx reverts together | Each contract's step is independent |\n| Classic bug | Reentrancy (call back before state update) | Broken message chains / lost bounces |\n| State consistency | Read-your-writes within a tx | Must design for partial completion |\n| Gas failure mode | Tx reverts, state unchanged | Message can bounce or get stuck; TON must be reserved for bounces |\n| Ordering guarantees | Deterministic tx order in a block | No guaranteed ordering across shards |\n| Auditor's main question | "Can an external call re-enter here?" | "What if this message never arrives, or arrives twice?" |\n| Tooling maturity | Slither, Foundry invariant tests, Echidna | Sandbox/emulator tests, far fewer static analyzers |\n\n## Where this actually bites in production\n\nSharding makes it worse before it makes it better. TON splits state across workchains and shardchains so it can scale, but a message from contract A to contract B might cross a shard boundary, and cross-shard delivery is not instant. A protocol that assumes "the follow-up message lands within N seconds" is making an assumption TON itself doesn't guarantee under load. We've watched teams build trading dashboards that poll on-chain state on EVM-style finality timing, then get confused when a TON position update lags the UI by several blocks because the settlement message queued behind shard congestion — the same async-vs-sync mismatch that shows up comparing Jito's bundle model against Yellowstone gRPC streaming on Solana, where ordering guarantees, not raw throughput, decide whether a strategy is safe to run live.\n\nThe upside is real: TON contracts don't need reentrancy guards at all, because there's no call stack to re-enter. Gas exhaustion attacks that plague EVM external calls (unbounded loops, call.gas() griefing) mostly don't translate either, since every message carries its own gas budget and a failed message bounces rather than dragging down the caller. What you trade reentrancy for is a much larger surface of "did this multi-step process actually finish" — a state-machine problem, not a single-function correctness problem.\n\n## Which to pick, and when to worry\n\nIf you're building anything where a single logical action must be all-or-nothing — a liquidation, a vault withdrawal paired with a collateral check, an oracle-gated settlement like the resolution mechanics behind Polymarket's CLOB versus UMA — EVM's synchronous atomicity is genuinely easier to get right, and that's fewer state machines to hand-verify, not nostalgia. Choose EVM, or an EVM L2, when your protocol logic is deeply interdependent across contracts and you want the runtime to enforce atomicity for you.\n\nChoose TON when your bottleneck is throughput and parallelism: payments, high-volume notifications, anything naturally shardable into independent user-level state, and you're willing to design every multi-message flow as a resumable state machine with bounce handling and idempotent handlers. Don't pick TON for speed and assume your EVM mental model carries over; it doesn't, and its failure modes are quieter than a revert, which is what makes them expensive. Either way, get the message-chain design reviewed before mainnet, ideally in a strategy consultation alongside the audit — on TON the bug usually isn't in the function you're staring at, it's in the message that never showed up."
}
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