Skip to main content
This section summarizes common patterns and conventions used in idiomatic Tolk code. While Basic syntax introduces the language itself, this page outlines the preferred ways of expressing ideas and best practices in Tolk. It may serve as a style reference throughout development.

Auto-serialization instead of slices/builders

Tolk type system is designed to entirely avoid manual cell parsing. The presence of beginCell() indicates a possibly wrong approach. All practical use cases in contract interaction are expressed with structures, unions, and references.
See: automatic serialization.

Cell<T> — a “cell with known shape”

All data in TON is stored in cells that reference each other. To express clear data relation, use typed cellsCell<T>. Literally, it means: a cell whose contents is T:
See: cell references in serialization.

Not “fromCell”, but “lazy fromCell”

In practice, when reading data from cells, prefer lazy:
  • lazy SomeStruct.fromCell(c) over SomeStruct.fromCell(c)
  • lazy typedCell.load() over typedCell.load()
The compiler loads only requested fields, skipping the rest. It reduces gas consumption and bytecode size.
See: lazy loading.

Custom serializers — if can’t express with types

Even though the type system is very rich, there still may occur situations where binary serialization is non-standard. Tolk allows to declare custom types with arbitrary serialization rules.
And just use MyString as a regular type — everywhere:
An interesting example. Imagine a structure which tail is signed:
The task is to parse it and check signature. A manual solution is obvious: read uint256, calculate the hash of the remainder, read other fields. What about the type system? Even this complex scenario can be expressed by introducing a synthetic field that is populated on loading:
See: serialization of type aliases.

Contract storage = struct + load + save

Contract storage is a regular struct, serialized into persistent on-chain data. It is convenient to add load and store methods:
See: contract storage.

Message = struct with a 32-bit prefix

By convention, every message in TON has an opcode — a unique 32-bit number. In Tolk, every struct can have a “serialization prefix” of arbitrary length. 32-bit prefixes are called opcodes. So, every incoming and outgoing message is a struct with a prefix:
See: structures.

Handle a message = structs + union + match

The suggested pattern to handle messages:
  • each incoming message is a struct with an opcode
  • combine these structs into a union
  • parse it via lazy fromSlice and match over variants
Notice lazy: it also works with unions and does “lazy match” by a slice prefix. It’s much more efficient than manual parsing an opcode and branching via if (op == TRANSFER_OP). See: handling messages and pattern matching.

Send a message = struct + createMessage

To send a message from contract A to contract B,
  • declare a struct, specify an opcode and fields expected by a receiver
  • use createMessage + send

Deploy another contract = createMessage

A common case: a minter deploys a jetton wallet, knowing wallet’s code and initial state. This “deployment” is actually sending a message, auto-attaching code+data, and auto-calculating its address:
A preferred way is to extract generating stateInit to a separate function, because it’s used not only to send a message, but also to calculate/validate an address without sending.
See: tolk-bench repo for reference jettons.

Shard optimization = createMessage

“Deploy a contract to a specific shard” is also done with the same technique. For example, in sharded jettons, a jetton wallet must be deployed to the same shard as the owner’s wallet.
Following the guideline above, the task is resolved by adding just a couple lines of code. Sharding will automatically be supported in createMessage and other address calculations.
See: sharding in createMessage.

Emitting events/logs to off-chain

Emitting events and logs “to the outer world” is done via external messages. They are useful for monitoring: being indexed by TON indexers, they show “a picture of on-chain activity”. These are also messages and also cost gas, but are constructed in a slightly different way.
  1. Create a struct to represent the message body
  2. Use createExternalLogMessage + send
See: sending external messages.

Return a struct from a get method

When a contract getter (get fun) needs to return several values — introduce a structure and return it. Do not return unnamed tensors like (int, int, int). Field names provide clear metadata for client wrappers and human readers.
See: contract getters.

Use assertions to validate user input

After parsing an incoming message, validate required fields with assert:
Execution will terminate with some errCode, and a contract will be ready to serve the next request. This is the standard mechanism for reacting on invalid input. See: exceptions.

Organize a project into several files

No matter whether a project contains one contract or multiple — split it into files. Having identical file structure across all projects simplifies navigation:
  • errors.tolk with constants or enums
  • storage.tolk with a storage and helper methods
  • messages.tolk with incoming/outgoing messages
  • some-contract.tolk as an entrypoint
  • probably, some other
When several contracts are developed simultaneously, their share the same codebase. For instance, struct SomeMessage, outgoing for contract A, is incoming for contract B. Or for deployment, contract A should know B’s storage to assign stateInit. See: imports.

Prefer methods to functions

All symbols across different files share the same namespace and must have unique names project-wise. There are no “modules” or “exports”. Using methods avoids name collisions:
Methods are also more convenient: obj.someMethod() looks nicer than someFunction(obj):
Same for static methods: Auction.createFrom(...) seems better than createAuctionFrom(...). A method without self is a static one:
Static methods may also be used to group various utility functions. For example, standard functions blockchain.now() and others are essentially static methods of an empty struct.
In large projects, this technique may be used to emulate namespaces. See: functions and methods.

How to describe “forward payload” in jettons

By a standard, a jetton transfer may have forwardPayload attached, which TL-B format is (Either Cell ^Cell). How to describe this in Tolk?
The union above is exactly what TL-B’s Either means. It will work, but has some disadvantages in gas consumption and validation (for a cell we also need to check that no extra data exists besides the ref). Actually, no universal solution exists — it depends on particular requirements:
  • is the validation needed?
  • are custom error codes needed on error?
  • should it be convenient to be assigned from code?
All these cases are described on a separate page. See: forward payload in jettons.

How to describe “address or none” in a field

A nullable address — address? — is a pattern to say “optional address”, sometimes called “maybe address”.
  • null is “none”, serialized as ‘00’ (two zero bits)
  • address is “internal”, serialized as 267 bits: ‘100’ + workchain + hash
See: addresses.

How to calculate crc32/sha256 at compile-time

Several built-in functions operate on strings and work at compile-time:
See: standard library.

How to return a string from a contract

TVM has no strings, it has only slices. A binary slice must be encoded in a specific way to be parsed and interpreted correctly as a string.
  1. Fixed-size strings via bitsN — possible if the size is predefined.
  2. Snake strings: portion of data → the rest in a ref cell, recursively.
  3. Variable-length encoding via custom serializers.
See: strings.

Final suggestion: do not micro-optimize

Tolk compiler is smart. It targets “zero overhead”: clean, consistent logic must be as efficient as low-level code. It automatically inlines functions, reduces stack permutations, and does a lot of underlying work to let a developer focus on business logic. And it works. Any attempts to overtrick the compiler result either in negligible or even in negative effect. That’s why, follow the “readability-first principle”:
  • use one-line methods without any worries — they are auto-inlined
  • use small structures — they are as efficient as raw stack values
  • extract constants and variables for clarity
  • do not use assembler functions unless being absolutely sure
See: compiler optimizations.