How to stop worrying and start coding on TON: Compared to EVM networks (ethereum, etc.)

I have a solid background in development across EVM-compatible networks (Ethereum, Polygon, etc.). I’ve built structural products: liquidity pools, lending protocols, limit order systems, arbitrage bots, and various utilities.
And then TON appeared on my horizon. When I started reading the documentation, I experienced a real cognitive dissonance. My brain simply refused to accept that this is an asynchronous network, where a single transaction can span multiple blocks, where there is no familiar global state, no logs (in the EVM sense), and many other fundamental things are missing.
After writing a few contracts, I decided to help those who already know Solidity but want to quickly understand TON’s architecture. I won’t dive into the syntax of Func or Tact/Tolk - I will focus only on the fundamental differences.
Navigation
- What is TON
- Contract interaction
- Gas and transaction value
- Data storage
- Logs: they don’t exist
- Transaction rollback
- Message race conditions
- Idempotency
- Summary
What is TON
- In TON, any active address is a contract (even a regular wallet). That’s why there are multiple wallet versions - in fact, these are different types/versions of wallet contracts (v4, v5).
- A transaction is the processing of a single incoming message.
- What we used to call a transaction in EVM (multiple interactions between contracts) is called a chain of transactions (trace).
Contract interaction: forget about atomicity
The most important and fundamental difference is the interaction model.

EVM: to send a transaction, we call a contract method. Once the transaction is accepted and included in the network, execution becomes exclusive: “the whole world belongs to me.” The network freezes, the state of all contracts is fixed. I can dynamically “ask” any other contract for its balance or a variable, get the answer within the same transaction, and continue the logic. If something goes wrong at the end, I revert, and the entire call chain rolls back as if nothing ever happened.
TON: there is no way to call a method on another contract and instantly get a response. All interaction happens via messages. By sending a message, you create a new transaction that will be processed later. Maybe in the same block, or maybe in one, three, or even more blocks later.
This is what asynchronicity means. What used to be a single monolithic transaction in EVM becomes a “chain” (trace) of transactions in TON, with other users’ actions possibly interleaving in between. The network guarantees message delivery (if there is enough value to execute it), but we must design logic so that contracts can wait for responses and handle intermediate states.
In TON, you cannot “ask and immediately get an answer.” You can only “send a letter and eventually receive a reply.”
Gas and transaction value

Fees here are split into three parts:
- Storage fee - payment for space in the blockchain. In TON, a contract pays for every byte of data in every block (the cost exists in each block and accrues over time). If the contract’s balance drops to zero due to this fee, the contract may be deleted / become uninitialized, including all its data and variables.
- Computation fee - analogue of gas in EVM for actual code execution.
- Forward fee - the cost of delivering a message from one contract to another.
In EVM, you simply set a gas_limit for the entire transaction.
In TON, you must attach enough coins (value) in the first message to cover the entire chain (forwarding + computation + storage on each contract). If you send 1 TON and a chain of 5 contracts consumes it at step 3, the transaction will just stop midway, and a manual rollback may fail (there won’t be enough gas for the rollback itself).
EVM: msg.value is used only to transfer native currency (ETH). Gas is calculated separately. The payer is always the transaction signer (EOA - external wallet).
TON: you only use value, attaching some amount to the transaction - this is the “fuel” for all subsequent contracts in the chain. If you don’t attach enough value, the next message simply won’t reach the recipient or won’t be processed. Each subsequent message in the chain is paid for by the contract that sends it.
Typically, this works like a relay: the user sends 1 TON in the first call, the contract takes its fee (for execution and possibly additional TON), and forwards the rest (for example, 0.9 TON) to the next contract. That contract does its work, deducts its fees, and forwards the remainder further. The final “excess” gas is usually returned to the user in the last message.
If, in the middle of the chain, a contract runs out of coins to pay for gas:
- The current message will not be processed.
- A bounced message (rollback) will not be sent, because it also requires gas.
- The chain “hangs” in an undefined state. Funds may remain locked in intermediate contracts.
Because of this “undefined state” issue, a gas griefing attack is possible: an attacker sends a carefully calculated minimal amount of gas so that part of the operation executes (e.g., state changes or a message is sent), but the process stops due to lack of gas before completing all intended logic. Therefore, you must validate value on input and reject transactions with insufficient gas immediately.
That’s why transactions in TON are usually sent with a gas margin.
Data storage

EVM: we are used to mapping(address => uint256) and the idea that data can live forever as long as the contract exists.
TON: everything is different. Data is not forever - it exists only as long as you keep paying for it. This is a feature of the architecture, and it removes the need to worry about scaling, since it is built into the network itself.
Due to storage fees, keeping huge mappings in a single contract is a luxury (and technically difficult). Therefore, TON uses a master contract + child contracts architecture. Instead of one smart contract holding, for example, all balances in a mapping, we have:
- One master contract (with metadata).
- Thousands of small “wallet” contracts for each user.
A user’s balance is stored in their personal mini-contract for that token. This is cheap, scalable, and shifts storage costs from the protocol to the user.
At the same time, this approach imposes specific requirements on contract logic: you must handle both positive and negative responses from user “wallets/states,” implementing callbacks and message passing between contracts, keeping in mind that everything is asynchronous.
Logs: they don’t exist (in the usual form)

