Back to all stories
Blogs
Ecosystem
Secure Smart Contract Programming in Tact: Popular Mistakes in the TON Ecosystem
12/12/2024
Secure Smart Contract Programming in Tact: Popular Mistakes in the TON Ecosystem

The Open Network (TON) continues to push the boundaries of blockchain technology with its innovative features and robust smart contract capabilities. Building on the insights and lessons learned from previous blockchain platforms like Ethereum, TON offers developers a more efficient and flexible environment. One of the key components driving this advancement is the Tact language.

Tact is a new programming language for the TON blockchain that is focused on efficiency and simplicity. It is designed to be easy to learn and use, and to fit well with smart contracts. Tact is a statically typed language with a simple syntax and a powerful type system.

Nevertheless, many of the pitfalls developers face with FunC remain relevant when developing in Tact. Below, we will examine some of the common mistakes from our audit practice of Tact.

Data Structures

Optional Addresses

Tact makes it easy to declare, decode, and encode data structures. However, it is still necessary to be cautious. Let’s consider an example:

Script 1

This is the declaration of the InternalTransfer message used for transferring Jettons according to the TEP-74 standard. Note the declaration of response_destination, which is an Address. In Tact, an Address must be a non-zero address. However, the reference implementation of the Jetton standard allows for zero address (addr_none), which is represented by two zero bits. This means that the user or another contract will probably try to send jettons with zero response_destination and it will unexpectedly fail.

Moreover, if the Transfer message (from the user to their wallet) allows response_destination, but the InternalTransfer message (from sender wallet to recipient wallet) doesn’t allow it, the jettons will fly out, but will not get to the destination and will be lost. Later, we will discuss an exception, whereby the bounced message is properly handled.

In this case, the better structure declaration allowing zero address would be Address?, but passing the optional address further to the next message is currently a bit cumbersome in Tact.

Data Serialization

In Tact, the developer can specify how the field is serialized.

Script 2

In this example, the totalAmount will be serialized as coins, and releasedAmount will be serialized as int257 (default for Int). It can be negative and it will occupy 257 bits. In most cases, omitting the serialization type will not bring any problems; however, if the data is involved in communication, it becomes crucial.

Here is an example from the project we audited:

Script 3

This data structure is used by the NFT item as a reply to an on-chain get_static_data request. According to the standard, the reply should be:

Reply

The index above is uint256 (not int257), which means that the returned data will be misinterpreted by the caller with unpredictable results. It is likely that the report_static_data handler will revert and the message flow will break. These examples illustrate why it is important to think about data serialization, even when working in Tact.

Signed Integers

Not specifying the serialization type for Int can lead to much more serious consequences than in the above examples. Unlike coins, int257 can be negative, which often surprises programmers. For instance, among live contracts on Tact, it's incredibly common to see amount: Int.

Javascript 4

