All articles
Guides·April 29, 2026·5 min read

TON Contract Security Pitfalls: Bounces, Replay, Gas Griefing

TON smart contract security pitfalls that EVM-trained reviewers miss: unhandled bounces, message replay, and gas griefing explained.

{"excerpt":"TON smart contract security pitfalls that EVM-trained reviewers miss: unhandled bounces, message replay, and gas griefing explained.","tags":["TON","smart contract security","FunC","Tact","gas griefing"],"cover":"nodes","content":"TON does not have a mempool, does not have synchronous calls, and does not have a single global state you can snapshot mid-transaction. Every contract-to-contract interaction is an asynchronous message, and that one architectural fact creates three failure classes that never show up in a Solidity audit checklist. A reviewer who cut their teeth on reentrancy and integer overflow will walk right past them.\n\n## Why the actor model changes the threat surface\n\nOn Ethereum, a call either succeeds and its state changes commit, or it reverts and nothing happens. On TON, a contract fires an outbound message and moves on. The receiving contract might process it three seconds later, in a different shard, possibly never. Your contract has already updated its own state by the time it finds out whether the downstream step worked. That gap between "I sent it" and "it arrived" is where most TON-specific bugs live.\n\n### Pitfall 1: unhandled bounced messages\n\nWhen contract A sends a message to contract B and B's recv_internal throws (out of gas, bad opcode, failed require), TON does not just drop the message. If the original message had the bounce flag set — which it does by default unless you explicitly clear it — the TVM returns a bounced message to A with the original body truncated to 224 bits and prefixed with 0xffffffff.\n\nMost FunC contracts I review never check for that prefix. They treat every inbound internal message as a fresh instruction. So when a jetton transfer bounces back after a failed mint, the vault or router reads the bounced payload as if it were a normal deposit notification, credits a balance that was never actually delivered, or double-decrements a counter it already decremented optimistically before sending.\n\nThe fix is boring but mandatory:\n\nfunc\n() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {\n slice cs = in_msg_full.begin_parse();\n int flags = cs~load_uint(4);\n if (flags & 1) { ;; bounced flag\n ;; this is a bounce, NOT a new instruction\n ;; roll back whatever you optimistically committed\n handle_bounce(cs, in_msg_body);\n return ();\n }\n ;; normal message handling below\n}\n\n\nIf you skip this, any contract that optimistically debits state before confirming delivery — a common pattern for withdrawal queues and cross-contract swaps — leaks value on every downstream revert. I have seen this exact bug in three separate jetton-adjacent audits in the last year, and it is the single most common finding we flag in TON-specific reviews; we cover the mechanics of what auditors actually check for on TON in more depth in our FunC and Tact audit checklist.\n\n### Pitfall 2: message replay across layers\n\nEVM developers assume nonces are handled for them — every transaction has one, enforced at the protocol level. TON internal messages have no such guarantee at the contract level. If your contract accepts a signed off-chain payload (a common pattern for gasless relays, meta-transactions, or multisig approvals forwarded through an intermediate wallet), nothing stops that exact message body from being replayed, either by resending the same external message or by a malicious relayer resubmitting a cached internal message after a bounce-and-retry cycle.\n\nThe standard TON wallet contracts solve this with a seqno stored in contract state, incremented on every accepted message and checked before signature verification. Custom contracts that implement their own signed-instruction pattern — vault approvals, oracle price pushes, admin actions gated by an off-chain signer — need the same discipline: a monotonic counter or a valid_until timestamp checked against now(), stored on-chain, and bumped atomically before any external effect fires. Skip the counter and an attacker who captures one valid signed withdrawal message can replay it every time the contract's balance refills.\n\nThis gets worse with jetton wallets specifically, because each holder has their own deployed wallet contract and the notification messages between jetton wallet and jetton master pass through multiple hops. A replay at any hop can double-credit a transfer notification. We go through the standard-specific version of this in our breakdown of jetton and NFT standard pitfalls.\n\n### Pitfall 3: gas griefing through forwarded value\n\nTON gas is paid in TON coin attached to the message itself, not deducted from a separate account balance. Every message you forward needs enough value attached to cover the receiver's execution, plus its own forwarding fee, plus whatever it forwards onward. Get this wrong in either direction and you have a griefing vector.\n\nUnder-fund a forwarded message and the downstream contract runs out of gas mid-execution, which triggers pitfall 1 — a bounce your contract probably does not handle — except now an attacker can trigger it deliberately by calling your contract with the exact minimum value needed to pass your own checks but not enough to cover the next hop. Over-fund it and you have created a griefing opportunity in the other direction: contracts that forward msg_value - my_fee without a floor can be drained by an attacker who sends thousands of near-zero-value messages, each one costing your contract real gas to process and reject.\n\nThe practical rule: never forward msg_value verbatim. Compute a fixed reserve for your own storage rent and processing cost, subtract it explicitly, and set a hard minimum below which you refuse the message outright rather than forwarding an underfunded one:\n\nfunc\nint fwd_fee = get_forward_fee(...);\nthrow_unless(709, msg_value > min_tons_for_storage + fwd_fee);\nint to_forward = msg_value - min_tons_for_storage - fwd_fee;\n\n\n## What this means for audit scope\n\nA reviewer running an EVM-shaped checklist against a TON codebase will check for reentrancy that structurally cannot happen the same way, miss the bounce-handling gap entirely, and treat gas accounting as a footnote instead of a primary attack surface. If you are budgeting for a review, it is worth understanding how TON-specific scope changes the estimate versus an EVM job — we lay out real numbers in our audit cost breakdown across EVM, Solana, and TON.\n\nIf you are still at the design stage, get the message-flow diagram in front of someone who has shipped TON contracts before you write the state machine — a short strategy consultation before implementation catches these patterns cheaper than a post-launch audit does. And if the contracts are already written, treat bounce handling, replay protection, and gas accounting as mandatory checklist items, not optional hardening, before you accept production traffic — our smart contract audit service covers all three as standard scope on every TON engagement."}

Need a bot like this built?

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

Start a project
#TON#smart contract security#FunC#Tact#gas griefing