Skip to main content

Low-level fees overview

caution

This section provides low-level instructions for interacting with TON; it includes raw formulas for calculating commissions and fees; however, most of these are already implemented as opcodes, so you should use those instead of manual calculations.

This document provides a general idea of transaction fees on TON and particularly computation fees for the FunC code. There is also a detailed specification in the TVM whitepaper.

Transactions and phases

As described in the TVM overview, transaction execution consists of a few phases. During those phases, the corresponding fees may be deducted. There is a high-level fees overview.

Storage fee

TON validators collect storage fees from smart contracts.

Storage fees are collected from the smart contract balance at the Storage phase of any transaction due to storage payments for the account state (including smart-contract code and data, if present) up to the present time. Even if a contract receives 1 nanoton, it will pay all the debt since the last payment. The smart contract may be frozen as a result. Only unique cells (by hash) are counted for storage and forward fees, i.e., three identical hash cells are counted as one. In particular, it deduplicates data: if there are several equivalent sub-cells referenced in different branches, their content is only stored once.

On TON, you pay for both smart-contract execution and used storage (see the @thedailyton article); the storage_fee depends on your contract size—the number of cells and the total number of bits across those cells—so you even pay a storage fee for having a TON wallet (even if it’s very small).

If you have not used your TON wallet for a long time, you will pay a larger fee than usual because the wallet pays fees on both sending and receiving transactions.

Note:

When a message is bounced from the contract, the contract will pay its current storage_fee.

Formula

You can approximately calculate storage fees for smart contracts using this formula:

storage_fee = ceil(
(account.bits * bit_price
+ account.cells * cell_price)
* time_delta / 2^16)

Let's examine each value more closely:

  • storage_fee — price for storage for time_delta seconds
  • account.cells — count of cells used by smart contract
  • account.bits — count of bits used by smart contract
  • cell_price — price of single cell
  • bit_price — price of single bit

Both cell_price and bit_price could be obtained from Network Config param 18.

Current values are:

  • WorkChain.
    bit_price_ps:1
    cell_price_ps:500
  • MasterChain.
    mc_bit_price_ps:1000
    mc_cell_price_ps:500000

Calculator example

You can use this JS script to calculate storage price for 1 MB in the workchain for 1 year

Live Editor
// Welcome to LIVE editor!
// feel free to change any variables
// Source code uses RoundUp for the fee amount, so does the calculator

function storageFeeCalculator() {
  const size = 1024 * 1024 * 8; // 1MB in bits
  const duration = 60 * 60 * 24 * 365; // 1 Year in secs

  const bit_price_ps = 1;
  const cell_price_ps = 500;

  const pricePerSec =
    size * bit_price_ps + Math.ceil(size / 1023) * cell_price_ps;

  let fee = Math.ceil((pricePerSec * duration) / 2 ** 16) * 10 ** -9;
  let mb = (size / 1024 / 1024 / 8).toFixed(2);
  let days = Math.floor(duration / (3600 * 24));

  let str = `Storage Fee: ${fee} TON (${mb} MB for ${days} days)`;

  return str;
}
Result
Loading...

Computation fees

Gas

All computation costs are denominated in gas units. The price of gas units is determined by this chain config (Config 20 for MasterChain and Config 21 for BaseChain) and may be changed only by consensus of validators. Note that unlike in other systems, the user cannot set his own gas price, and there is no fee market.

Current settings in BaseChain are as follows: 1 unit of gas costs 400 nanotons.

TVM instructions cost

On the lowest level (TVM instruction execution) the gas price for most primitives equals the basic gas price, computed as P_b := 10 + b + 5r, where b is the instruction length in bits and r is the number of cell references included in the instruction.

Apart from those basic fees, the following fees appear:

InstructionGAS priceDescription
Creation of cell500Operation of transforming builder to cell.
Parsing a cell for the first time100Operation of transforming cells into slices for the first time during the same transaction.
Parsing a cell repeatedly25Operation of transforming cells into slices, already parsed during the same transaction.
Throwing exception50
Operations on tuples1Price multiplied by the number of tuple elements.
Implicit Jump10It is paid when all instructions in the current continuation cell are executed. However, there are references in that continuation cell, and the execution flow jumps to the first reference.
Implicit Back Jump5It is paid when all instructions in the current continuation are executed and execution flow jumps back to the continuation from which the just finished continuation was called.
Moving stack elements1Charges the corresponding gas price per element; the first 32 elements moved are free.

