All articles
Smart Contracts·July 2, 2026·6 min read

Common Jetton Contract Bugs on TON (and How Audits Catch Them)

Article on common jetton contract bugs on TON for TierZero blog, returned as structured JSON.

{"excerpt":"The recurring jetton contract bugs TON audits keep flagging: fake notifications, minter overreach, bounce mishandling, and gas math errors.","tags":["TON","Jetton","Smart Contract Audit","FunC","Tact"],"cover":"nodes","content":"## Where jetton bugs actually live\n\nMost jetton contracts on TON are copy-pasted from the standard implementation, then modified just enough to break something. In three years of reviewing FunC and Tact jetton code, the same five failure modes show up in maybe 80% of findings. None of them are exotic. They're the kind of thing a careful engineer catches on a second read, which is exactly why a second reader matters.\n\nTON's actor model is the root cause behind most of these. Every jetton transfer is really a chain of asynchronous messages between the sender's jetton-wallet contract, the receiver's jetton-wallet contract, and sometimes a third contract that reacts to the transfer. Solidity developers moving to TON keep reasoning as if a transfer were one atomic call. It isn't, and the bugs below are what happens when that assumption leaks into the code.\n\n### 1. Trusting transfer_notification without checking the sender\n\nThe jetton standard (TEP-74) sends a transfer_notification message to the receiving contract after a transfer completes, so DeFi contracts, vaults, and marketplaces can react. The message carries a sender field telling you who sent the tokens. Auditors see this pattern constantly:\n\nfunc\n() recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {\n int op = in_msg_body~load_uint(32);\n if (op == op::transfer_notification()) {\n int jetton_amount = in_msg_body~load_coins();\n slice from_address = in_msg_body~load_msg_addr();\n ;; credits from_address's balance here, no further checks\n credit_balance(from_address, jetton_amount);\n return ();\n }\n}\n\n\nThe bug: nothing verifies that the message actually came from the jetton-wallet contract your protocol expects. Anyone can deploy a fake jetton-wallet, or even a plain contract, and send a transfer_notification with a fabricated sender and jetton_amount. The contract just credited itself free balance. The fix is to recompute the expected jetton-wallet address from your known minter and the sender's address using calculate_user_jetton_wallet_address, then compare it against msg::sender() before trusting anything in the payload. This single check is missing often enough that we treat it as a default finding to look for, not an edge case — it's the TON equivalent of forgetting msg.sender checks in a Solidity withdraw function.\n\n### 2. Minter authority that's too easy to abuse or too hard to rotate\n\nThe jetton-minter holds the admin address that can mint new supply and change metadata. Two opposite failure patterns keep showing up. First: the admin check is implemented with a raw slice comparison against a hardcoded address baked into data.func, with no upgrade or timelock path, so a compromised deploy key means permanent, silent inflation risk with no way to rotate. Second, and more common on projects trying to look decentralized: the mint function has an admin_only guard that's copy-pasted correctly, but a separate change_admin or change_content handler is missing that same guard because it was added later, after the initial contract was reviewed once and never looked at again. We've found this exact gap in production, live contracts holding real liquidity. Any admin-controlled entry point needs the identical authorization check, tested individually, not inherited by assumption from a sibling function.\n\n### 3. Bounce logic that leaves TON — or tokens — stuck\n\nTON messages carry a bounce flag. If a message with bounce=true fails to process on the receiving contract, the TON (and the message body) bounces back to the sender automatically. Jetton-wallet contracts rely on this for refunding failed transfers. The mistake is setting bounce=false on outbound messages to reduce gas or "simplify" the flow, which means a failed transfer — say, the receiver's jetton-wallet contract doesn't exist yet and can't be deployed inline — silently burns the TON attached to cover gas, with no notification and no recovery path. The inverse mistake is just as common: not handling the bounced case explicitly in recv_internal, so a bounced message parses as a normal message and corrupts internal state, like decrementing a balance twice. TEP-74 compliant wallets check the bounced flag as the very first thing in message handling, before touching op codes. Skipping that check is a two-line omission with real fund-loss consequences.\n\n### 4. Skipping jetton-wallet code verification\n\nA jetton-wallet's address is deterministic — it's derived from the minter address, the owner address, and the wallet code hash via calculate_user_jetton_wallet_address. Contracts that accept jetton payments need to verify the incoming message actually originates from that exact derived address, using the code hash your protocol was audited against, not just checking that the sender matches some cached address in storage. We've seen integrations that cache the jetton-wallet address once at initialization and never revalidate it, which breaks silently if the jetton project ever redeploys or migrates its wallet code (rare, but it happens with upgradable jetton implementations). Recomputing the address per-message costs a bit of gas; skipping it costs the ability to trust any inbound transfer.\n\n### 5. forward_ton_amount and gas underestimation\n\nJetton transfers let the sender specify a forward_ton_amount — TON forwarded to the receiver alongside the notification, meant to cover the receiver's processing gas. Contracts that hardcode a fixed forward amount, or worse, forward zero and expect the receiver to have pre-funded balance, cause transfers to fail intermittently depending on network gas prices and receiver contract complexity. This shows up as "works in testing, fails under load" bugs that are miserable to reproduce because they're gas-price-dependent, not logic-dependent.\n\n## Why these keep slipping through\n\nNone of these five patterns require deep cryptography knowledge to catch. They require someone reading message-handling code with TON's async model actually in mind, tracing every recv_internal branch, and checking authorization on every state-mutating entry point independently. That's slower than it sounds when a jetton contract has a dozen op codes and three or four external integration points. It's also exactly the process outlined in our FunC and Tact audit checklist, which is the same checklist we run against jetton code specifically.\n\nIf you're weighing whether an audit is worth the cost before you ship, the honest answer depends on what's at stake — see our breakdown of audit pricing across EVM, Solana, and TON and the more fundamental question of whether you need one before mainnet at all. For jettons specifically, the pattern is consistent enough that a targeted code review ahead of a full audit often surfaces two or three of the five bugs above in a single pass, cheaply, before they reach production.\n\nIf you're building the jetton and its surrounding contracts from scratch, it's worth getting the transfer-notification and bounce handling right the first time rather than patching them post-launch — our smart contract development team builds jetton and dApp logic with these exact failure modes as a checklist, and the same applies if you're wiring a dApp to accept jetton payments from contracts you don't control.\n\nGet your jetton contract in front of a smart contract audit before mainnet — these five bugs are cheap to fix in review and expensive to explain after a drain."}

Need a bot like this built?

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

Start a project
#TON#Jetton#Smart Contract Audit