ton-best-practices

Use when auditing, reviewing, writing, or testing TON smart contracts in Tolk. Security vulnerabilities, async model pitfalls, bounce message handling, gas management, access control, serialization, audit checklists. Triggers: Tolk, TVM, TVM 12, TON contract, jetton, NFT TON, TON audit, bounce message, smart contract security.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "ton-best-practices" with this command: npx skills add elsvv/ton-best-practices-skill/elsvv-ton-best-practices-skill-ton-best-practices

TON Smart Contract Best Practices (Tolk)

Overview

TON uses an asynchronous actor model: one transaction changes state of one account processing one message. A single Ethereum tx can span thousands of TON txs across hundreds of blocks. This creates unique vulnerability classes absent from Solidity.

Language: Tolk v1.2 -- compiles to TVM 12 bytecode. Modern syntax, explicit mutation, union types, lazy fields, built-in message construction APIs.

Based on: 233 vulnerabilities from 34 audits (29 projects, 11 firms). Top findings: logical errors (70), auth (25), centralization (19).

Key reference files:

  • vulnerabilities.md — Full vulnerability catalog with code examples
  • audit-checklist.md — Complete audit checklist
  • tolk-security.md — Tolk-specific language pitfalls
  • tvm-async.md — TVM internals, async model, bounce messages (Tolk 1.2 / TVM 12)
  • tolk-best-practices.md — Tolk language best practices

TON vs EVM: Critical Differences

AspectTONEVM
ExecutionAsync message-passingSynchronous, atomic
Cross-contractAsync messages only (no sync calls)Synchronous calls
ReentrancyNot possible (classical form)Major attack class
Failure recoveryManual bounce handlers requiredAutomatic revert
Token standardJetton (separate wallet per user)ERC-20 (central mapping)
Gas OOGCannot be caught in try/catchReverts entire tx
Replay protectionMust implement manually (seqno)Protocol nonces
StorageManual cell serialization, 65536 cell limit256-bit slots, unbounded
Account freezingYes (storage debt -> frozen -> deleted)No
RandomnessValidator-manipulable (block seed)Same issue

Top 10 Critical Vulnerabilities (Quick Reference)

1. Unauthorized Access / Missing Auth Checks -- CRITICAL

All state-mutating functions must verify sender identity.

// WRONG -- anyone can call this
fun transferOwnership(newOwner: address) {
    owner = newOwner;
    saveData();
}

// CORRECT -- verify sender
fun transferOwnership(msg: InternalMessage, newOwner: address) {
    assert(msg.sender == owner, 401);
    owner = newOwner;
    saveData();
}

2. Integer Overflow/Underflow -- CRITICAL

Tolk int is 257-bit signed. Overflow is silent at runtime -- only caught during cell serialization (exit code 5) when the value exceeds the field's bit width. Sized types (uint32, uint64, etc.) overflow silently in arithmetic until stored.

// DANGEROUS -- silent overflow in arithmetic
var balance: uint64 = maxUint64;
balance = balance + 1; // wraps silently, NO runtime error

// Only caught here when serializing to cell:
beginCell().storeUint(balance, 64); // exit code 5 if out of range

// CORRECT -- validate before arithmetic
assert(balance + amount >= balance, 400); // overflow check
assert(fromVotes >= amount, 998);         // underflow check
fromVotes = fromVotes - amount;

3. Reentrancy via Async Messages -- HIGH

While chain A->B->C processes, attacker launches parallel chain. State checked at start may be invalid later.

Fix: Carry-value pattern -- embed critical values in message payload, don't query state. Debit balance immediately before sending dependent messages.

4. Lazy Loading Validation Bypass -- HIGH

Tolk's lazy fields defer deserialization. Unloaded portions bypass schema validation.

struct VaultData {
    owner: address,
    lazy config: VaultConfig, // NOT loaded until accessed
    lazy ledger: Ledger,      // NOT loaded until accessed
}

// DANGEROUS -- config and ledger are NOT validated on load
fun onInternalMessage(msg: InternalMessage) {
    var data = loadData<VaultData>();
    // If only 'owner' is checked, a malformed config/ledger
    // passes deserialization silently
    assert(msg.sender == data.owner, 401);
    // ... operates without ever touching lazy fields
    saveData(data); // re-serializes unvalidated lazy data as-is
}