In Ethereum (and other EVM networks), events are the backbone of frontend and analytics. A contract emits an event, and any indexer can read it at any time.
In TON, there is no event log as a separate data structure inside the block that can be cheaply read externally “by subscription.” Technically, transactions include external outbound messages, which can be interpreted as events, but working with them is quite different.
If logs in EVM are a byproduct of a transaction, then in TON, to “tell the world” something, a contract sends an External Outbound Message. Indexers (like Tonapi, Toncenter, or custom ones) parse these messages.
Be prepared: there is no eth_getLogs here, and early debugging via “external messages” may feel painful. TON has no built-in event index like EVM - everything relies on indexers, and the frontend depends on external services.
Transaction rollback (bounced messages)

This is probably the hardest part for an EVM developer. In Solidity, if require() fails - everything collapses, and the transaction is as if it never existed.
In asynchronous TON, if your message chain reaches the third contract and fails there, the first two contracts do not automatically know about it. Their state may have already changed. Therefore, you must design everything so that critical state changes happen only when the entire chain has successfully completed.
TON uses bounced messages to handle errors:
If a message was sent with the bounce: true flag and an error occurs on the receiver side (or the contract is not found), the network automatically generates a response message back to the sender, including all unused TON (minus execution gas).
This “return” message contains part of the original request data. You must implement an on_bounce handler in your contract to restore balances to their original state (e.g., return tokens to the user if a transfer failed).
Important: a bounced message carries only limited gas and data. This is not a full rollback - it is manual work to restore consistency. If you forget to handle bounce, funds may simply get “stuck” in an intermediate contract.
In practice, instead of relying on classic bounce (which is mostly used for returning forward fees), critical logic should be designed as a carefully structured transaction flow.
Let’s look at a simplified example: we have a master contract that should sell no more than 50 items. A user can buy or sell items while supply < 50, after which trading stops.
Purchase (initiated on the master contract)
- Check that items are still available (we have not exceeded 50).
- Check that enough TON was sent with the transaction (price + gas for further execution).
- Calculate and reserve the purchase amount on the master contract.
- Increase supply.
- Send (or deploy) the user’s balance contract. Since the network guarantees message delivery, we know the balance will be set correctly, and we don’t need to track this step further.
Now everything seems fine - but the user wants to sell.
Sale (initiated on the user balance contract)
- Verify that the user actually has at least 1 item.
- Check that there is enough gas for further execution.
- Decrease the number of items on the balance contract.
- Send a message to the master contract about the sale.
- On the master contract, verify that the call came from a valid balance contract.
- If
supply < 50: decrease supply and send TON to the user from the master contract. - If
supply = 50: restore the balance contract state (undo step 3) by sending a message back to increase the balance.
- If
This kind of sequence and careful handling of all possible outcomes is required. Sometimes, you must initiate interaction not from the master contract to correctly emulate a “revert,” which does not exist natively.
Message race conditions

EVM: execution is exclusive during a transaction - no one can interfere.
TON: between the first and second transactions in your chain, many other messages from other users can be processed, potentially changing your contract’s state.
TON processes messages sequentially, but you cannot control or guarantee their order. A race condition in TON is not “two threads modifying a variable simultaneously,” but rather “you don’t know the state of the contract when your message is processed.”
Example: a vault with deposit/withdraw and a Jetton balance.
Transaction trace (simplified)
- tx1:
wallet → vault: withdraw → check balance → send message to jettonWallet_Vault to transfer 100 tokens. - tx2:
vault → jettonWallet_Vault: reduce vault balance by 100 + transfer. - tx3:
jettonWallet_Vault → jettonWallet_User: increase user balance + send confirmation. - tx4:
jettonWallet_User → vault: confirmation received → decrease user’s internal balance.
Now imagine the user sends two withdraws almost simultaneously:
- withdraw1 → tx1 → balance = 100 → continue
- withdraw1 → tx2 → tokens sent
- withdraw2 → tx1 → balance = 100 → continue (race condition!)
- withdraw2 → tx2 → tokens sent
- withdraw1 → tx3 → user gets 100 tokens
- withdraw1 → tx4 → balance decreases by 100
- withdraw2 → tx3 → user gets 100 tokens
- withdraw2 → tx4 → balance goes negative (or fails)
Result: the user receives extra 100 tokens.
The mistake is sending funds first and updating balance later. You must update critical state before sending any messages and always read actual contract state during execution - never trust incoming message data.
Idempotency

- EVM: uses a global nonce per account. Two transactions with the same nonce - only one is accepted.
- TON: no system-wide nonce. Protection from duplicates is the developer’s responsibility. Wallet contracts (v3/v4) use
seqno, but this is contract logic, not a network rule. - EVM: you can precompute a transaction hash before sending it.
- TON: the hash of an external message and the final transaction hash are different. The transaction hash is generated only after execution.
To track status and ensure idempotency on the API side, use a combination like address + subwallet_id + seqno or a custom unique payload.
Consistency implementation
Transactions are often initiated by external messages (from wallets). To prevent double execution:
- Seqno: the contract stores a counter and accepts only the expected sequence number.
- valid_until (expire): each message has a time limit. If not included in a block before that, it becomes invalid.
Within a logical operation, multiple internal messages (a trace) may occur. A best practice is to generate and propagate a unique query_id between contracts. By storing it temporarily, you can detect duplicates and avoid re-executing logic.
Summary

Moving to TON is not about learning new syntax - it’s about switching paradigms: from a “synchronous computer” to a “postal system between millions of small offices.”
If you learn to think in terms of messages and asynchronous responses, you unlock a network with near-infinite scalability - one that cannot be “brought down” by a single popular NFT mint.
Development rules in TON:
- never trust incoming data
- update state before sending messages
- always handle bounce
- validate value
- make operations idempotent