FunC constructs gas fees

Almost all FunC functions used in this article are defined in stablecoin stdlib.fc contract, but you can use stdlib.fc from the TON source code as a reference which maps FunC functions to Fift assembler instructions. In turn, Fift assembler instructions are mapped to bit-sequence instructions in asm.fif. So if you want to understand how much exactly the instruction call will cost you, you need to find asm representation in stdlib.fc, then find bit-sequence in asm.fif and calculate instruction length in bits.

However, generally, fees related to bit-lengths are minor in comparison with fees related to cell parsing and creation, as well as jumps and just number of executed instructions.

So, if you try to optimize your code start with architecture optimization, the decreasing number of cell parsing/creation operations, and then with the decreasing number of jumps.

Operations with cells

Just an example of how proper cell work may substantially decrease gas costs.

Let's imagine that you want to add some encoded payload to the outgoing message. Straightforward implementation will be as follows:

slice payload_encoding(int a, int b, int c) {
return
begin_cell().store_uint(a,8)
.store_uint(b,8)
.store_uint(c,8)
.end_cell().begin_parse();
}

() send_message(slice destination) impure {
slice payload = payload_encoding(1, 7, 12);
var msg = begin_cell()
.store_uint(0x18, 6)
.store_slice(destination)
.store_coins(0)
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) ;; default message headers (see sending messages page)
.store_uint(0x33bbff77, 32) ;; opcode (see smart-contract guidelines)
.store_uint(cur_lt(), 64) ;; query_id (see smart-contract guidelines)
.store_slice(payload)
.end_cell();
send_raw_message(msg, 64);
}

What is the problem with this code? To generate a slice bit-string, payload_encoding first creates a cell via end_cell() (+500 gas units), then parses it with begin_parse() (+100 gas units); the same logic can be implemented without these operations by changing some commonly used types:

;; we add asm for function which stores one builder to the another, which is absent from stdlib
builder store_builder(builder to, builder what) asm(what to) "STB";

builder payload_encoding(int a, int b, int c) {
return
begin_cell().store_uint(a,8)
.store_uint(b,8)
.store_uint(c,8);
}

() send_message(slice destination) impure {
builder payload = payload_encoding(1, 7, 12);
var msg = begin_cell()
.store_uint(0x18, 6)
.store_slice(destination)
.store_coins(0)
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) ;; default message headers (see sending messages page)
.store_uint(0x33bbff77, 32) ;; opcode (see smart-contract guidelines)
.store_uint(cur_lt(), 64) ;; query_id (see smart-contract guidelines)
.store_builder(payload)
.end_cell();
send_raw_message(msg, 64);
}

By passing the bit-string in another form (a builder instead of a slice), we substantially decrease computation cost with a very small code change.

Inline and inline_refs

By default, when you have a FunC function, it gets its own id, stored in a separate leaf of id->function dictionary, and when you call it somewhere in the program, a search of the function in dictionary and subsequent jump occur. Such behavior is justified if your function is called from many places in the code and thus jumps allow to decrease the code size (by storing a function body once). However, if the function is only used once or twice, it is often much cheaper to declare this function as inline or inline_ref. inline modifier places the body of the function right into the code of the parent function, while inline_ref places the function code into the reference (jumping to the reference is still much cheaper than searching and jumping to the dictionary entry).

Dictionaries

Dictionaries on TON are introduced as trees (DAGs to be precise) of cells. That means that if you search, read, or write to the dictionary, you need to parse all cells of the corresponding branch of the tree. That means that

  • a) dictionary operations are not fixed in gas costs (since the size and number of nodes in the branch depend on the given dictionary and key)
  • b) it is expedient to optimize dict usage by using special instructions like replace instead of delete and add
  • c) developer should be aware of iteration operations (like next and prev), as well as min_key/max_key operations to avoid unnecessary iteration through the whole dict

Stack operations

Note that FunC manipulates stack entries under the hood. That means that the code:

(int a, int b, int c) = some_f();
return (c, b, a);

will be translated into a few instructions which change the order of elements on the stack.

