From 1a549abc898347e5b3ce3c83eab9939ac1c82084 Mon Sep 17 00:00:00 2001 From: Antonoff <35700168+memearchivarius@users.noreply.github.com> Date: Tue, 23 Sep 2025 19:47:48 +0300 Subject: [PATCH 01/17] Early draft --- snippets/feePlayground.jsx | 177 +++++++++++++++++++ ton/phases-and-fees.mdx | 343 +++++++++++++++++++++++++++++++++++++ 2 files changed, 520 insertions(+) create mode 100644 snippets/feePlayground.jsx create mode 100644 ton/phases-and-fees.mdx diff --git a/snippets/feePlayground.jsx b/snippets/feePlayground.jsx new file mode 100644 index 00000000..c0c420c8 --- /dev/null +++ b/snippets/feePlayground.jsx @@ -0,0 +1,177 @@ +export const FeePlayground = () => { + const nano = 10 ** -9; + const bit16 = 2 ** 16; + const Note = ({ title, children }) => ( +
+ {title && ( +
+ {title} +
+ )} +
+ {children} +
+
+ ); + + const compute = (form) => { + const presets = { + // #25 - Messages prices (workchain) + workchain: { + lump_price: 400000, + bit_price: 26214400, + cell_price: 2621440000, + ihr_price_factor: 98304, + first_frac: 21845, + next_frac: 21845, + }, + // #24 - Masterchain messages prices + masterchain: { + lump_price: 10000000, + bit_price: 655360000, + cell_price: 65536000000, + ihr_price_factor: 98304, + first_frac: 21845, + next_frac: 21845, + }, + }; + const storagePrices = { + workchain: { bit_ps: 1, cell_ps: 500 }, + masterchain: { bit_ps: 1000, cell_ps: 500000 }, + }; + + const net = form.network.value === 'masterchain' ? 'masterchain' : 'workchain'; + const { lump_price: lumpPrice, bit_price: bitPrice, cell_price: cellPrice, ihr_price_factor: ihrPriceFactor, first_frac: firstFrac } = presets[net]; + const ihrDisabled = form.ihr_disabled.checked ? 1 : 0; + + const importBits = Number(form.import_bits.value || 0); + const importCells = Number(form.import_cells.value || 0); + const fwdBits = Number(form.fwd_bits.value || 0); + const fwdCells = Number(form.fwd_cells.value || 0); + + const gasFeesTon = Number(form.gas_fees_ton.value || 0); + + // Account storage parameters + const accountBits = Number(form.account_bits.value || 0); + const accountCells = Number(form.account_cells.value || 0); + const { bit_ps, cell_ps } = storagePrices[net]; + + const timeDelta = Number(form.time_delta.value || 69); + + // Compute storage fee from account params (nanotons) + const storageFeeNano = Math.ceil(((accountBits * bit_ps + accountCells * cell_ps) * timeDelta) / bit16); + const storageFeesTon = storageFeeNano * nano; + // storage fee is displayed in the results area only + + const fwdFee = lumpPrice + Math.ceil((bitPrice * fwdBits + cellPrice * fwdCells) / bit16); + const ihrFee = ihrDisabled ? 0 : Math.ceil((fwdFee * ihrPriceFactor) / bit16); + const totalFwdFees = fwdFee + ihrFee; + const totalActionFees = +((fwdFee * firstFrac) / bit16).toFixed(9); + const importFee = lumpPrice + Math.ceil((bitPrice * importBits + cellPrice * importCells) / bit16); + const totalFeeTon = gasFeesTon + storageFeesTon + importFee * nano + totalFwdFees * nano; + + const setOut = (key, value) => { + const el = form.querySelector(`[data-out="${key}"]`); + if (el) el.textContent = value; + }; + + setOut('total', totalFeeTon.toFixed(9)); + setOut('action', (totalActionFees * nano).toFixed(9)); + setOut('fwd', (totalFwdFees * nano).toFixed(9)); + setOut('import', (importFee * nano).toFixed(9)); + setOut('ihr', (ihrFee * nano).toFixed(9)); + setOut('gas', gasFeesTon.toFixed(9)); + setOut('storage', storageFeesTon.toFixed(9)); + }; + + const init = (node) => { + if (node) compute(node); + }; + + return ( +
compute(e.currentTarget)} className="not-prose my-4 p-4 border rounded-xl dark:border-white/20 border-black/10"> +
+
Network
+
+ +
+
+ +
+
+

Import Payload

+ + +
+ +
+

Forward Payload

+ + + + +
+
+ +
+

Account storage

+
+ + + +
+ + You can find import, forward and storage parameters in the Executor logs (txtracer/retracer) for a specific transaction. + +
+ +
+

Compute fee

+ + The compute (gas) cost cannot be predicted by a static formula.
+ Measure it in tests or read it from the executor logs / explorer, then enter the gas fee here. +
+
+ +
+
+ +
+
+
Fwd. fee: TON
+
Gas fee: TON
+
Storage fee: TON
+
Action fee: TON
+
+
+
Import fee: TON
+
IHR fee: TON
+
Total fee: TON
+
+
+
+ ); +}; + diff --git a/ton/phases-and-fees.mdx b/ton/phases-and-fees.mdx new file mode 100644 index 00000000..368431d8 --- /dev/null +++ b/ton/phases-and-fees.mdx @@ -0,0 +1,343 @@ +--- +title: "Execution phases and fees" +--- + +import { FeePlayground } from '/snippets/feePlayground.jsx'; + +# Transactions and phases + +When an event occurs on an account in the TON blockchain, it triggers a **transaction**. +The most common event is receiving a message, but other events like `tick-tock`, `merge`, and `split` can also initiate transactions. + +Each transaction consists of up to five phases: + +1. **Storage phase**: calculates storage fees for the contract based on the space it occupies in the blockchain state. +2. **Credit phase**: updates the contract balance by accounting for incoming message values and storage fees. +3. **Compute phase**: executes the contract code on TVM. The result includes `exit_code`, `actions`, `gas_details`, `new_storage`, and other data. +4. **Action phase**: processes actions from the compute phase if it succeeds. +Actions may include sending messages, updating contract code, or modifying libraries. If an action fails (for example, due to a lack of funds), the transaction may revert or skip the action, depending on its mode. For example, `mode = 0, flag = 2` means that any errors arising while processing this message during the action phase are ignored. +5. **Bounce phase**: if the compute or action phase ends with an error and the inbound message has the bounce flag set, this phase generates a bounce message. + +## Compute phase + +The compute phase involves executing the contract code on TVM. + +> See section 4.3.5 of the [TON whitepaper](/resources/pdfs/tblkch.pdf). It may include outdated information. + +### When the compute phase is skipped + +The compute phase may be skipped under certain conditions, such as when the account is missing, uninitialized, or frozen, or when the incoming message lacks code or data fields. These scenarios are represented by specific constructors: + +- `cskip_no_state$00`: the account or message lacks a valid state, +for example, [missing code or data](https://testnet.tonviewer.com/transaction/7e78394d082882375a5d21affa6397dec60fc5a3ecbea87f401b0e460fb5c80c). +- `cskip_bad_state$01`: the message contains an invalid state, +for example, incorrect state for a frozen or uninitialized account. +- `cskip_no_gas$10`: the account lacks enough funds to cover gas costs. + +# Fee calculation + +When your contract begins processing an incoming message, you should verify the amount of Toncoin attached to the message to ensure it is sufficient to cover all types of fees. To achieve this, you need to calculate (or predict) the fee for the current transaction. + +This document explains how to calculate fees in FunC contracts using the latest TVM opcodes. + +> For a comprehensive list of TVM opcodes, refer to [TVM instructions](/tvm/instructions). + +## Basic fees formula + +Fees on TON are calculated using this formula: + +```cpp +transaction_fee = storage_fees + + in_fwd_fees // also called import fee + + computation_fees + + action_fees + + out_fwd_fees +``` + +```jsx expandable +// Check https://txtracer.ton.org/?tx=b5e14a9c4a4e982fda42d6079c3f84fa48e76497a8f3fca872f9a3737f1f6262 + +function FeeCalculator() { + // Config param 25 (fees): https://tonviewer.com/config#25 — names mirror config fields + const lump_price = 400000; + const bit_price = 26214400; + const cell_price = 2621440000; + const ihr_price_factor = 98304; + const first_frac = 21845; + const nano = 10 ** -9; + const bit16 = 2 ** 16; + + const ihr_disabled = 0; // First, define whether IHR will be counted + + let fwd_fee = + lump_price + Math.ceil((bit_price * 0 + cell_price * 0) / bit16); + + let ihr_fee; + if (ihr_disabled) { + ihr_fee = 0; + } else { + ihr_fee = Math.ceil((fwd_fee * ihr_price_factor) / bit16); + } + + let total_fwd_fees = fwd_fee + ihr_fee; + let gas_fees = 0.0011976; // Gas fees are out of scope here + let storage_fees = 0.000000003; // Storage fees as well + let total_action_fees = +((fwd_fee * first_frac) / bit16).toFixed(9); + let import_fee = + lump_price + Math.ceil((bit_price * 528 + cell_price * 1) / bit16); + let total_fee = + gas_fees + + storage_fees + + // total_action_fees * nano + <- already included in total_fwd_fees + import_fee * nano + + total_fwd_fees * nano; // <- not included in explorer's total fee + + return ( +
+