// CORRECT -- explicitly load and validate lazy fields when their
// invariants matter for the operation's correctness
fun onInternalMessage(msg: InternalMessage) {
    var data = loadData<VaultData>();
    assert(msg.sender == data.owner, 401);
    var config = data.config; // force load -- triggers deserialization
    assert(config.minDeposit > 0, 402); // validate invariants
}

5. Non-Exhaustive Union Type Dispatch -- HIGH

A catch-all else branch in match silently swallows unknown message types, hiding bugs or allowing unexpected operations.

union IncomingOp {
    Transfer,
    Burn,
    Mint,
    UpdateConfig,
}

// DANGEROUS -- else hides unhandled ops
fun dispatch(op: IncomingOp) {
    match op {
        Transfer => handleTransfer(op),
        Burn => handleBurn(op),
        else => { } // silently ignores Mint and UpdateConfig!
    }
}

// CORRECT -- exhaustive match, compiler enforces all arms
fun dispatch(op: IncomingOp) {
    match op {
        Transfer => handleTransfer(op),
        Burn => handleBurn(op),
        Mint => handleMint(op),
        UpdateConfig => handleUpdateConfig(op),
    }
}

6. Incorrect Message Mode Flags -- HIGH

  • Mode 64 after mode 64 in same tx = subsequent messages fail (balance already forwarded)
  • Mode 128 + 32 = send all + destroy account -- must be authorization-gated
  • Flag +2 (IgnoreErrors) = silent failures, state changes persist with no rollback notification
// Use Tolk's createMessage API with mode enums
createMessage(MessageFlags.NonBounce)
    .storeAddress(destination)
    .storeCoins(amount)
    .send(SendMode.RemainingValue | SendMode.IgnoreErrors); // 64 + 2

// DANGEROUS -- mode 128 + 32 without auth check
createMessage(MessageFlags.NonBounce)
    .storeAddress(attacker)
    .storeCoins(0)
    .send(SendMode.RemainingBalance | SendMode.DestroyOnZero); // drains + destroys

7. Storage Deserialization Vulnerability -- HIGH

Mismatched load/store calls or disabled assertEndAfterReading allows extra data injection.

// DANGEROUS -- extra data in slice is silently ignored
fun loadConfig(s: slice): Config {
    var owner = s.loadAddress();
    var amount = s.loadCoins();
    // missing assertEndAfterReading -- attacker can append extra data
    return Config { owner, amount };
}

// CORRECT -- ensure entire slice is consumed
fun loadConfig(s: slice): Config {
    var owner = s.loadAddress();
    var amount = s.loadCoins();
    s.assertEndAfterReading(); // throws if leftover bits/refs
    return Config { owner, amount };
}

8. Unsafe Null Assertion (! Operator) -- HIGH

! force-unwrap on nullable types crashes at runtime (TVM exit code 7) if null.

// DANGEROUS -- crashes if jettonWallet is null
fun forwardToWallet(data: ContractData) {
    var wallet = data.jettonWallet!; // TVM crash if null
    sendTransfer(wallet, amount);
}

// CORRECT -- check before unwrap
fun forwardToWallet(data: ContractData) {
    if (data.jettonWallet == null) {
        throw(404); // explicit, testable error
    }
    var wallet = data.jettonWallet!;
    sendTransfer(wallet, amount);
}

// ALSO CORRECT -- use pattern matching
fun forwardToWallet(data: ContractData) {
    match data.jettonWallet {
        null => throw(404),
        wallet => sendTransfer(wallet, amount),
    }
}

9. Bounce Message Handling Errors -- HIGH

Bounceable message fails -> bounce returns -> no handler = permanent fund loss.

BounceMode selection (Tolk 1.2 / TVM 12):

  • BounceMode.Only256BitsOfBody (legacy) -- bounce returns only first 256 bits of original body after the 0xFFFFFFFF prefix. Insufficient for complex recovery.
  • BounceMode.RichBounce (TVM 12) -- bounce returns the FULL original message body with prefix 0xFFFFFFFE. Enables complete state recovery from bounced messages.
