Skip to main content
Contracts that receive the initial external message are referred to as receiver contracts. Other contracts in the same system are called internal contracts. This terminology is local to this article.

Contract system

Consider a contract system with three contracts:
  1. receiver;
  2. internal contract A;
  3. internal contract B.

Transaction trace

In that system, the typical trace looks like this, with transactions moving from left to right.

Value flow

There is no separate message balance and contract balance. After the message is received, coins from the message are stored to the contract balance, and then the contract is executed. Sending message modes and reserve actions help to properly divide contract balance in the action phase. This diagram of a possible value flow illustrates this.

Receiver requirements

Receiver contracts must verify that the attached Toncoin is sufficient to cover fees for all contracts in the subsequent trace. If an entry contract accepts an external message, it must guarantee that the message will not later fail due to insufficient attached Toncoin. “Accept” doesn’t mean the call to accept_message(), but semantic acceptance, i.e., no throw and no asset returns. The reason for this requirement is that reverting the contract system state is usually not possible, because the Toncoin is already spent. When a contract system’s correctness depends on successful execution of the remaining transaction trace, it must guarantee that an incoming message carries enough attached Toncoin to cover all fees. This article describes how to compute those fees.

Define fee limits

  • Define variables for limits and initialize them to zero. Set them to actual values afterwards.
  • Use descriptive names for the operation and the contract. Store them in a dedicated file with constants.
  • Run tests covering all execution paths. Missing a path might hide the most expensive one.
  • Extract resource consumption from the send() method’s return value. The sections below describe ways to compute consumption of different kinds of resources.
  • Use expect(extractedValue).toBeLessThanOrEqual(hardcodedConstant) to verify that the hardcoded limit was not exceeded.
After the first run, use the displayed error message to find the actual value for the constant.

Compute fees

There are two kinds of values: gas units and Toncoin. The price of contract execution is fixed in gas units. However, the price of the gas itself is determined by the blockchain configuration. Convert gas to Toncoin on-chain using blockchain configuration:
This function uses the GETGASFEE TVM opcode.

Forward fees

Forward fee is calculated with this formula:
where
  • lumpPrice is the fixed value from config paid once for the message.
  • msgSizeInCells is the number of cells in the message.
  • msgSizeInBits is the number of bits in all the cells of the message.
In Tolk, cell.calculateSizeStrict() can be used to compute msgSizeInCells and msgSizeInBits. In TVM, it’s implemented as CDATASIZE instruction. In Tolk, the formula above is implemented in calculateForwardFee(). In TVM, it’s implemented as GETFORWARDFEE instruction.
If intermediate (cells, bits) values are not required, msg.send() with mode 1024 can be used instead, allowing TVM to perform the same calculations as calculateForwardFee(). In TVM, this is implemented as SENDRAWMSG and consumes approximately the same amount of gas.

Optimized forward fee calculation

If the size of the incoming message bounds the size of the outgoing message, the forward fee of the outgoing message can be estimated as no larger than the forward fee of the incoming message, which TVM already computes. In this case, the forward fee does not need to be calculated again. This estimation is valid only for contract systems within the same workchain, because gas prices depend on the workchain.
Tolk

Complex forward fee calculation

Assume the contract receives a message with an unknown size and forwards it further adding fields with total of a bits and b cells to the message, e.g., StateInit. For this case, in Tolk, there is a function calculateForwardFeeWithoutLumpPrice(). In TVM, it’s implemented as GETFORWARDFEESIMPLE. This function does not take lumpPrice into account.
Tolk

Storage fees

Storage fees for sending messages cannot be predicted in advance, because they depend on how long the target contract has not paid storage fees. In this respect, storage fees differ from forward and compute fees, as they must be handled in both receiver and internal contracts.

Maintain a positive reserve

Always keep a minimum balance on all contracts in the system. Storage fees get deducted from this reserve. The reserve gets replenished with each external interaction. Do not hardcode Toncoin values for fees. Instead, hardcode the maximum possible contract size in cells and bits. This approach affects code of internal contracts.
In this approach, a receiver contract should calculate maximum possible storage fees for all contracts in trace.

Cover storage on demand

The order of phases depends on the bounce flag of an incoming message. If all messages in the protocol are unbounceable, then the storage phase comes after the credit phase. So, the contract’s storage fees are deducted from the joint balance of the contract and incoming message. In this case the pattern where the contract’s balance is zero and incoming messages cover storage fees can be applied. It is impossible to know in advance what the storage fee due will be on the contract, so a threshold must be selected depending on the network configuration. It is a good practice to use freeze_due_limit as the threshold. Otherwise, the contract likely is already frozen and a transaction chain is likely to fail anyway. This pattern can be generalized to both bounceable and unbounceable messages with contract.getStorageDuePayment(), which returns storage_fees_due. This approach affects code of internal contracts.
If the remaining trace involves n unique contracts, no more than n freeze limits are required to cover their storage fees. Therefore, the receiver contract should perform the following check:
For contracts using this approach, confirm there is no excess accumulation:
This confirms that all incoming value was consumed or forwarded, with none left behind. It helps identify any bugs that cause accumulation of Toncoin on any contract.

Implement fee validation

The final code in the receiver contract could look like this:

Helper functions

Getting gas for the transaction in sandbox is:
To calculate the size of a message in cells, use this function:
To extract a contract’s size in tests, use this function:
Message-size constants should be verified across all possible paths in tests. Otherwise, the resulting gas estimates might be wrong.