Gas best practices
There are several anti-patterns that unnecessarily increase gas usage or are suboptimal in most cases. Below, we discuss various trade-offs when writing gas-efficient and safe Tact smart contracts.
Suggestions are given in no particular order as a simple and quick checklist to see how your contract is doing regarding gas usage. You don’t have to check all the points, but try to follow as many as possible without neglecting the security best practices.
General considerations
Prefer contract parameters to init()
and contract fields
If you require some on-chain deployment logic that runs just once after the contract’s deployment is completed, then use the lazy initialization strategy provided by the init()
function. It uses an extra bit in the contract’s persistent state, runs extra checks upon receiving any message, and disables storage write optimizations of Tact compiler.
However, most contracts do not require lazy initialization and define their initial data only when the contract is first deployed, with everything for it prepared off-chain. Therefore, prefer using contract parameters syntax to define the data of the contract’s persistent storage that way.
contract ContractParams( // Persistent state variables declared via the contract parameters syntax val1: Int as uint64, // `as`-annotations are supported val2: String,) { // For deployments receive() { cashback(sender()) }}
// TypeScript wrappers generated by Tact compilerimport { ContractParams } from '../wrappers/ContractParams';
// Various utility librariesimport { NetworkProvider } from '@ton/blueprint';import { toNano } from '@ton/core';
// Default deployment function / script of the Blueprintexport async function run(provider: NetworkProvider) { const contract = provider.open(await ContractParams.fromInit( 42, // `val1` in the contract "The time has come", // `val2` in the contract )); await playground.send( provider.sender(), { value: toNano('0.05') }, null, // because there's a `null` message body // `receive()` function in the contract ); await provider.waitForDeploy(playground.address);}
Do not deploy contracts with Deployable
trait
The Deployable
trait is now deprecated and should only be used if you require the queryId
, which serves as a unique identifier for tracing transactions across multiple contracts.
Instead of inheriting the Deployable
trait, prefer having a simple receiver for the empty message body and deploying your contracts with it.
contract Friendly { // This is when you DO want to send excesses back receive() { cashback(sender()) } // expects a `null` body}
contract Scrooge { // This is when you don't want to send excesses back receive() {} // expects a `null` body}
Pay attention to “Gas-expensive” badge
Some functions in the Tact documentation are annotated with a special badge, “Gas-expensive”, which marks the functions that use 500 gas units or more. It is placed right under the function name heading and looks like this: Gas-expensive
If you use one of those functions, consider finding cheaper alternatives.
Inline functions that are rarely called
Some kinds of functions allow their code to be inlined by adding an inline
annotation. If the function is used often, this might result in a significant increase in contract code size, but generally, it allows to save gas on redundant function calls.
Furthermore, you might reach for the experimental inline
field in tact.config.json,
which enables the inlining of all functions that can be inlined.
This advice needs benchmarks to decide its usefulness on a case-by-case basis.
Prefer manipulating strings off-chain
Strings on-chain are represented as slices, which are expensive for handling Unicode strings and quite costly even for ASCII ones. Prefer not to manipulate strings on-chain.
Prefer arithmetic to branching operators
On average, branching uses many more instructions than equivalent arithmetic. Common examples of branching are the if...else
statement and the ternary operator ?:
.
Use arithmetic and standard library functions over branching or complex control flow whenever possible.
// If the forwardAmount is non-negative, thismsg.forwardAmount > 0 ? 2 : 1;
// is more expensive than doing this1 + sign(msg.forwardAmount);
Asm functions
Many commonly used TVM instructions expect the same values but in a different order on the stack. Often, the code you write will result in instructions defined by your logic intermittent with stack-manipulation instructions, such as SWAP
or ROT
, which would’ve been unnecessary if the positioning of values on the stack was better planned before.
On the other hand, if you know the layout and boundaries of your data, the generic choice of underlying instructions might be suboptimal in terms of gas usage or code size.
In both cases, you can use assembly functions (or asm
functions for short) to manually describe the logic by writing a series of TVM instructions. If you know what you’re doing, asm
functions can offer you the smallest possible gas usage and the most control over TVM execution.
Read more about them on their dedicated page: Assembly functions.
Receiving messages
Prefer binary receivers to text receivers
Tact automatically handles various message body types, including binary and string (text) ones. Both message bodies start with a 32-bit integer header (or tag) called an opcode, which helps distinguish their following contents.
To prevent conflicts with binary message bodies which are usually sent on the blockchain, the string (text) receivers skip the opcode and instead route based on the hash of the message body contents — an expensive operation that requires more than 500 units of gas.
While text receivers are convenient during development in testing, when preparing your contract for production you should replace all text receivers with binary ones and create relevant message structs even if they’ll be empty and only their opcodes will be used.
message(1) One {}message(2) Two {}
contract Example { // Prefer this receive(_: One) { // ... } receive(_: Two) { // ... }
// Over this receive("one") { // ... } receive("two") { // ... }}
Avoid internal contract functions
The internal functions of a contract (often called contract methods) are similar to global functions, except that they can access the contract’s storage variables and constants.
However, they push the contract’s variables on the stack at the start of their execution and pop them off afterward. This creates lots of unnecessary stack-manipulation instructions and consumes gas.
If your contract method does not access any of its persistent state variables, move it outside the contract and make it a global, module-level function instead.
Use sender()
over context().sender
When you receive an internal message, you can obtain the address of the contract that has sent it. This can be done by calling the sender()
function or by accessing the .sender
field of the Context
struct after calling the context()
function.
If you only need the sender’s address and no additional context on the incoming message that is contained in the Context
struct, then use the sender()
function as it is less gas-consuming.
message(MessageParameters{ to: sender(), value: ton("0.05"),});
Use throwUnless()
over require()
The require()
function is convenient for stating assumptions in code, especially in debug environments. Granted, currently, it generates exit codes greater than , making it a bit expensive compared to alternatives.
If you’re ready for production and are willing to sacrifice some convenience for gas, use throwUnless()
function, keep track of your exit codes by declaring them as constants, and keep exit codes within the inclusive range of . It’s essential to respect the latter range because the exit code values from 0 to 255 are reserved by TVM and the Tact compiler.
const SOMETHING_BAD_1: Int = 700;const SOMETHING_BAD_2: Int = 701; // it is convenient to increment by 1
fun example() { throwUnless(SOMETHING_BAD_1, now() > 1000); throwUnless(SOMETHING_BAD_2, now() > 1000000);}
Sending messages
Prefer message
and cashback
to self.forward
, self.reply
, and self.notify
Every contract in Tact implicitly inherits the BaseTrait
trait, which contains a number of internal functions for any contract. Those internal functions are gas-expensive for the same reasons as stated earlier.
Calls to self.forward()
, self.reply()
and self.notify()
can be replaced with respective calls to the send()
or message()
functions with suitable values.
Moreover, if all you want is to forward the remaining value back to the sender, it is best to use the cashback()
function instead of self.notify()
function.
// Thisself.forward(sender(), null, false, initOf SomeContract());
// could be replaced with thislet initState: initOf SomeContract();send(SendParameters{ to: sender(), mode: SendRemainingValue | SendIgnoreErrors, bounce: false, value: 0, code: initState.code, data: initState.data, body: null,})
// Thisself.reply(null);
// should be replaced with thismessage(MessageParameters{ body: null, to: sender(), mode: SendRemainingValue | SendIgnoreErrors, bounce: true,});
// And thisself.notify(null);
// should be replaced with thiscashback(sender());
Use deploy()
function for on-chain deployments
There are many message-sending functions, and the deploy()
function is optimized for cheaper on-chain deployments compared to the send()
function.
deploy(DeployParameters{ init: initOf SomeContract(), // with initial code and data of SomeContract // and with an empty message body mode: SendIgnoreErrors, // skip the message in case of errors value: ton("1"), // send 1 Toncoin (1_000_000_000 nanoToncoin)});
Use message()
function for non-deployment messages
There are many message-sending functions, and the message()
function is optimized for cheaper non-deployment regular messages compared to the send()
function.
message(MessageParameters{ to: addrOfSomeInitializedContract, value: ton("1"), // sending 1 Toncoin (1_000_000_000 nanoToncoin), // with an empty message body});
Applied examples of best gas practices
Tact has a growing set of contracts benchmarked against their reference FunC implementations. We fine-tune each Tact contract following the gas-preserving approaches discussed on this page while staying true to the original code and without doing precompilation or excessive ASM usage.
See those examples with recommendations applied: