Skip to main content

Transaction fees

Fees on TON are calculated using this formula:
FORMULAS
transaction_fee = storage_fees
                + in_fwd_fees // also called import fee
                + computation_fees
                + action_fees
                + out_fwd_fees
All fees are denominated in nanotons (often scaled by 2^16 for precision) and come from network configuration:
  • storage_fees: param 18
  • in_fwd_fees: params 24 and 25
  • computation_fees: params 20 and 21
  • action_fees: params 24 and 25
  • out_fwd_fees: params 24 and 25
  • storage_fees is the amount you pay for storing a smart contract on the blockchain. In fact, you pay for every second the smart contract is stored on the blockchain.
    • Example: your TON wallet is also a smart contract, and it pays a storage fee every time you receive or send a transaction.
  • in_fwd_fees is a charge for importing messages only from outside the blockchain, for example, external messages. Every time you make a transaction, it must be delivered to the validators who will process it. For ordinary messages from contract to contract, this fee does not apply. Read the TON Blockchain paper to learn more about inbound messages.
    • Example: each transaction you make with your wallet app (like Tonkeeper) must first be distributed among validators.
  • computation_fees is the amount you pay for executing code in the virtual machine. Computation fees depend on executed operations (gas used), not code size.
    • Example: each time you send a transaction with your wallet (which is a smart contract), you execute the code of your wallet contract and pay for it.
  • action_fees is a charge for sending outgoing messages made by a smart contract, updating the smart contract code, updating libraries, etc.
  • out_fwd_fees is a charge for forwarding outgoing internal messages within TON between shardchains; it depends on message size.

Storage fee

Storage fee for smart contracts is calculated using the following formula, values are defined in network config param 18:
FORMULAS
storage_fee = ceil(
                   (account.bits * bit_price
                   + account.cells * cell_price)
                * time_delta / 2^16)
TypeScript example
import { Cell, beginCell } from '@ton/core';

// Read latest storage prices (config param 18)
const storage = getStoragePrices(configCell);

// Account state as a Cell (e.g., code+data root)
const accountRoot: Cell = /* load from blockchain */;
const stats = collectCellStats(accountRoot, []);

// Charge for one hour
const fee = shr16ceil(
  (stats.bits * storage.bit_price_ps + stats.cells * storage.cell_price_ps) *
    3600n
);
See Helper functions appendix for full implementations.
DeduplicationThe system counts only unique hash cells for storage and forward fees. For example, it counts three identical hash cells as one. This mechanism deduplicates data by storing the content of multiple equivalent sub-cells only once, even if they are referenced across different branches. Read more about deduplication.

Gas fee

All computation is measured in gas units; each TVM operation has a fixed gas cost.
The gas price is defined by network configuration and can not be set by users.
  • Basechain: 1 gas = 26214400 / 2^16 nanotons = 0.0000004 TON
  • Masterchain: 1 gas = 655360000 / 2^16 nanotons = 0.00001 TON
See config parameters 20 and 21 for current gas prices.
The values can change through validator governance.
TypeScript example
const gasUsed = 50_000n;
const prices = getGasPrices(configCell, 0); // 0 = basechain
const gasFee = 
  gasUsed <= prices.flat_gas_limit
    ? prices.flat_gas_price
    : prices.flat_gas_price + 
      (prices.gas_price * (gasUsed - prices.flat_gas_limit)) / 65536n;
See Helper functions appendix for full implementations.

Forward fee

Forward fee for message size (msg.bits, msg.cells) per params 24/25:
FORMULAS
// 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)
             );

TypeScript example
const msgPrices = getMsgPrices(configCell, 0);
const total =
  msgPrices.lumpPrice +
  shr16ceil(
    msgPrices.bitPrice * BigInt(bits) +
      msgPrices.cellPrice * BigInt(cells)
  );
const actionFee = (total * msgPrices.firstFrac) >> 16n;
const forwardFee = total - actionFee;

// From a Cell or Message:
const fwdFromCell = computeCellForwardFees(msgPrices, messageRootCell);
// For a full internal Message object (validates default lump case, handles init):
const details = computeMessageForwardFees(msgPrices, internalMessage);
Invalid type passed!
Received: info
Expected one of: note, tip, caution, danger

Import fee

Import fee is the same as forward fee for inbound external messages.

Action fee

