Security best practices
此内容尚不支持你的语言。
There are several anti-patterns and potential attack vectors that Tact smart contract developers should be aware of. These can affect the security, efficiency, and correctness of the contracts. Below we discuss the do’s and don’ts specific to writing and maintaining secure Tact smart contracts.
For a deeper understanding, refer to the following resources:
- Smart contracts guidelines in TON Docs
- Secure Smart Contract Programming in TON Docs
- FunC Security Best Practices in GitHub repo
Send sensitive data on-chain
The entire smart contract computation is transparent, and if you had some confidential values at run-time, they could be retrieved with a simple emulation.
Do’s ✅
Do not send or store sensitive data on-chain.
Don’ts ❌
Misuse of signed integers
Unsigned integers are safer because they prevent most errors by design, while signed integers can have unpredictable consequences if not used carefully. Therefore, signed integers should be used only when absolutely necessary.
Do’s ✅
Prefer to use unsigned integers unless signed integers are required.
Don’ts ❌
The following is an example of the incorrect use of a signed integer. In the Vote
Message, the type of the votes
field is Int as int32
, which is a -bit singed integer. This can lead to spoofing if the attacker sends the negative number of votes instead of a positive one.
Invalid throw values
Exit codes and indicate normal execution of the compute phase of the transaction. Execution can be unexpectedly aborted by calling a throw()
or similar functions directly with exit codes and . This can make debugging very difficult since such aborted execution would be indistinguishable from a normal one.
Do’s ✅
Prefer to use the require()
function to state expectations.
Don’ts ❌
Don’t throw or directly.
Insecure random numbers
Generating truly secure random numbers in TON is challenging. The random()
function is pseudo-random and depends on logical time. A hacker can predict the randomized number by brute-forcing the logical time in the current block.
Do’s ✅
-
For critical applications avoid relying solely on on-chain solutions.
-
Use
random()
with randomized logical time to enhance security by making predictions harder for attackers without access to a validator node. Note, however, that it is still not entirely foolproof. -
Consider using the commit-and-disclose scheme:
- Participants generate random numbers off-chain and send their hashes to the contract.
- Once all hashes are received, participants disclose their original numbers.
- Combine the disclosed numbers (e.g., summing them) to produce a secure random value.
For more details, refer to the Secure Random number generation page in TON Docs.
Don’ts ❌
Don’t rely on the random()
function.
Don’t use randomization in external_message
receivers, as it remains vulnerable even with randomizing logical time.
Optimized message handling
String parsing from human-friendly formats into machine-readable binary structures should be done off-chain. This approach ensures that only optimized and compact messages are sent to the blockchain, minimizing computational and storage costs while avoiding unnecessary gas overhead.
Do’s ✅
Perform string parsing from human-readable formats into machine-readable binary structures off-chain to keep the contract efficient.
Don’ts ❌
Avoid parsing strings from human-readable formats into binary structures on-chain, as this increases computational overhead and gas costs.
Gas limitation
Be careful with the Out of gas error
. It cannot be handled, so try to pre-calculate the gas consumption for each receiver using tests whenever possible. This will help to avoid wasting extra gas because the transaction will fail anyway.
Do’s ✅
Identity validation
Always validate the identity of the sender if your contract logic revolves around trusted senders. This can be done using the Ownable
trait or using state init validation. You can read more about Jetton validation and NFT validation.
Do’s ✅
Use the Ownable
trait.
Don’ts ❌
Don’t execute a message without validating the sender’s identity!
Replay protection
Replay protection is a security mechanism that prevents an attacker from reusing a previous message. More information about replay protection can be found on the External messages page in TON Docs.
Do’s ✅
To differentiate messages, always include and validate a unique identifier, such as seqno
. Update the identifier after successful processing to avoid duplicates.
Alternatively, you can implement a replay protection similar to the one in the highload v3 wallet, which is not based on seqno
.
Don’ts ❌
Do not rely on signature verification without the inclusion of a sequence number. Messages without replay protection can be resent by attackers because there is nothing to distinguish a valid original message from a replayed one.
Race condition of messages
A message cascade can be processed over many blocks. Assume that while one message flow is running, an attacker can initiate a second message flow in parallel. That is, if a property was checked at the beginning, such as whether the user has enough tokens, do not assume that it will still be satisfied at the third stage in the same contract.
Handle/Send bounced messages
Send messages with the bounce flag set to true
, which is a default for the send()
function. Messages bounce when the execution of a contract has failed. You may want to deal with this by rolling back the state of the contract by wrapping code in try...catch
statements and some additional processing depending on your logic.
Do’s ✅
Handle bounced messages via a bounced message receiver to correctly react to failed messages.
Transaction and phases
From Sending messages page of the Book:
Each transaction on TON Blockchain consists of multiple phases. Outbound messages are evaluated in compute phase, but are not sent in that phase. Instead, they’re queued in order of appearance for the action phase, where all actions listed in compute phase, like outbound messages or reserve requests, are executed.
Hence, if the compute phase fails, registers c4
(persistent data) and c5
(actions) won’t be updated. However, it is possible to manually save their state using commit()
function.
Return gas excesses carefully
If excess gas is not returned to the sender, the funds will accumulate in your contracts over time. Nothing terrible in principle, just a suboptimal practice. You can add a function to rake out excess, but popular contracts like TON Jetton still return to the sender with the Message with 0xd53276db
opcode.
Do’s ✅
Return excesses using a Message with 0xd53276db
opcode.
Also, you can leverage notify()
or forward()
standard functions.
Pulling data from other contract
Contracts in the blockchain can reside in separate shards, processed by other set of validators, meaning that one contract cannot pull data from other contracts. That is, no contract can call a getter function) from other contracts.
Thus, any on-chain communication is asynchronous and done by sending and receiving messages.
Do’s ✅
Exchange messages to pull data from other contract.