// Bounce handler in Tolk 1.2
fun onBounceMessage(msg: BouncedMessage) {
    if (msg.isRichBounce()) {
        // Full body available (prefix 0xFFFFFFFE)
        var body = msg.body;
        var op = body.loadUint(32);
        if (op == OP_INTERNAL_TRANSFER) {
            var jettonAmount = body.loadCoins();
            var fromAddress = body.loadAddress();
            // Full recovery -- restore total_supply
            totalSupply = totalSupply - jettonAmount;
            saveData();
        }
    } else {
        // Legacy bounce (prefix 0xFFFFFFFF, only 256 bits)
        var body = msg.body;
        body.skipBits(32); // skip op
        var jettonAmount = body.loadCoins();
        // Partial recovery with limited data
        totalSupply = totalSupply - jettonAmount;
        saveData();
    }
}

Send messages with BounceMode.RichBounce on TVM 12 to enable full error recovery.

10. Gas Exhaustion / TON Draining -- MEDIUM

Gas exhaustion mid-flow leaves state inconsistent. OOG cannot be caught.

// Validate gas BEFORE expensive operations
assert(msg.value > getComputeFee(voteGasUsage, false), 400);

Design each handler to be atomic. Use bounce handlers to restore state on failure. No unbounded loops over user-controlled data.


Transaction Phases (Security Impact)

Storage Phase -> Credit Phase -> Compute Phase -> Action Phase -> Bounce Phase
  • Storage phase deducts rent BEFORE credit -- if accumulated debt > incoming value -> freeze
  • Compute phase failure: state reverts, bounce triggers automatically
  • Action phase failure: state changes persist but messages NOT sent; bounce only if flag +16 set
  • Bounce phase: only fires if inbound message had bounce bit AND there was a failure

Critical rule: "We cannot allow fails in action phase since there will be no bounce. Check and throw in computation phase."


Carry-Value Pattern

TON cannot query another contract's state synchronously. Embed the value in the message itself.

BAD:  A sends request to B asking "what's your balance?"
       B sends back balance
       A uses balance (may be stale by now)

GOOD: A embeds required_amount in message to B
       B processes with the embedded amount
       B responds with result embedded in bounce/response

Gas Management Rules

  1. Pre-calculate gas for each handler; validate msg.value >= computeFee + forwardFee
  2. Return excess gas: send with SendMode.RemainingValue | SendMode.IgnoreErrors with op 0xd53276db
  3. No unbounded loops over user-controlled data structures
  4. No infinite storage growth -- tokenize into separate contracts if needed
  5. Storage fees drain balance independently of message value -- account for rent
  6. If contract can't afford bounce -- silent failure, funds lost

Quick Security Checklist (Tolk)

[] All state-mutating functions check `sender == owner` or equivalent
[] `lazy` fields: verify all required fields are loaded before validation
[] Union type `match`: exhaustive -- no hidden `else` branch
[] Nullable types: no unsafe `!` force-unwrap on untrusted data
[] Integer arithmetic: check overflow before/after operation (sized types)
[] Message modes: correct flags in `createMessage`, `BounceMode` appropriate
[] `assertEndAfterReading` not disabled (prevents extra data injection)
[] Bounce handler uses correct `BounceMode` (prefer `RichBounce` on TVM 12)
[] External messages: seqno + expiration + signature validation all present
[] Async flows: balance debited immediately before sending dependent messages

Tools

ToolPurpose
Tolk CompilerTolk v1.2 (tolk-js@1.2.0), targets TVM version 12
MistiStatic analyzer -- 42 detectors, CI/CD ready
TSASymbolic execution analyzer (bytecode-level)
@ton/sandboxTest harness with printTransactionFees()
BugMagnifierAsync race condition simulator
verifier.ton.orgSource code verification

Run Misti as minimum; get professional audit for production.


Audit Firms (TON-Specialized)

TonBit, Beosin, Quantstamp, Nowarp, Hacken, CertiK, SlowMist, Zellic, Positive Technologies, Cantina/Spearbit, Trail of Bits

CTF / Practice

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

yandex-direct

No summary provided by upstream source.

Repository SourceNeeds Review
General

yandex-metrica

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

yandex-cloud-cli

No summary provided by upstream source.

Repository SourceNeeds Review
General

tonapi

No summary provided by upstream source.

Repository SourceNeeds Review