Action fee is charged in the Action phase and is the sender’s share of the forward fee.
You pay it for SENDRAWMSG; actions like RAWRESERVE, SETCODE do not incur the fees.
FORMULAS
action_fee = floor((msg_fwd_fees * first_frac)/ 2^16);  // internal

action_fee = msg_fwd_fees;  // external
first_frac (params 24/25) divided by 2^16 ≈ 1/3 of msg_fwd_fees. Action fine (failed send): Starting with Global Version 4, a failed “send message” action incurs a penalty proportional to the attempted message size. It is calculated as:
FORMULAS
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);

Helper functions (full code)

import { Cell, Slice, beginCell, Dictionary, Message, DictionaryValue } from '@ton/core';

export type GasPrices = {
	flat_gas_limit: bigint,
	flat_gas_price: bigint,
	gas_price: bigint
};

export type StorageValue = {
    utime_since: number,
    bit_price_ps: bigint,
    cell_price_ps: bigint,
    mc_bit_price_ps: bigint,
    mc_cell_price_ps: bigint
};

export class StorageStats {
    bits: bigint;
    cells: bigint;

    constructor(bits?: number | bigint, cells?: number | bigint) {
        this.bits  = bits  !== undefined ? BigInt(bits)  : 0n;
        this.cells = cells !== undefined ? BigInt(cells) : 0n;
    }
    add(...stats: StorageStats[]) {
        let cells = this.cells, bits = this.bits;
        for (let stat of stats) {
            bits  += stat.bits;
            cells += stat.cells;
        }
        return new StorageStats(bits, cells);
    }
    addBits(bits: number | bigint) {
        return new StorageStats(this.bits + BigInt(bits), this.cells);
    }
    addCells(cells: number | bigint) {
        return new StorageStats(this.bits, this.cells + BigInt(cells));
    }
}

function shr16ceil(src: bigint) {
    const rem = src % 65536n;
    let res = src / 65536n;
    if (rem !== 0n) res += 1n;
    return res;
}

export function collectCellStats(cell: Cell, visited: Array<string>, skipRoot: boolean = false): StorageStats {
    let bits  = skipRoot ? 0n : BigInt(cell.bits.length);
    let cells = skipRoot ? 0n : 1n;
    const hash = cell.hash().toString();
    if (visited.includes(hash)) {
        return new StorageStats();
    }
    visited.push(hash);
    for (const ref of cell.refs) {
        const r = collectCellStats(ref, visited);
        cells += r.cells;
        bits += r.bits;
    }
    return new StorageStats(bits, cells);
}

export function getGasPrices(configRaw: Cell, workchain: 0 | -1): GasPrices {
    const config = configRaw.beginParse().loadDictDirect(Dictionary.Keys.Int(32), Dictionary.Values.Cell());
    const ds = config.get(21 + workchain)!.beginParse();
    if (ds.loadUint(8) !== 0xd1) throw new Error('Invalid flat gas prices tag');
    const flat_gas_limit = ds.loadUintBig(64);
    const flat_gas_price = ds.loadUintBig(64);
    if (ds.loadUint(8) !== 0xde) throw new Error('Invalid gas prices tag');
    return { flat_gas_limit, flat_gas_price, gas_price: ds.preloadUintBig(64) };
}

export function computeGasFee(prices: GasPrices, gas: bigint): bigint {
    if (gas <= prices.flat_gas_limit) return prices.flat_gas_price;
    return prices.flat_gas_price + (prices.gas_price * (gas - prices.flat_gas_limit)) / 65536n;
}

export const storageValue: DictionaryValue<StorageValue> = {
    serialize: (src, builder) => {
        builder
            .storeUint(0xcc, 8)
            .storeUint(src.utime_since, 32)
            .storeUint(src.bit_price_ps, 64)
            .storeUint(src.cell_price_ps, 64)
            .storeUint(src.mc_bit_price_ps, 64)
            .storeUint(src.mc_cell_price_ps, 64);
    },
    parse: (src) => {
        return {
            utime_since: src.skip(8).loadUint(32),
            bit_price_ps: src.loadUintBig(64),
            cell_price_ps: src.loadUintBig(64),
            mc_bit_price_ps: src.loadUintBig(64),
            mc_cell_price_ps: src.loadUintBig(64)
        };
    }
};

