# Security best practices (https://docs-5plbi9fp8-ton-core-docs.vercel.app/llms/contract-dev/techniques/security/content.md)



There are several anti-patterns and potential attack vectors that smart contract developers should be aware of. These can affect the security, efficiency, and correctness of the contracts. Below are some common pitfalls and best practices to avoid them.

<Callout type="tip">
  Provide the `$tolk` skill from the [Acton development skillset](https://ton-blockchain.github.io/acton/docs/agent-skills/overview) to the agent, making it use Tolk best practices.
</Callout>

## Signed/unsigned integer vulnerabilities [#signedunsigned-integer-vulnerabilities]

Improper handling of signed integers can let attackers submit negative values that reverse the intended operation, exploiting overflow or underflow conditions. Tolk arithmetic uses TVM's 257-bit integers at runtime, while fixed-width types such as `uint64` primarily define serialization and range validation.

### Vulnerable code [#vulnerable-code]

```tolk
fun transferVotingPower(mutate votes: cell, from: slice, to: slice, amount: int): void {
    var fromVotes = getVotingPower(votes, from);
    var toVotes = getVotingPower(votes, to);

    fromVotes -= amount;  // Can become negative
    toVotes += amount;

    votes.setVotingPower(from, fromVotes);
    votes.setVotingPower(to, toVotes);
}
```

### Secure implementation [#secure-implementation]

```tolk
fun transferVotingPower(mutate votes: cell, from: slice, to: slice, amount: uint64): void {
    var fromVotes = getVotingPower(votes, from);
    var toVotes = getVotingPower(votes, to);

    // Validate sufficient balance
    assert (amount > 0) throw 997;
    assert (fromVotes >= amount) throw 998;

    fromVotes -= amount;
    toVotes += amount;

    votes.setVotingPower(from, fromVotes);
    votes.setVotingPower(to, toVotes);
}
```

## Sending sensitive data on-chain [#sending-sensitive-data-on-chain]

Contract state, messages, and computations must be treated as public. They can be inspected from blockchain data or retrieved through emulation. Hashing a low-entropy secret such as a password does not make it confidential because attackers can test candidate values off-chain.

### Vulnerable code [#vulnerable-code-1]

```tolk
// DON'T: Store password hash or private data
val privateData = beginCell()
    .storeString("secret_password_hash")
    .storeUint(userPrivateKey, 256)
    .endCell();
```

### Secure approach [#secure-approach]

None: do not send private or sensitive data on chain.

## Account destruction [#account-destruction]

The [send mode](https://docs-5plbi9fp8-ton-core-docs.vercel.app/llms/foundations/messages/modes/content.md) `SEND_MODE_CARRY_ALL_BALANCE | SEND_MODE_DESTROY` (`128 + 32`) transfers the remaining contract balance and destroys the account if its resulting balance is zero. This operation is irreversible and is highly vulnerable to race conditions: some messages previously sent to the contract might still be in flight.

### Vulnerable code [#vulnerable-code-2]

```tolk
fun onInternalMessage(in: InMessage) {
    if (in.body.isEmpty()) {
        return;  // Ignoring an empty top-up is normally safe
    }

    // Dangerous: no authorization or terminal-state validation
    sendRawMessage(msg, SEND_MODE_CARRY_ALL_BALANCE | SEND_MODE_DESTROY);  // Destroys the account!
}
```

### Secure approach [#secure-approach-1]

Before scheduling account destruction:

* authenticate using `in.senderAddress`, not an address supplied in the body;
* require an explicit terminal state;
* settle or cancel all pending operations;
* validate the destination receiving the remaining balance;
* document how later messages to the deleted address are handled.

```tolk
fun onInternalMessage(in: InMessage) {
    // Proper validation before any destruction
    assert (authorizedSender(in.senderAddress)) throw ErrCode.Unauthorized;

    // Ensure no pending operations
    assert (safeToDestroy()) throw ErrCode.PendingOperations;

    // Then proceed with destruction if really needed
}
```

Beware that a contract cannot generally prove that no messages remain in flight.

## Missing replay protection [#missing-replay-protection]

Replay protection is a security mechanism that prevents an attacker from [reusing a previous message](https://en.wikipedia.org/wiki/Replay_attack). External messages without replay protection can be executed multiple times by an attacker which could lead to fund loss or other undesired behavior.

Replay state is not authentication by itself: bind the sequence number to a signed request containing a contract-specific domain and expiration time.

### Secure implementation [#secure-implementation-1]

```tolk
// Incoming signed request
struct (0xBABECAFE) SignedRequest {
    contractId: uint32
    validUntil: uint32
    seqno: uint32
}

// Possible errors
enum ReplayErrors {
    InvalidSignature = 100,
    InvalidContract = 101,
    InvalidSeqno = 102,
    Expired = 103,
}

// Contract storage
struct Storage {
    publicKey: uint256
    contractId: uint32
    seqno: uint32
}

fun Storage.load() {
    return Storage.fromCell(contract.getData());
}

fun Storage.save(self) {
    contract.setData(self.toCell());
}

fun onExternalMessage(inMsgBody: slice) {
    val signature = inMsg.getLastBits(512);
    val signedSlice = inMsgBody.removeLastBits(512);
    val request = SignedRequest.fromSlice(signedSlice);
    var storage = lazy Storage.load();

    // Prevent all kinds of replay issues
    assert (request.contractId == storage.contractId)
        throw ReplayErrors.InvalidContract;
    assert (request.seqno == storage.seqno)
        throw ReplayErrors.InvalidSeqno;
    assert (request.validUntil > blockchain.now())
        throw ReplayErrors.Expired;
    assert (
        isSignatureValid(signedSlice.hash(), signature, storage.publicKey)
    ) throw ReplayErrors.InvalidSignature;

    // Update and store the sequence number
    acceptExternalMessage();

    storage.seqno += 1;
    storage.save();
    commitContractDataAndActions();
}
```

## Unconditional accepting of external messages [#unconditional-accepting-of-external-messages]

Incoming [external messages](https://docs-5plbi9fp8-ton-core-docs.vercel.app/llms/foundations/messages/external-in/content.md) do not transfer funds. Receiving smart contract must pay for their processing from its balance. Upon receiving an external message, a smart contract can spend some free gas ([`gas_credit`](https://docs-5plbi9fp8-ton-core-docs.vercel.app/llms/foundations/config/content.md)) in order to decide whether it is ready to accept it, and then pay for its further processing.

External messages are usually accepted through the [`ACCEPT`](https://docs-5plbi9fp8-ton-core-docs.vercel.app/llms/tvm/instructions/content.md) TVM instruction, which sets `gas_limit` to its maximum possible value specified in the network configuration, and sets `gas_credit` to zero. From this point on, the contract will have to pay for the entire processing of the incoming message.

Therefore, if `ACCEPT` is not guarded by a condition, an attacker might repeatedly send messages, and spend the entire balance of the contract.

The [`SETGASLIMIT`](https://docs-5plbi9fp8-ton-core-docs.vercel.app/llms/tvm/instructions/content.md) instruction can lead to the same issue.

### Vulnerable code [#vulnerable-code-3]

```tolk
fun onExternalMessage(inMsgBody: slice) {
    acceptExternalMessage();
    // ...
}
```

### Secure implementation [#secure-implementation-2]

Checks before accepting an external message vary by use case. The following example is a part of [wallet v3 code](https://github.com/ton-blockchain/ton/blob/53ec9684bd213983e1fe0f7610d3e3453a4ec628/crypto/smartcont/wallet3-code.fc), that doesn't accept a message if it isn't signed by the wallet's owner.

```tolk
fun onExternalMessage(inMsgBody: slice) {
    // parse message and contract storage (omitted)
    assert (msgSeqno == storedSeqno) throw 33;
    assert (subwalletId == storedSubwallet) throw 34;
    assert (isSignatureValid(inMsg.hash(), signature, publicKey)) throw 35;
    acceptExternalMessage();
    // handle the message (omitted)
}
```

## Invalid throw values [#invalid-throw-values]

Using `throw` with exit codes `0` and `1` can unexpectedly terminate execution. Since these codes indicate successful execution of the compute phase, it can be difficult to differentiate between failure and success. Prefer named developer-defined exit codes in the range `100` through `65535`.

## Gas limitation [#gas-limitation]

The [out of gas error](https://docs-5plbi9fp8-ton-core-docs.vercel.app/llms/tvm/exit-codes/content.md) cannot be handled in the contract's code. Measure worst-case execution paths and maintain explicit fee and reserve assumptions. Try to precompute gas consumption whenever possible.

Note that checking only the attached value against the computed gas is not sufficient. Storage fees, forwarding fees, outgoing action fees, variable execution paths, and any required balance reserve must also be considered.

### Secure implementation [#secure-implementation-3]

```tolk
struct Vote {
    votes: int32
}

const voteGasUsage = 10000;  // precompute with tests

fun onInternalMessage(in: InMessage) {
    val msg = Vote.fromSlice(in.body);

    assert (
        in.valueCoins > calculateGasFee(BASECHAIN, voteGasUsage)
    ) throw ErrCode.NotEnoughGas;  // Not enough gas!

    // ...subsequent logic...
}
```

## Insecure random numbers [#insecure-random-numbers]

Generating truly secure random numbers in TON is challenging. The built-in random functions are pseudo-random and depend on logical time. An attacker can predict the randomized number by [brute-forcing](https://en.wikipedia.org/wiki/Brute-force_attack) the logical time in the current block.

Call `random.initialize()` before `random.uint256()` or `random.range()` so repeated executions of the same contract in one block do not start from the same seed.

### Secure approach [#secure-approach-2]

* For critical applications, avoid relying solely on the on-chain solutions.

* Use built-in random functions 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*:

  1. Participants generate random numbers off-chain and send their hashes to the contract.
  2. Once all hashes are received, participants disclose their original numbers.
  3. Combine the disclosed numbers (e.g., by taking their sum) to produce a secure random value.

* Do not use randomization in external message receivers, as it remains vulnerable even with randomized logical time.

## Executing third-party code [#executing-third-party-code]

Executing untrusted code can compromise contract security.

### Prevention [#prevention]

```tolk
// Validate all external code before execution
assert (verifyCodeSignature(code)) throw ErrCode.UntrustedCode;
assert (validateCodeSafety(code)) throw ErrCode.InvalidCode;
```

## Race condition of messages [#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.

## Preventing front-running with signature verification [#preventing-front-running-with-signature-verification]

In TON blockchain, all pending messages are publicly visible in the mempool. Front-running can occur when an attacker observes a pending transaction containing a valid signature and quickly submits their own transaction using the same signature before the original transaction is processed.

### Secure approach [#secure-approach-3]

Include critical parameters like the recipient address (`to`) within the data that is signed. This ensures that the signature is valid only for the intended operation and recipient, preventing attackers from reusing the signature for their benefit. Also, implement replay protection to prevent the same signed message from being used multiple times.

```tolk
struct RequestBody {
    to: address
    seqno: uint64
}

struct (0x988d4037) Request {
    signature: bytes64
    requestBody: RequestBody
}

enum ErrCode {
    InvalidSignature = 1000,
    InvalidSeqno = 1001,
}

struct Storage {
    publicKey: uint256
    seqno: uint64
}

fun Storage.load() {
    return Storage.fromCell(contract.getData())
}

fun Storage.save(self) {
    contract.setData(self.toCell())
}

fun onInternalMessage(in: InMessage) {
    // ignore internal messages
}

fun onExternalMessage(inMsg: slice) {
    val request = Request.fromSlice(inMsg);
    var storage = lazy Storage.load();

    assert (
        isSignatureValid(
            request.requestBody.toCell().hash(),
            request.signature as slice,
            storage.publicKey
        )
    ) throw ErrCode.InvalidSignature;

    assert (request.requestBody.seqno == storage.seqno) throw ErrCode.InvalidSeqno;
    storage.seqno += 1;

    acceptExternalMessage();
    storage.save();

    val msg = createMessage({
      bounce: BounceMode.NoBounce,
      dest: request.requestBody.to,
      value: 0,
      body: "Your action payload here"
    });
    msg.send(SEND_MODE_REGULAR);
}

get fun seqno(): uint64 {
    val storage = lazy Storage.load();
    return storage.seqno;
}
```

Remember to also implement replay protection to prevent reusing the same signature even if it's correctly targeted.

### Vulnerable approach [#vulnerable-approach]

Do not sign data without including essential context like the recipient address. An attacker could intercept the message, copy the signature, and replace the recipient address in their own transaction, effectively redirecting the intended action or funds.

```tolk
struct (0x988d4037) Request {
    signature: bytes64
    data: RemainingBitsAndRefs  // `to` address is not part of the signed data
}

struct Storage {
    publicKey: uint256
}

fun Storage.load() {
    return Storage.fromCell(contract.getData())
}

fun onInternalMessage(in: InMessage) {
    val request = Request.fromSlice(in.body);
    val storage = lazy Storage.load();

    // The signature only verifies `request.data`, not the intended recipient.
    if (isSignatureValid(
        request.data.hash(),
        request.signature as slice,
        storage.publicKey
    )) {
        // Attacker can see this message, copy the signature, and send their own
        // message to a different `to` address before this one confirms.
        // The `in.senderAddress` here is the original sender, but the attacker can initiate
        // a similar transaction targeting themselves or another address.
        val msg = createMessage({
            bounce: BounceMode.NoBounce,
            dest: in.senderAddress,  // Vulnerable: recipient isn't verified by the signature
            value: 0,
        });
        msg.send(128);  // Caution: sending the whole balance!
    }
}
```

Furthermore, once a signature is used in a transaction, it becomes publicly visible on the blockchain. Without proper replay protection, anyone can potentially reuse this signature and the associated data in a new transaction if the contract logic doesn't prevent it.

## Return gas excesses carefully [#return-gas-excesses-carefully]

If excess gas is not returned to the sender, funds accumulate in contracts over time. This is suboptimal. Add a function to rake out excess, but popular contracts like [Jetton wallet](https://docs-5plbi9fp8-ton-core-docs.vercel.app/llms/standard/tokens/jettons/how-it-works/content.md) return it to the sender with a message using the `0xd53276db` opcode.

### Secure gas handling [#secure-gas-handling]

```tolk
struct (0xd53276db) Excesses {}

struct Vote {
    votes: uint32
}

struct Storage {
    votes: uint32
}

fun Storage.load() {
    return Storage.fromCell(contract.getData())
}

fun Storage.save(self) {
    contract.setData(self.toCell())
}

fun onInternalMessage(in: InMessage) {
    val msg = Vote.fromSlice(in.body);

    var storage = lazy Storage.load();
    storage.votes += msg.votes;
    storage.save();

    val excessesMsg = createMessage({
        bounce: BounceMode.NoBounce,
        dest: in.senderAddress,
        value: 0,
        body: Excesses {}
    });
    excessesMsg.send(
        SEND_MODE_IGNORE_ERRORS +
        SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE
    );
}
```

## Pulling data from another contract [#pulling-data-from-another-contract]

Contracts in the blockchain can reside in separate shards processed by another set of validators. This means that one contract cannot pull data from another contract. Specifically, no contract can call a getter function from another contract.

Thus, any on-chain communication is asynchronous and done by sending and receiving messages.

### Secure approach [#secure-approach-4]

Exchange messages to pull data from another contract.

Shared messages:

```tolk title="message.tolk"
struct (0x100001) GetMoney {}

struct (0x100002) ProvideMoney {}

struct (0x100003) TakeMoney {
    money: coins
}
```

```tolk title="OneContract.tolk"
import "message"

struct OneContractStorage {
    money: coins
}

fun OneContractStorage.load() {
    return OneContractStorage.fromCell(contract.getData())
}

fun onInternalMessage(in: InMessage) {
    val msg = lazy ProvideMoney.fromSlice(in.body);

    match (msg) {
        ProvideMoney => {
            val storage = lazy OneContractStorage.load();

            val reply = createMessage({
                bounce: BounceMode.NoBounce,
                dest: in.senderAddress,
                value: 0,
                body: TakeMoney {
                    money: storage.money
                }
            });
            reply.send(
                SEND_MODE_IGNORE_ERRORS +
                SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE
            );
        }

        else => {
            assert (in.body.isEmpty()) throw 0xFFFF;
        }
    }
}
```

```tolk title="AnotherContract.tolk"
import "message"

enum ErrCode {
    InvalidMoneyProvider = 100,
}

type AnotherContractMessage = GetMoney | TakeMoney

struct AnotherContractStorage {
    oneContractAddress: address
}

fun AnotherContractStorage.load() {
    return AnotherContractStorage.fromCell(contract.getData())
}

fun onInternalMessage(in: InMessage) {
    val msg = lazy AnotherContractMessage.fromSlice(in.body);
    val storage = lazy AnotherContractStorage.load();

    match (msg) {
        GetMoney => {
            val request = createMessage({
                bounce: BounceMode.NoBounce,
                dest: storage.oneContractAddress,
                value: 0,
                body: ProvideMoney {}
            });
            request.send(
                SEND_MODE_IGNORE_ERRORS +
                SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE
            );
        }

        TakeMoney => {
            assert (in.senderAddress == storage.oneContractAddress)
                throw ErrCode.InvalidMoneyProvider;
            // ...further processing...
        }

        else => {
            assert (in.body.isEmpty()) throw 0xFFFF;
        }
    }
}
```

## TON address representation issues [#ton-address-representation-issues]

[TON addresses](https://docs-5plbi9fp8-ton-core-docs.vercel.app/llms/foundations/addresses/formats/content.md) have multiple formats that must be handled correctly.

### Address formats [#address-formats]

```tolk
// Raw: 0:b4c1b2ede12aa76f4a44353944258bcc8f99e9c7c474711a152c78b43218e296
// Bounceable: EQC0wbLt4Sqnb0pENTlEJYvMj5npx8R0cRoVLHi0MhjilkPX
// Non-bounceable: UQC0wbLt4Sqnb0pENTlEJYvMj5npx8R0cRoVLHi0Mhjilh4S

// Always validate workchain
forceChain(toAddress);
```

## Name collision vulnerabilities [#name-collision-vulnerabilities]

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

### Best practice [#best-practice]

```tolk
// Use descriptive, unique names
var userBalance = 0;  // Instead of just `balance`
fun validateUserSignature() { }  // Instead of just `validate()`
```

## Incorrect data type handling [#incorrect-data-type-handling]

Reading or writing incorrect data types can corrupt contract state.

### Vulnerable code [#vulnerable-code-4]

```tolk
// Writing uint but reading int
val data = beginCell()
    .storeUint(value, 32)
    .endCell();

var storage = data.beginParse();
val readValue = storage.loadInt(32);  // Type mismatch
```

### Secure implementation [#secure-implementation-4]

```tolk
// Consistent type usage
val data = beginCell()
    .storeUint(value, 32)
    .endCell();

var storage = data.beginParse();
val readValue = storage.loadUint(32);
```

## Missing function return value checks [#missing-function-return-value-checks]

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

### Vulnerable code [#vulnerable-code-5]

```tolk
infoDict.delete(index);  // Ignoring success flag
```

### Secure implementation [#secure-implementation-5]

```tolk
val success = infoDict.delete(index);
assert (success) throw ErrCode.FailToDeleteDict;
```

## Contract code updates [#contract-code-updates]

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

### Secure implementation [#secure-implementation-6]

```tolk
fun updateCode(sender: address, newCode: cell) {
    assert (authorizedAdmin(sender)) throw ErrCode.Unauthorized;
    assert (validateCode(newCode)) throw ErrCode.InvalidCode;

    contract.setCodePostponed(newCode);
}
```