Total fee: {+total_fee.toFixed(9)} TON

+

Action fee: {+(total_action_fees * nano).toFixed(9)} TON

+

Fwd fee: {+(total_fwd_fees * nano).toFixed(9)} TON

+

Import fee: {+(import_fee * nano).toFixed(9)} TON

+

IHR fee: {+(ihr_fee * nano).toFixed(9)} TON

+
+ ); +} +``` + +### Fee calculator + + + + +## Elements of the transaction fee + +- `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, e.g., `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](/resources/pdfs/tblkch.pdf) 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 and routing via HR/IHR. + +## Storage fee + +### Overview + +In short, storage fees are the costs of storing a smart contract on the blockchain. You pay for every second the smart contract remains stored on the blockchain. + +Use the `GETSTORAGEFEE` opcode with the following parameters: + +| Param name | Description | +| :--------- | :------------------------------------------------------ | +| cells | Number of contract cells | +| bits | Number of contract bits | +| seconds | Duration in seconds for the estimate | +| is_mc | True if execution is in the masterchain; false if basechain | + +> The 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](/tvm/serialization/library). + +### Calculation flow + +Each contract has its own balance. You can calculate how many TON your contract requires to remain valid for a specified `seconds` duration using the function: + +```func wrap +int get_storage_fee(int is_mc, int seconds, int bits, int cells) asm(cells bits seconds is_mc) "GETSTORAGEFEE"; +``` + +You can then hardcode this value into the contract and calculate the current storage fee using: + +```func wrap +;; functions from the FunC stdlib +() raw_reserve(int amount, int mode) impure asm "RAWRESERVE"; +int get_storage_fee(int is_mc, int seconds, int bits, int cells) asm(cells bits seconds is_mc) "GETSTORAGEFEE"; +int my_storage_due() asm "DUEPAYMENT"; + +;; constants from stdlib +;;; Creates an output action which reserves exactly x nanotons (if y = 0). +const int RESERVE_REGULAR = 0; +;;; Creates an output action which reserves at most x nanotons (if y = 2). +;;; Bit +2 in y ensures the external action does not fail if the specified amount cannot be reserved. Instead, it reserves all remaining balance. +const int RESERVE_AT_MOST = 2; +;;; In the case of action failure, the transaction is bounced. No effect if RESERVE_AT_MOST (+2) is used. TVM UPGRADE 2023-07. +const int RESERVE_BOUNCE_ON_ACTION_FAIL = 16; + +() calculate_and_reserve_at_most_storage_fee(int balance, int msg_value, int workchain, int seconds, int bits, int cells) inline { + int on_balance_before_msg = my_ton_balance - msg_value; + int min_storage_fee = get_storage_fee(workchain, seconds, bits, cells); ;; You can hardcode this value if the contract code will not be updated. + raw_reserve(max(on_balance_before_msg, min_storage_fee + my_storage_due()), RESERVE_AT_MOST); +} +``` + +If `min_storage_fee` is hardcoded, **remember to update it** during the contract update process. Not all contracts support updates, so this is an optional requirement. + +## Computation fee + +### Overview + +In most cases, use the `GETGASFEE` opcode with the following parameters: + +| Param | Description | +| :--------- | :------------------------------------------------------------- | +| `gas_used` | Gas amount, calculated in tests and hardcoded | +| `is_mc` | True if execution is in the masterchain; false if basechain | + +### Calculation flow + +```func wrap +int get_compute_fee(int is_mc, int gas_used) asm(gas_used is_mc) "GETGASFEE"; +``` + +But how do you determine `gas_used`? Through testing! + +To calculate `gas_used`, you should write a test for your contract that: + +1. Executes a transfer. +2. Verifies its success and retrieves the transfer details. +3. Checks the amount of gas the transfer uses for computation. + +The contract's computation flow can depend on input data. You should run the contract in a way that maximizes gas usage. Ensure you are using the most computationally expensive path to test the contract. + +```ts expandable +// Initialization code +const deployerJettonWallet = await userWallet(deployer.address); +let initialJettonBalance = await deployerJettonWallet.getJettonBalance(); +const notDeployerJettonWallet = await userWallet(notDeployer.address); +let initialJettonBalance2 = await notDeployerJettonWallet.getJettonBalance(); +let sentAmount = toNano("0.5"); +let forwardAmount = toNano("0.05"); +let forwardPayload = beginCell().storeUint(0x1234567890abcdefn, 128).endCell(); +// Ensure the payload is unique to charge cell loading for each payload. +let customPayload = beginCell().storeUint(0xfedcba0987654321n, 128).endCell(); + +// Let's use this case for fee calculation +// Embed the forward payload into the custom payload to ensure maximum gas usage during computation +const sendResult = await deployerJettonWallet.sendTransfer( + deployer.getSender(), + toNano("0.17"), // TON + sentAmount, + notDeployer.address, + deployer.address, + customPayload, + forwardAmount, + forwardPayload +); +expect(sendResult.transactions).toHaveTransaction({ + // excesses + from: notDeployerJettonWallet.address, + to: deployer.address, +}); +/* +transfer_notification#7362d09c query_id:uint64 amount:(VarUInteger 16) + sender:MsgAddress forward_payload:(Either Cell ^Cell) + = InternalMsgBody; +*/ +expect(sendResult.transactions).toHaveTransaction({ + // notification + from: notDeployerJettonWallet.address, + to: notDeployer.address, + value: forwardAmount, + body: beginCell() + .storeUint(Op.transfer_notification, 32) + .storeUint(0, 64) // default queryId + .storeCoins(sentAmount) + .storeAddress(deployer.address) + .storeUint(1, 1) + .storeRef(forwardPayload) + .endCell(), +}); +const transferTx = findTransactionRequired(sendResult.transactions, { + on: deployerJettonWallet.address, + from: deployer.address, + op: Op.transfer, + success: true, +}); + +let computedGeneric: (transaction: Transaction) => TransactionComputeVm; +computedGeneric = (transaction) => { + if (transaction.description.type !== "generic") + throw "Expected generic transaction"; + if (transaction.description.computePhase.type !== "vm") + throw "Compute phase expected"; + return transaction.description.computePhase; +}; + +let printTxGasStats: (name: string, trans: Transaction) => bigint; +printTxGasStats = (name, transaction) => { + const txComputed = computedGeneric(transaction); + console.log(`${name} used ${txComputed.gasUsed} gas`); + console.log(`${name} gas cost: ${txComputed.gasFees}`); + return txComputed.gasFees; +}; + +const send_gas_fee = printTxGasStats("Jetton transfer", transferTx); +``` + +## Forward fee + +### Overview + +The forward fee is charged for outgoing messages. + +Generally, there are three scenarios for forward fee processing: + +1. The message structure is deterministic, and you can predict the fee. +2. The message structure depends heavily on the incoming message structure. +3. You cannot predict the outgoing message structure at all. + +### Calculation flow + +1. If the message structure is deterministic, use the `GETFORWARDFEE` opcode with the following parameters: + +| Param name | Description | +| :--------- | :------------------------------------------------------ | +| cells | Number of cells | +| bits | Number of bits | +| is_mc | True if execution is in the masterchain; false if basechain | + + +> The 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](/tvm/serialization/library). + +2. However, if the outgoing message depends significantly on the incoming structure, you may not be able to fully predict the fee. In such cases, try using the `GETORIGINALFWDFEE` opcode with the following parameters: + +| Param name | Description | +| :--------- | :------------------------------------------------------ | +| fwd_fee | Parsed from the incoming message | +| is_mc | True if execution is in the masterchain; false if basechain | + +3. The `SENDMSG` opcode is the least optimal way to calculate fees, but it is better than not checking. + +> Be careful: `SENDMSG` uses an **unpredictable amount of gas**. Prefer the methods above whenever possible. + +| Param name | Description | +| :--------- | :---------------------------- | +| msg | Serialized message cell | +| mode | Message mode | + +Important modes when estimating with `SENDMSG`: + +- **`+1024`**: Returns the estimated fee without creating an output action. Other modes will send a message during the action phase. +- **`+128`**: Substitutes the value of the entire contract balance before the computation phase begins. This is slightly inaccurate because gas expenses, which cannot be estimated before the computation phase, are excluded. +- **`+64`**: Substitutes the entire balance of the incoming message as the outgoing value. This is also slightly inaccurate, as gas expenses that cannot be estimated until the computation is completed are excluded. +- Refer to the [message modes cookbook](/ton/message-modes) for additional modes. + +With modes other than `+1024`, it creates an output action and returns the fee for creating a message. With `+1024`, it only returns the estimated fee and does not create an output action. In all cases, it uses an unpredictable amount of gas, which cannot be calculated using formulas. To measure gas usage, use `GASCONSUMED`: + +```func +int send_message(cell msg, int mode) impure asm "SENDMSG"; +int gas_consumed() asm "GASCONSUMED"; +;; ... some code ... + +() calculate_forward_fee(cell msg, int mode) inline { + int gas_before = gas_consumed(); + int forward_fee = send_message(msg, mode); + int gas_usage = gas_consumed() - gas_before; + + ;; forward fee -- fee value + ;; gas_usage -- the amount of gas used to send the message +} +``` + +## See also + +- [Stablecoin contract with fees calculation](https://github.com/ton-blockchain/stablecoin-contract) \ No newline at end of file From 96772127e3498481f436ca7598b444dd43644834 Mon Sep 17 00:00:00 2001 From: Antonoff <35700168+memearchivarius@users.noreply.github.com> Date: Tue, 23 Sep 2025 22:17:56 +0300 Subject: [PATCH 02/17] Update phases-and-fees.mdx --- ton/phases-and-fees.mdx | 76 +++++++++++++++++------------------------ 1 file changed, 31 insertions(+), 45 deletions(-) diff --git a/ton/phases-and-fees.mdx b/ton/phases-and-fees.mdx index 368431d8..adaee0c7 100644 --- a/ton/phases-and-fees.mdx +++ b/ton/phases-and-fees.mdx @@ -3,17 +3,18 @@ title: "Execution phases and fees" --- import { FeePlayground } from '/snippets/feePlayground.jsx'; +import { Aside } from '/snippets/aside.jsx'; # Transactions and phases -When an event occurs on an account in the TON blockchain, it triggers a **transaction**. +When an event occurs on an account in The Open Network (TON) blockchain, it triggers a **transaction**. The most common event is receiving a message, but other events like `tick-tock`, `merge`, and `split` can also initiate transactions. Each transaction consists of up to five phases: 1. **Storage phase**: calculates storage fees for the contract based on the space it occupies in the blockchain state. 2. **Credit phase**: updates the contract balance by accounting for incoming message values and storage fees. -3. **Compute phase**: executes the contract code on TVM. The result includes `exit_code`, `actions`, `gas_details`, `new_storage`, and other data. +3. **Compute phase**: executes the contract code on the TON Virtual Machine (TVM). The result includes `exit_code`, `actions`, `gas_details`, `new_storage`, and other data. 4. **Action phase**: processes actions from the compute phase if it succeeds. Actions may include sending messages, updating contract code, or modifying libraries. If an action fails (for example, due to a lack of funds), the transaction may revert or skip the action, depending on its mode. For example, `mode = 0, flag = 2` means that any errors arising while processing this message during the action phase are ignored. 5. **Bounce phase**: if the compute or action phase ends with an error and the inbound message has the bounce flag set, this phase generates a bounce message. @@ -22,7 +23,9 @@ Actions may include sending messages, updating contract code, or modifying libra The compute phase involves executing the contract code on TVM. -> See section 4.3.5 of the [TON whitepaper](/resources/pdfs/tblkch.pdf). It may include outdated information. + ### When the compute phase is skipped @@ -36,16 +39,17 @@ for example, incorrect state for a frozen or uninitialized account. # Fee calculation -When your contract begins processing an incoming message, you should verify the amount of Toncoin attached to the message to ensure it is sufficient to cover all types of fees. To achieve this, you need to calculate (or predict) the fee for the current transaction. +Verify the attached Toncoin covers storage, import, compute, action, and forwarding fees. Then calculate the transaction fee. -This document explains how to calculate fees in FunC contracts using the latest TVM opcodes. - -> For a comprehensive list of TVM opcodes, refer to [TVM instructions](/tvm/instructions). + ## Basic fees formula Fees on TON are calculated using this formula: +Not runnable ```cpp transaction_fee = storage_fees + in_fwd_fees // also called import fee @@ -54,6 +58,7 @@ transaction_fee = storage_fees + out_fwd_fees ``` +Example of a fee calculator logic: ```jsx expandable // Check https://txtracer.ton.org/?tx=b5e14a9c4a4e982fda42d6079c3f84fa48e76497a8f3fca872f9a3737f1f6262 @@ -113,12 +118,12 @@ function FeeCalculator() { - `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, e.g., `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](/resources/pdfs/tblkch.pdf) to learn more about inbound messages. +- `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](/resources/pdfs/tblkch.pdf) 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 and routing via HR/IHR. +- `out_fwd_fees` is a charge for forwarding outgoing internal messages within TON between shardchains; it depends on message size and routing via Hypercube Routing (HR) or Instant Hypercube Routing (IHR). ## Storage fee @@ -126,16 +131,11 @@ function FeeCalculator() { In short, storage fees are the costs of storing a smart contract on the blockchain. You pay for every second the smart contract remains stored on the blockchain. -Use the `GETSTORAGEFEE` opcode with the following parameters: - -| Param name | Description | -| :--------- | :------------------------------------------------------ | -| cells | Number of contract cells | -| bits | Number of contract bits | -| seconds | Duration in seconds for the estimate | -| is_mc | True if execution is in the masterchain; false if basechain | +Use the [`GETSTORAGEFEE`](/tvm/instructions) opcode. -> The 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](/tvm/serialization/library). + ### Calculation flow @@ -175,12 +175,7 @@ If `min_storage_fee` is hardcoded, **remember to update it** during the contract ### Overview -In most cases, use the `GETGASFEE` opcode with the following parameters: - -| Param | Description | -| :--------- | :------------------------------------------------------------- | -| `gas_used` | Gas amount, calculated in tests and hardcoded | -| `is_mc` | True if execution is in the masterchain; false if basechain | +In most cases, use the [`GETGASFEE`](/tvm/instructions) opcode. ### Calculation flow @@ -277,7 +272,7 @@ const send_gas_fee = printTxGasStats("Jetton transfer", transferTx); ### Overview -The forward fee is charged for outgoing messages. +TON charges a forward fee for outgoing messages. Generally, there are three scenarios for forward fee processing: @@ -287,32 +282,23 @@ Generally, there are three scenarios for forward fee processing: ### Calculation flow -1. If the message structure is deterministic, use the `GETFORWARDFEE` opcode with the following parameters: - -| Param name | Description | -| :--------- | :------------------------------------------------------ | -| cells | Number of cells | -| bits | Number of bits | -| is_mc | True if execution is in the masterchain; false if basechain | - +1. If the message structure is deterministic, use the [`GETFORWARDFEE`](/tvm/instructions) opcode. -> The 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](/tvm/serialization/library). -2. However, if the outgoing message depends significantly on the incoming structure, you may not be able to fully predict the fee. In such cases, try using the `GETORIGINALFWDFEE` opcode with the following parameters: + -| Param name | Description | -| :--------- | :------------------------------------------------------ | -| fwd_fee | Parsed from the incoming message | -| is_mc | True if execution is in the masterchain; false if basechain | +2. However, if the outgoing message depends significantly on the incoming structure, you may not be able to fully predict the fee. In such cases, try using the [`GETORIGINALFWDFEE`](/tvm/instructions) opcode. -3. The `SENDMSG` opcode is the least optimal way to calculate fees, but it is better than not checking. +3. The [`SENDMSG`](/tvm/instructions) opcode is the least optimal way to calculate fees, but it is better than not checking. -> Be careful: `SENDMSG` uses an **unpredictable amount of gas**. Prefer the methods above whenever possible. + -| Param name | Description | -| :--------- | :---------------------------- | -| msg | Serialized message cell | -| mode | Message mode | +Use it with a serialized message cell and a message mode. Important modes when estimating with `SENDMSG`: From e2f9b46c91cb41b9723dc31638563add5d86ebd9 Mon Sep 17 00:00:00 2001 From: Antonoff <35700168+memearchivarius@users.noreply.github.com> Date: Thu, 25 Sep 2025 22:53:24 +0300 Subject: [PATCH 03/17] Update phases-and-fees.mdx --- ton/phases-and-fees.mdx | 111 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 109 insertions(+), 2 deletions(-) diff --git a/ton/phases-and-fees.mdx b/ton/phases-and-fees.mdx index adaee0c7..86d2c603 100644 --- a/ton/phases-and-fees.mdx +++ b/ton/phases-and-fees.mdx @@ -19,6 +19,15 @@ Each transaction consists of up to five phases: Actions may include sending messages, updating contract code, or modifying libraries. If an action fails (for example, due to a lack of funds), the transaction may revert or skip the action, depending on its mode. For example, `mode = 0, flag = 2` means that any errors arising while processing this message during the action phase are ignored. 5. **Bounce phase**: if the compute or action phase ends with an error and the inbound message has the bounce flag set, this phase generates a bounce message. +## Gas + +All computation is measured in gas units; each TVM operation has a fixed gas cost. The gas price is defined by network configuration and is not set by users (there is no fee market). + +- 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 (for example, `https://tonviewer.com/config#20`, `https://tonviewer.com/config#21`). The values can change through validator governance. + ## Compute phase The compute phase involves executing the contract code on TVM. @@ -137,6 +146,17 @@ Use the [`GETSTORAGEFEE`](/tvm/instructions) opcode. The 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](/tvm/serialization/library). +### Formula (low-level) + +Approximate storage fees for smart contracts (values are defined in network config param 18): + +```cpp +storage_fee = ceil( + (account.bits * bit_price + + account.cells * cell_price) + * time_delta / 2^16) +``` + ### Calculation flow Each contract has its own balance. You can calculate how many TON your contract requires to remain valid for a specified `seconds` duration using the function: @@ -268,6 +288,30 @@ printTxGasStats = (name, transaction) => { const send_gas_fee = printTxGasStats("Jetton transfer", transferTx); ``` +## Accept message effects + +The `accept_message` and `set_gas_limit` TVM primitives control gas limits and indicate readiness to pay fees. + +### External messages + +- Initial gas limit is a small credit (`gas_credit` = 10,000 gas units from config `20/21`). +- During Compute, call `accept_message` to raise the gas limit to the maximum allowed (`gm`), or use `set_gas_limit(limit)` to set it to `min(limit, gm)`. +- If `gas_credit` is exhausted or you never call `accept_message`, the external message is not included in a block and no transaction is created. +- After processing, full computation fees are deducted from the contract balance (the credit is not free gas). + +If an error occurs after `accept_message`: +- A transaction is recorded and fees are paid from the contract balance. +- State changes are reverted unless you committed state explicitly. +- Actions are not applied. + +Security note: accepting an invalid external message and then throwing makes the contract pay and can allow replay until funds are depleted. Validate before accepting. + +### Internal messages + +- Default gas limit equals `message_value / gas_price`; the incoming value covers its processing. +- You may still adjust limits with `accept_message`/`set_gas_limit`. +- Bounce semantics are unaffected: a bounceable message will bounce if there is enough value to pay processing and bounce creation. + ## Forward fee ### Overview @@ -280,10 +324,17 @@ Generally, there are three scenarios for forward fee processing: 2. The message structure depends heavily on the incoming message structure. 3. You cannot predict the outgoing message structure at all. -### Calculation flow + + +### Calculation in smart contracts +1. If the message structure is deterministic, use the [`GETFORWARDFEE`](/tvm/instructions) opcode. -### Formula (low-level) +### Low-level formula Approximate storage fees for smart contracts (values are defined in network config param 18): -```cpp +```cpp title="FORMULAS" storage_fee = ceil( (account.bits * bit_price + account.cells * cell_price) @@ -375,11 +374,11 @@ int gas_consumed() asm "GASCONSUMED"; } ``` -### Formula (low-level) +### Low-level formula Forwarding fees for a message of size `msg.bits` and `msg.cells` are computed using config params 24/25: -```cpp +```cpp title="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( @@ -399,7 +398,7 @@ total_fwd_fees = msg_fwd_fees + ihr_fwd_fees; // ihr_fwd_fees is 0 for external The action fee is charged when processing the action list (after Compute). Practically, you pay it for `SENDRAWMSG`; other actions such as `RAWRESERVE` or `SETCODE` do not incur action fees. -```cpp +```cpp title="FORMULAS" action_fee = floor((msg_fwd_fees * first_frac)/ 2^16); // internal action_fee = msg_fwd_fees; // external @@ -409,7 +408,7 @@ action_fee = msg_fwd_fees; // external Action fine (failed send): starting from Global Version 4, if a "send message" action fails, the account pays a fine proportional to the attempted message size: -```cpp +```cpp title="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); From 9321ea7c1c2175d48e5a3dd2e31a4ae160e92767 Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Sat, 4 Oct 2025 01:38:14 +0200 Subject: [PATCH 05/17] Apply suggestions from code review (batch 2) --- ton/phases-and-fees.mdx | 99 +++++++++++++++++++---------------------- 1 file changed, 47 insertions(+), 52 deletions(-) diff --git a/ton/phases-and-fees.mdx b/ton/phases-and-fees.mdx index 6ad128cc..b151f996 100644 --- a/ton/phases-and-fees.mdx +++ b/ton/phases-and-fees.mdx @@ -66,61 +66,56 @@ transaction_fee = storage_fees + out_fwd_fees ``` -Example of a fee calculator logic: -```jsx expandable -// Check https://txtracer.ton.org/?tx=b5e14a9c4a4e982fda42d6079c3f84fa48e76497a8f3fca872f9a3737f1f6262 - -function FeeCalculator() { - // Config param 25 (fees): https://tonviewer.com/config#25 — names mirror config fields - const lump_price = 400000; - const bit_price = 26214400; - const cell_price = 2621440000; - const ihr_price_factor = 98304; - const first_frac = 21845; - const nano = 10 ** -9; - const bit16 = 2 ** 16; - - const ihr_disabled = 0; // First, define whether IHR will be counted - - let fwd_fee = - lump_price + Math.ceil((bit_price * 0 + cell_price * 0) / bit16); - - let ihr_fee; - if (ihr_disabled) { - ihr_fee = 0; - } else { - ihr_fee = Math.ceil((fwd_fee * ihr_price_factor) / bit16); - } - - let total_fwd_fees = fwd_fee + ihr_fee; - let gas_fees = 0.0011976; // Gas fees are out of scope here - let storage_fees = 0.000000003; // Storage fees as well - let total_action_fees = +((fwd_fee * first_frac) / bit16).toFixed(9); - let import_fee = - lump_price + Math.ceil((bit_price * 528 + cell_price * 1) / bit16); - let total_fee = - gas_fees + - storage_fees + - // total_action_fees * nano + <- already included in total_fwd_fees - import_fee * nano + - total_fwd_fees * nano; // <- not included in explorer's total fee - - return ( -
-

