TON smart contract security best practices
This comprehensive guide covers the most critical security vulnerabilities found in TON smart contracts, based on real-world audits and security research. Understanding these pitfalls is essential for developing secure smart contracts on TON Blockchain.
Many of these vulnerabilities can lead to complete loss of funds. Always conduct thorough security audits before deploying contracts to mainnet.
Critical
Missing impure modifier
Severity: 🔴 Critical
The absence of the impure modifier allows the compiler to skip function calls if the return value is unused, potentially bypassing critical security checks.
Vulnerable code:
() authorize(sender) inline {
throw_unless(187, equal_slice_bits(sender, addr1) | equal_slice_bits(sender, addr2));
}
Secure implementation:
() authorize(sender) impure inline {
throw_unless(187, equal_slice_bits(sender, addr1) | equal_slice_bits(sender, addr2));
}
Always add the impure modifier to functions that perform state changes or critical validations.
Incorrect use of modifying/non-modifying methods
Severity: 🔴 Critical
Using . instead of ~ for modifying methods means the original data structure remains unchanged, leading to logic errors.
Vulnerable code:
(_, slice old_balance_slice, int found?) = accounts.udict_delete_get?(256, sender);
Secure implementation:
(_, int found?) = accounts~udict_delete_get?(256, sender);
if(found?) {
;; accounts dictionary has been modified
}
- Non-modifying (
.): Returns modified copy, original unchanged - Modifying (
~): Modifies the original variable in place
Signed/unsigned integer vulnerabilities
Severity: 🔴 Critical
Improper handling of signed integers can allow attackers to exploit overflow/underflow conditions.
Vulnerable code:
(cell,()) transfer_voting_power(cell votes, slice from, slice to, int amount) impure {
int from_votes = get_voting_power(votes, from);
int to_votes = get_voting_power(votes, to);
from_votes -= amount; // Can become negative!
to_votes += amount;
votes~set_voting_power(from, from_votes);
votes~set_voting_power(to, to_votes);
return (votes,());
}
Secure implementation:
(cell,()) transfer_voting_power(cell votes, slice from, slice to, int amount) impure {
int from_votes = get_voting_power(votes, from);
int to_votes = get_voting_power(votes, to);
throw_unless(998, from_votes >= amount); // Validate sufficient balance
from_votes -= amount;
to_votes += amount;
votes~set_voting_power(from, from_votes);
votes~set_voting_power(to, to_votes);
return (votes,());
}
Insecure random number generation
Severity: 🔴 Critical
Using predictable sources like logical time for randomness allows attackers to predict and exploit outcomes.
Vulnerable code:
int seed = cur_lt(); // Predictable!
int seed_size = min(in_msg_body.slice_bits(), 128);
if(in_msg_body.slice_bits() > 0) {
seed += in_msg_body~load_uint(seed_size);
}
set_seed(seed);
if(rand(10000) == 7777) {
;; Attacker can predict this
}
Never rely on on-chain randomness for critical operations. Validators can influence or predict random values. Consider using commit-reveal schemes or external oracles for true randomness.
Missing bounced message handling
Severity: 🔴 Critical
Failing to handle bounced messages can lead to inconsistent state and fund loss.
Secure implementation:
() recv_internal(int msg_value, cell in_msg_full, slice in_msg_body) impure {
slice in_msg_full_slice = in_msg_full.begin_parse();
int msg_flags = in_msg_full_slice~load_msg_flags();
if (msg_flags & 1) { // Check bounced flag
on_bounce(in_msg_body);
return ();
}
;; Normal message processing
}
() on_bounce(slice in_msg_body) impure {
in_msg_body~skip_bits(32); // Skip 0xFFFFFFFF
int op = in_msg_body~load_op();
;; Handle specific bounced operations
if (op == op::transfer) {
;; Restore user balance
}
}
Sending private data on-chain
Severity: 🔴 Critical
All data stored on the blockchain is public and permanent, including transaction history.
Vulnerable approach:
;; DON'T: Storing password hash or private data
cell private_data = begin_cell()
.store_slice("secret_password_hash")
.store_uint(user_private_key, 256)
.end_cell();
Everything on blockchain is public. Transaction history, contract storage, and message contents are permanently visible to everyone.
Account destruction race conditions
Severity: 🔴 Critical
Destroying accounts without proper checks can lead to fund loss in race conditions.
Vulnerable code:
() recv_internal(msg_value, in_msg_full, in_msg_body) {
if (in_msg_body.slice_empty?()) {
return (); ;; Dangerous: empty message handling
}
;; Process and destroy account
send_raw_message(msg, 128 + 32); ;; Destroys account
}
Secure approach:
() recv_internal(msg_value, in_msg_full, in_msg_body) {
;; Proper validation before any destruction
throw_unless(error::unauthorized, authorized_sender?(sender));
;; Ensure no pending operations
throw_unless(error::pending_operations, safe_to_destroy?());
;; Then proceed with destruction if really needed
}
Missing replay protection
Severity: 🔴 Critical
External messages without replay protection can be re-executed multiple times.
Secure implementation:
() recv_external(slice in_msg) impure {
slice ds = get_data().begin_parse();
int stored_seqno = ds~load_uint(32);
int msg_seqno = in_msg~load_uint(32);
throw_unless(33, msg_seqno == stored_seqno); ;; Prevent replay
accept_message();
;; Update sequence number
set_data(begin_cell().store_uint(stored_seqno + 1, 32)...);
}