By itself, this doesn’t necessarily indicate a vulnerability, because this amount is usually encoded in a JettonTransfer message or in send(SendParameters{ value: amount, which uses coins and doesn’t allow negative amounts. However, in one case, we encountered a contract with a significant balance that allowed the user to set everything negative: reward, fee, amount, price, etc. Consequently, a malicious actor could have exploited the vulnerability.

Concurrency

In Ethereum, developers must be conscious of reentrancy, which is the ability to call the function of the same contract again before the current function execution has been completed. In TON, reentrancy is not possible.

Since TON is a system with asynchronous and parallel smart contract calls, it can be even more difficult to follow the order of actions processed. Any internal message will definitely be received by the destination account, and transaction consequences are processed after the transaction itself, but there are no other guarantees (see more about messages delivery).

ABCD We can’t predict whether Message 3 or Message 4 will be delivered first.

In this case, a Man-in-the-Middle attack of the message flow is highly possible. To be safe, developers should assume that each message is delivered in 1 to 100 seconds, and during that period, any other messages can be delivered. Here are some other aspects to keep in mind to boost security:

  1. Don’t check or update something in your contract state to be used by the next steps of the message flow.
  2. Use carry-value pattern. Don’t send the information about the value. Instead, send the value with the message.

Here is the real example (buggy):

Buggy

In the above example, the following steps occur:

  1. The user sends jettons to NftCollection (via collection_jetton_wallet).
  2. TransferNotification is sent to the NftCollection contract. It remembers received_jetton_amount.
  3. It forwards the jettons to the NftCollection owner.
  4. Excesses message is sent to NftCollection as response_destination.
  5. NftItem is deployed in the Excesses handler, using received_jetton_amount.

There are several problems here:

  1. First, the Excesses message is not guaranteed to be delivered by the Jetton standard. If there is no gas to send as Excesses, it will be skipped and the message flow will stop.
  2. Updating the received_jetton_amount and using it later will make it vulnerable to concurrent execution. Another user can send another amount at the same time and overwrite the saved amount, which can also be exploited for a profit.

In case of concurrency, TON is similar to traditional centralized multi-threaded systems.

Handling Bounced Messages

Many contracts neglect the handling of bounced messages. However, Tact makes this process straightforward:

Script 4

To decide whether the message should be sent in bounceable mode, consider two factors:

  1. Who should get attached toncoins in case of failure? If the destination should get the money rather than the sending contract, send it in non-bounceable mode.
  2. What happens with the message flow if the next message is rejected? If the consistent state can be restored by handling the bounced message, it is better to do so. If not, it is better to change the flow.

Here is an example in Jetton standard:

Jetton Standard

  1. Excesses message is sent in a non-bounceable mode because the contract doesn’t need the toncoins to be returned.
  2. TransferNotification message is sent in non-bounceable mode because forward_ton_amount belongs to the caller and the contract doesn’t want to keep it.
  3. On the contrary, BurnNotification is sent in bounceable mode, because if it is bounced by the Jetton master contract, the wallet has to restore its balance to keep total_supply consistent.
  4. InternalTransfer is also bounceable. The sender’s wallet has to update the balance if the receiver has rejected the money.

Keep in mind the following:

  1. The bounced message receives only 256 bits of the original message; after message recognition, it gives only 224 bits of useful data. Consequently, you will have limited information about the operation that failed. Usually, that is some amount stored as coins.
  2. The bounced message will not be delivered if out of gas.
  3. The bounced message can’t be bounced itself.

Returning Jettons

In some cases, reverting and handling the bounced message is not an option. The most common example is when your contract gets the TransferNotification about arrived jettons. Bouncing it will likely make the jettons blocked forever. Instead, you should use the try-catch block.

Let’s take a look at an example. In the EVM, when a transaction is reverted, all consequences are rolled back (except the gas—it is taken by the miner). In the TVM, the “transaction” is divided into a cascade of messages, so reverting just one will likely make the “group of contracts” inconsistent.

To handle that, you must manually check all conditions and, in case of emergency, send fixing messages back and forth. But since parsing payloads in an exception-free manner is cumbersome, it is preferable to use a try-catch block.

Below is a typical Jetton receiving code:

Script 5

Notice that it will not function if the gas is insufficient, even for sending jettons back. Also notice that we send jettons back via the sender() “wallet”, not via our contract real jetton wallet, because anyone can send a TransferNotification message manually to fool us.

Managing Gas

One of the most common problem classes we detect during auditing of TON contracts is gas management problems. There are two primary reasons for that:

  1. Lack of gas control can lead to the following problems:
  • Incomplete message flow execution. Part of the action will take effect and another part will be reverted due to out-of-gas. For example, if rewards acquisition was done in the Jetton wallet, but burning shares was ignored in Jetton master, then the whole group of contracts will become inconsistent.
  • Users can extract their own contract balances. Additionally, excessive toncoins can accumulate on the contract.
  1. It is difficult for the TON contract developer to manage and control the gas. In Tact, you need to acquire gas consumption by testing, and update the values each time the message flow is updated during development.

We recommend the following:

  1. Determine the “entry points”. These are message handlers in all contracts that can accept messages from the “outside”, meaning from end users or from other contracts (like Jetton wallets).
  2. For each entry point, draw all possible paths and calculate gas consumption. Use printTransactionFees() (available in @ton/sandbox which comes with Blueprint).
  3. If a contract can be deployed during the message flow, assume it will be deployed. Deployment takes more gas and storage fees.
  4. In each entry point, add a minimal gas requirement accordingly.

Code 1

  1. If the handler doesn’t send more messages (message flow terminates here), then it’s better to return Excesses like the following:

Code 2

It is fine to not send Excesses, but for contracts with big throughput like Jetton Master with a lot of BurnNotifications, or Jetton Wallet with a lot of incoming transfers, the amount accumulated can quickly expand.

  1. If the handler sends only one message—including emit(), which is actually an external message—the easiest way involves passing the rest of the gas using forward() (see above).
  2. If the handler sends several messages, or if the ton amounts are involved in the communication, it’s easier to calculate how much to send, rather than how much to leave.

In the next example, let’s assume that the contract wants to send forwardAmount to two child contracts as deposit:

Code 3

As you can see, gas management requires significant attention, even in simple cases. Remember that you can’t use the SendRemainingValue flag in send() mode if you have already sent messages, unless you deliberately want to spend money from the contract balance itself.

Conclusion

Secure smart contract development in Tact will become increasingly more important as the TON ecosystem evolves. While Tact offers enhanced efficiency and simplicity, developers must remain vigilant to avoid common pitfalls. By understanding the common mistakes and implementing best practices, developers can harness the full potential of Tact to create robust and secure smart contracts. Continuous learning and adherence to security guidelines will ensure that the innovative capabilities of the TON blockchain are leveraged safely and effectively, contributing to a more secure and trustworthy blockchain environment.