Skip to content

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 compiler
import { ContractParams } from '../wrappers/ContractParams';
// Various utility libraries
import { NetworkProvider } from '@ton/blueprint';
import { toNano } from '@ton/core';
// Default deployment function / script of the Blueprint
export 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, this
msg.forwardAmount > 0 ? 2 : 1;
// is more expensive than doing this
1 + 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 2112^{11}, 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 256211256-2^{11}. 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.

// This
self.forward(sender(), null, false, initOf SomeContract());
// could be replaced with this
let initState: initOf SomeContract();
send(SendParameters{
to: sender(),
mode: SendRemainingValue | SendIgnoreErrors,
bounce: false,
value: 0,
code: initState.code,
data: initState.data,
body: null,
})
// This
self.reply(null);
// should be replaced with this
message(MessageParameters{
body: null,
to: sender(),
mode: SendRemainingValue | SendIgnoreErrors,
bounce: true,
});
// And this
self.notify(null);
// should be replaced with this
cashback(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: