Skip to main content

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.

Critical

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));
}
Best practice

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
}
Key difference
  • 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
}
warning

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 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();
warning

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)...);
}

Improper message bounce handling

Severity: 🔴 Critical

Using non-bounceable messages when bounceable is needed can cause fund loss.

Vulnerable code:

var msg = begin_cell()
.store_uint(0x10, 6) ;; Non-bounceable
.store_slice(to_address)
.store_coins(amount)
.end_cell();

Secure implementation:

var msg = begin_cell()
.store_uint(0x18, 6) ;; Bounceable message
.store_slice(to_address)
.store_coins(amount)
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
.store_uint(op::excesses(), 32)
.end_cell();

Executing third-party code

Severity: 🔴 Critical

Executing untrusted code can compromise contract security.

Prevention:

;; Validate all external code before execution
throw_unless(error::untrusted_code, verify_code_signature(code));
throw_unless(error::invalid_code, validate_code_safety(code));

Medium

Race conditions in message flows

Severity: 🟡 Medium

Message cascades can span multiple blocks, allowing attackers to initiate parallel flows.

Best practice:

() handle_transfer(slice sender, int amount) impure {
int current_balance = get_balance(sender);
throw_unless(error::insufficient_funds, current_balance >= amount);

;; Don't assume balance will remain the same in subsequent messages
}

Improper gas management

Severity: 🟡 Medium

Failing to return excess gas or miscalculating gas requirements.

Secure gas handling:

int ton_balance_before_msg = my_ton_balance - msg_value;
int storage_fee = const::min_tons_for_storage - min(ton_balance_before_msg, const::min_tons_for_storage);
msg_value -= storage_fee + const::gas_consumption;

if (msg_value > 0) {
var msg = begin_cell()
.store_uint(0x18, 6) // Bounceable message
.store_slice(response_address)
.store_coins(msg_value)
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
.store_uint(op::excesses(), 32)
.store_uint(query_id, 64)
.end_cell();
send_raw_message(msg, 1);
}

Fake jetton token validation

Severity: 🟡 Medium

Accepting transfers without validating the sender's jetton wallet address.

Secure validation:

() handle_jetton_transfer(slice sender_address, int jetton_amount, slice from_user) impure {
;; Calculate expected jetton wallet address
slice expected_wallet = calculate_jetton_wallet_address(from_user, jetton_master_address);

throw_unless(error::invalid_jetton_wallet, equal_slice_bits(sender_address, expected_wallet));

;; Process valid transfer
}

Missing function return value checks

Severity: 🟡 Medium

Ignoring function return values can lead to logic errors and unexpected behavior.

Vulnerable code:

dictinfos~udict_delete?(32, index);  ;; Ignoring success flag

Secure implementation:

int success = dictinfos~udict_delete?(32, index);
throw_unless(error::fail_to_delete_dict, success);

Name collision vulnerabilities

Severity: 🟡 Medium

Function or variable names can collide with built-in functions or reserved keywords.

Best practice:

;; Use descriptive, unique names
int user_balance = 0; ;; Instead of just 'balance'
() validate_user_signature() ;; Instead of just 'validate()'

Incorrect data type handling

Severity: 🟡 Medium

Reading or writing incorrect data types can corrupt contract state.

Vulnerable code:

;; Writing uint but reading int
storage~store_uint(value, 32);
int read_value = storage~load_int(32); ;; Type mismatch

Secure implementation:

;; Consistent type usage
storage~store_uint(value, 32);
int read_value = storage~load_uint(32);

Contract code updates

Severity: 🟡 Medium

Contracts can be updated if not properly protected, changing their behavior unexpectedly.

Secure implementation:

() update_code(cell new_code) impure {
throw_unless(error::unauthorized, authorized_admin?(sender()));
throw_unless(error::invalid_code, validate_code?(new_code));

set_code(new_code);
}

TON address representation issues

Severity: 🟡 Medium

TON addresses have multiple representations that must be handled correctly.

Address formats:

;; Raw: 0:b4c1b2ede12aa76f4a44353944258bcc8f99e9c7c474711a152c78b43218e296
;; Bounceable: EQC0wbLt4Sqnb0pENTlEJYvMj5npx8R0cRoVLHi0MhjilkPX
;; Non-bounceable: UQC0wbLt4Sqnb0pENTlEJYvMj5npx8R0cRoVLHi0Mhjilh4S

;; Always validate workchain
force_chain(to_address);

Carry-value pattern violations

Severity: 🟡 Medium

Not following carry-value pattern can lead to inconsistent state across contracts.

Correct pattern:

;; Sender contract subtracts and sends
sender_balance -= amount;
send_transfer_message(destination, amount);

;; Receiver contract receives and adds
receiver_balance += received_amount;

Transaction phases misunderstanding

Severity: 🟡 Medium

Not understanding TON's 5-phase transaction model can lead to unexpected behavior.

Transaction phases
  1. Storage phase: Account state loading
  2. Credit phase: Value crediting
  3. Compute phase: Contract execution
  4. Action phase: Message sending
  5. Bounce phase: Error handling

Cross-contract data access

Severity: 🟡 Medium

Attempting to directly access other contract data instead of using message-based communication.

Correct approach:

;; Don't try to read other contract's data directly
;; Use message-based communication instead
send_get_data_request(target_contract, query_id);

Predefined method IDs

Severity: 🟡 Medium

Not properly understanding predefined method IDs can cause conflicts.

Reserved method IDs:

() recv_internal(int msg_value, cell in_msg_cell, slice in_msg) impure {  ;; method_id = 0
}

() recv_external(slice in_msg) impure { ;; method_id = -1
}

Security checklist

Before deploying your smart contract, ensure you've addressed:

Critical security checks

  • All functions with side effects have impure modifier
  • Correct use of modifying (~) vs non-modifying (.) methods
  • Proper validation of integer operations and ranges
  • Bounced message handling implemented
  • No private data stored on-chain
  • Account destruction safety measures
  • Replay protection for external messages
  • Bounceable vs non-bounceable message usage
  • Third-party code execution safety
  • Avoid on-chain randomness for critical operations

Important considerations

  • Gas calculation and excess return logic
  • Input validation for all external calls
  • Race condition considerations in message flows
  • Jetton wallet address validation (if applicable)
  • Function return values checked
  • Name collision prevention
  • Correct data type usage
  • Code update protection mechanisms
  • Proper address format handling
  • Carry-value pattern compliance
  • Understanding of transaction phases
  • Message-based cross-contract communication
  • Awareness of predefined method IDs

Testing and auditing

Security first
  • Use Blueprint testing framework for comprehensive testing
  • Conduct formal security audits before mainnet deployment
  • Test edge cases and failure scenarios
  • Verify bounced message handling

See also

References

Was this article useful?