export function getStoragePrices(configRaw: Cell): StorageValue {
    const config = configRaw.beginParse().loadDictDirect(Dictionary.Keys.Int(32), Dictionary.Values.Cell());
    const storageData = Dictionary.loadDirect(Dictionary.Keys.Uint(32), storageValue, config.get(18)!);
    const values = storageData.values();
    return values[values.length - 1];
}

export function calcStorageFee(prices: StorageValue, stats: StorageStats, duration: bigint) {
    return shr16ceil((stats.bits * prices.bit_price_ps + stats.cells * prices.cell_price_ps) * duration);
}

export const configParseMsgPrices = (sc: Slice) => {
    const magic = sc.loadUint(8);
    if (magic !== 0xea) throw new Error('Invalid message prices magic number');
    return {
        lumpPrice: sc.loadUintBig(64),
        bitPrice: sc.loadUintBig(64),
        cellPrice: sc.loadUintBig(64),
        ihrPriceFactor: sc.loadUintBig(32),
        firstFrac: sc.loadUintBig(16),
        nextFrac: sc.loadUintBig(16)
    };
};

export type MsgPrices = ReturnType<typeof configParseMsgPrices>;

export const getMsgPrices = (configRaw: Cell, workchain: 0 | -1) => {
    const config = configRaw.beginParse().loadDictDirect(Dictionary.Keys.Int(32), Dictionary.Values.Cell());
    const prices = config.get(25 + workchain);
    if (prices === undefined) throw new Error('No prices defined in config');
    return configParseMsgPrices(prices.beginParse());
};

export function computeDefaultForwardFee(msgPrices: MsgPrices) {
    return msgPrices.lumpPrice - ((msgPrices.lumpPrice * msgPrices.firstFrac) >> 16n);
}

export function computeFwdFees(msgPrices: MsgPrices, cells: bigint, bits: bigint) {
    return msgPrices.lumpPrice + shr16ceil(msgPrices.bitPrice * bits + msgPrices.cellPrice * cells);
}

export function computeFwdFeesVerbose(msgPrices: MsgPrices, cells: bigint | number, bits: bigint | number) {
    const fees = computeFwdFees(msgPrices, BigInt(cells), BigInt(bits));
    const res = (fees * msgPrices.firstFrac) >> 16n;
    return { total: fees, res, remaining: fees - res };
}

export function computeCellForwardFees(msgPrices: MsgPrices, msg: Cell) {
    const storageStats = collectCellStats(msg, [], true);
    return computeFwdFees(msgPrices, storageStats.cells, storageStats.bits);
}

export function computeMessageForwardFees(msgPrices: MsgPrices, msg: Message) {
    if (msg.info.type !== 'internal') throw new Error('Helper intended for internal messages');
    let storageStats = new StorageStats();

    const defaultFwd = computeDefaultForwardFee(msgPrices);
    if (msg.info.forwardFee === defaultFwd) {
        return {
            fees: msgPrices.lumpPrice,
            res: defaultFwd,
            remaining: defaultFwd,
            stats: storageStats
        };
    }

    const visited: Array<string> = [];

    if (msg.init) {
        let addBits = 5n;
        let refCount = 0;
        if (msg.init.splitDepth) addBits += 5n;
        if (msg.init.libraries) {
            refCount++;
            storageStats = storageStats.add(
                collectCellStats(beginCell().storeDictDirect(msg.init.libraries).endCell(), visited, true)
            );
        }
        if (msg.init.code) {
            refCount++;
            storageStats = storageStats.add(collectCellStats(msg.init.code, visited));
        }
        if (msg.init.data) {
            refCount++;
            storageStats = storageStats.add(collectCellStats(msg.init.data, visited));
        }
        if (refCount >= 2) {
            storageStats = storageStats.addCells(1).addBits(addBits);
        }
    }

    const lumpBits = BigInt(msg.body.bits.length);
    const bodyStats = collectCellStats(msg.body, visited, true);
    storageStats = storageStats.add(bodyStats);

    let feesVerbose = computeFwdFeesVerbose(msgPrices, storageStats.cells, storageStats.bits);
    if (feesVerbose.remaining < msg.info.forwardFee) {
        storageStats = storageStats.addCells(1).addBits(lumpBits);
        feesVerbose = computeFwdFeesVerbose(msgPrices, storageStats.cells, storageStats.bits);
    }
    if (feesVerbose.remaining !== msg.info.forwardFee) {
        throw new Error('Forward fee calculation mismatch');
    }
    return { fees: feesVerbose, stats: storageStats };
}
I