Total fee: {+total_fee.toFixed(9)} TON

-

Action fee: {+(total_action_fees * nano).toFixed(9)} TON

-

Fwd fee: {+(total_fwd_fees * nano).toFixed(9)} TON

-

Import fee: {+(import_fee * nano).toFixed(9)} TON

-

IHR fee: {+(ihr_fee * nano).toFixed(9)} TON

-
- ); -} -``` - -### Fee calculator +## Fee calculator +The calculator above works as follows under the hood: + +```jsx title="Fee calculation example" expandable +// Variable names below mirror fields of config param 25 (fees). +// See: https://tonviewer.com/config#25 +const lump_price = 400000; +const bit_price = 26214400; +const cell_price = 2621440000; +const ihr_price_factor = 98304; +const first_frac = 21845; +const nano = 10 ** -9; +const bit16 = 2 ** 16; +const ihr_disabled = 0; // First, define whether IHR will be counted + +// Calculations +let fwd_fee = + lump_price + Math.ceil((bit_price * 0 + cell_price * 0) / bit16); + +let ihr_fee; +if (ihr_disabled) { + ihr_fee = 0; +} else { + ihr_fee = Math.ceil((fwd_fee * ihr_price_factor) / bit16); +} +let total_fwd_fees = fwd_fee + ihr_fee; +let gas_fees = 0.0011976; // Gas fees are out of scope here +let storage_fees = 0.000000003; // Storage fees as well +let total_action_fees = +((fwd_fee * first_frac) / bit16).toFixed(9); +let import_fee = + lump_price + Math.ceil((bit_price * 528 + cell_price * 1) / bit16); +let total_fee = + gas_fees + + storage_fees + + // total_action_fees * nano + <- already included in total_fwd_fees + import_fee * nano + + total_fwd_fees * nano; // <- not included in explorer's total fee + +// Results +console.log("Total fee:", total_fee.toFixed(9), "Toncoin"); +console.log("Action fee:", (total_action_fees * nano).toFixed(9), "Toncoin"); +console.log("Forwarding fee:", (total_fwd_fees * nano).toFixed(9), "Toncoin"); +console.log("Import fee:", (import_fee * nano).toFixed(9), "Toncoin"); +console.log("IHR fee:", (ihr_fee * nano).toFixed(9), "Toncoin"); + +// Example transaction with such fees: https://txtracer.ton.org/?tx=b5e14a9c4a4e982fda42d6079c3f84fa48e76497a8f3fca872f9a3737f1f6262 +``` ## Elements of the transaction fee From c0d8c81b7986e3bcf96ac842c548b83e2bd6ef99 Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Sat, 4 Oct 2025 01:38:46 +0200 Subject: [PATCH 06/17] Apply suggestions from code review (batch 3) --- ton/phases-and-fees.mdx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ton/phases-and-fees.mdx b/ton/phases-and-fees.mdx index b151f996..5931f52c 100644 --- a/ton/phases-and-fees.mdx +++ b/ton/phases-and-fees.mdx @@ -413,11 +413,11 @@ action_fine = fine_per_cell * min(max_cells, cells_in_msg); All fees are denominated in nanotons (often scaled by `2^16` for precision) and come from network configuration: -- storage_fees: param 18 (`https://tonviewer.com/config#18`) -- in_fwd_fees: params 24/25 (`https://tonviewer.com/config#24`, `https://tonviewer.com/config#25`) -- computation_fees: params 20/21 (`https://tonviewer.com/config#20`, `https://tonviewer.com/config#21`) -- action_fees: params 24/25 (`https://tonviewer.com/config#24`, `https://tonviewer.com/config#25`) -- out_fwd_fees: params 24/25 (`https://tonviewer.com/config#24`, `https://tonviewer.com/config#25`) +- `storage_fees`: param [18](https://tonviewer.com/config#18) +- `in_fwd_fees`: params [24](https://tonviewer.com/config#24) and [25](https://tonviewer.com/config#25) +- `computation_fees`: params [20](https://tonviewer.com/config#20) and [21](https://tonviewer.com/config#21) +- `action_fees`: params [24](https://tonviewer.com/config#24) and [25](https://tonviewer.com/config#25) +- `out_fwd_fees`: params [24](https://tonviewer.com/config#24) and [25](https://tonviewer.com/config#25) ### Message bouncing From e9a5a6a1326796384109acc627ae5cfb571e47fa Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Mon, 6 Oct 2025 16:26:59 +0200 Subject: [PATCH 07/17] fmt --- ton/phases-and-fees.mdx | 92 ++++++++++++++++++++++++++--------------- 1 file changed, 58 insertions(+), 34 deletions(-) diff --git a/ton/phases-and-fees.mdx b/ton/phases-and-fees.mdx index 5931f52c..c8b79f96 100644 --- a/ton/phases-and-fees.mdx +++ b/ton/phases-and-fees.mdx @@ -7,17 +7,17 @@ import { Aside } from '/snippets/aside.jsx'; # Transactions and phases -When an event occurs on an account in The Open Network (TON) blockchain, it triggers a **transaction**. +When an event occurs on an account in The Open Network (TON) blockchain, it triggers a **transaction**.\ The most common event is receiving a message, but other events like `tick-tock`, `merge`, and `split` can also initiate transactions. Each transaction consists of up to five phases: 1. **Storage phase**: calculates storage fees for the contract based on the space it occupies in the blockchain state. -2. **Credit phase**: updates the contract balance by accounting for incoming message values and storage fees. -3. **Compute phase**: executes the contract code on the TON Virtual Machine (TVM). The result includes `exit_code`, `actions`, `gas_details`, `new_storage`, and other data. -4. **Action phase**: processes actions from the compute phase if it succeeds. -Actions may include sending messages, updating contract code, or modifying libraries. If an action fails (for example, due to a lack of funds), the transaction may revert or skip the action, depending on its mode. For example, `mode = 0, flag = 2` means that any errors arising while processing this message during the action phase are ignored. -5. **Bounce phase**: if the compute or action phase ends with an error and the inbound message has the bounce flag set, this phase generates a bounce message. +1. **Credit phase**: updates the contract balance by accounting for incoming message values and storage fees. +1. **Compute phase**: executes the contract code on the TON Virtual Machine (TVM). The result includes `exit_code`, `actions`, `gas_details`, `new_storage`, and other data. +1. **Action phase**: processes actions from the compute phase if it succeeds.\ + Actions may include sending messages, updating contract code, or modifying libraries. If an action fails (for example, due to a lack of funds), the transaction may revert or skip the action, depending on its mode. For example, `mode = 0, flag = 2` means that any errors arising while processing this message during the action phase are ignored. +1. **Bounce phase**: if the compute or action phase ends with an error and the inbound message has the bounce flag set, this phase generates a bounce message. ## Gas @@ -32,26 +32,32 @@ See config parameters `20` and `21` for current gas prices (for example, `https: The compute phase involves executing the contract code on TVM. -