When the number of stack entries is substantial (10+), and they are actively used in different orders, stack operation fees may become non-negligible.

Forward fees

Internal messages define an ihr_fee in nanotons, which is subtracted from the value attached to the message and awarded to the validators of the destination ShardChain if they include the message through the IHR mechanism. The fwd_fee is the original total forwarding fee paid for using the HR mechanism; it is automatically computed from the 24 and 25 configuration parameters and the size of the message at the time the message is generated. Note that the total value carried by a newly created internal outbound message equals the sum of the value, ihr_fee, and fwd_fee. This sum is deducted from the balance of the source account. Of these components, only the ihr_fee value is credited to the destination account upon message delivery. The fwd_fee is collected by the validators on the HR path from the source to the destination, and the ihr_fee is either collected by the validators of the destination ShardChain (if the message is delivered via IHR) or credited to the destination account.

IHR

What is IHR?

Instant Hypercube Routing (IHR) is an alternative mechanism for message delivery without intermediate hops between shards. To understand why IHR is not currently relevant:

  • IHR is not implemented and is not yet fully specified
  • IHR would only be relevant when the network has more than 16 shards and not all shards are neighbors to each other
  • Current network settings forbid splitting deeper than 16 shards, which means IHR is not relevant in any practical sense

In the current TON network configuration, all message routing uses standard Hypercube Routing (HR), which can handle message delivery efficiently with the current shard topology. The ihr_fee field exists in the message structure for future compatibility, but serves no functional purpose today.

If you set the ihr_fee to a non-zero value, it will always be added to the message value upon receipt. For now, there are no practical reasons to do this.

Formula

// In_msg and Ext_msg are using the same method of calculation
// It is called import_fee or in_fwd_fee for the Ext_msg
// https://github.com/ton-blockchain/ton/blob/7151ff26279fef6dcfa1f47fc0c5b63677ae2458/crypto/block/transaction.cpp#L2071-L2090

// bits in the root cell of a message are not included in msg.bits (lump_price pays for them)
msg_fwd_fees = (lump_price
+ ceil(
(bit_price * msg.bits + cell_price * msg.cells) / 2^16)
);

ihr_fwd_fees = ceil((msg_fwd_fees * ihr_price_factor) / 2^16);

total_fwd_fees = msg_fwd_fees + ihr_fwd_fees; // ihr_fwd_fees - is 0 for external messages
IMPORTANT

Please note that msg_fwd_fees above includes action_fee below. For a basic message this fee = lump_price = 400000 nanotons, action_fee = (400000 * 21845) / 65536 = 133331. Or approximately a third of the msg_fwd_fees.

fwd_fee = msg_fwd_fees - action_fee = 266669 nanotons = 0.000266669 TON

Action fee

The action fee is deducted from the balance of the source account during the processing of the action list, which occurs after the Compute phase. Practically, the only action for which you pay an action fee is SENDRAWMSG. Other actions, such as RAWRESERVE or SETCODE, do not incur any fee during the action phase.

action_fee = floor((msg_fwd_fees * first_frac)/ 2^16);  //internal

action_fee = msg_fwd_fees; //external

first_frac is part of the 24 and 25 parameters (for master chain and work chain) of the TON Blockchain. Currently, both are set to a value of 21845, which means that the action_fee is approximately a third of the msg_fwd_fees. In the case of an external message action, SENDRAWMSG, the action_fee is equal to the msg_fwd_fees.

tip

Remember that an action register can contain up to 255 actions, which means that all formulas related to fwd_fee and action_fee will be computed for each SENDRAWMSG action, resulting in the following sum:

total_fees = sum(action_fee) + sum(total_fwd_fees);

Starting from the fourth global version of TON, if a "send message" action fails, the account is required to pay for processing the cells of the message, referred to as the action_fine.

fine_per_cell = floor((cell_price >> 16) / 4)

max_cells = floor(remaining_balance / fine_per_cell)

action_fine = fine_per_cell * min(max_cells, cells_in_msg);

Fees config file

All fees are denominated in nanotons, or in nanotons multiplied by 2^16 to maintain accuracy while using integers and may be changed. The config file represents the current fee cost.

References

  • Based on @thedailyton article from July 24th

See also

Was this article useful?