Auto-serialization instead of slices/builders
Tolk type system is designed to entirely avoid manual cell parsing. The presence ofbeginCell() indicates a possibly wrong approach.
All practical use cases in contract interaction are expressed with structures, unions, and references.
Familiar with TL-B?The type system is considered as a replacement for TL-B.
Read Tolk vs TL-B.
Read Tolk vs TL-B.
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 cells —Cell<T>.
Literally, it means: a cell whose contents is T:
Not “fromCell”, but “lazy fromCell”
In practice, when reading data from cells, preferlazy:
lazy SomeStruct.fromCell(c)overSomeStruct.fromCell(c)lazy typedCell.load()overtypedCell.load()
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.MyString as a regular type — everywhere:
Contract storage = struct + load + save
Contract storage is a regularstruct, serialized into persistent on-chain data.
It is convenient to add load and store methods:
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:Guidelines for choosing opcodes
- When implementing jettons, NFTs, and other standards, use predefined prefixes according to the specification of each TEP.
- When developing custom protocols, use any random numbers.
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 fromCellandmatchover variants
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
When both contracts are developed in the same project (sharing common codebase),
such a struct is both an outgoing message for A and an incoming message for B.
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:stateInit to a separate function, because
it’s used not only to send a message, but also to calculate/validate an address without sending.
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.createMessage and other address calculations.
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.- Create a
structto represent the message body - Use
createExternalLogMessage+send
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.
Use assertions to validate user input
After parsing an incoming message, validate required fields withassert:
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.tolkwith constants or enumsstorage.tolkwith a storage and helper methodsmessages.tolkwith incoming/outgoing messagessome-contract.tolkas an entrypoint- probably, some other
SomeMessage, outgoing for contract A, is incoming for contract B.
Or for deployment, contract A should know B’s storage to assign stateInit.
Use only minimal declarations inside each contract.tolkTypically, each
some-contract.tolk file contains:- a union with available incoming messages
- entrypoints:
onInternalMessage,get fun - structures for complex replies from getters
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:obj.someMethod() looks nicer than someFunction(obj):
Auction.createFrom(...) seems better than createAuctionFrom(...).
A method without self is a static one:
blockchain.now() and others are essentially static methods of an empty struct.
How to describe “forward payload” in jettons
Guidelines state that transferring a jetton may have someforwardPayload to provide custom data for a transaction’s recipient.
They restrict a payload to have the following format (TL-B (Either Cell ^Cell)):
- bit ‘0’ means: payload = “all the next bits/refs” (inline payload)
- bit ‘1’ means: payload = “the next ref”
- Some implementations allow empty data to be passed (no bits at all). It is invalid, because either bit ‘0’ or bit ‘1’ must exist. An empty payload should actually be encoded as bit ‘0’: empty inline payload.
- Some implementations do not check that no extra data is left after bit ‘1’.
- Error codes differ between various implementations.
- Do you need validation or just proxy any data as-is?
- Do you need custom error codes while validating?
- Do you need to assign it dynamically or just to carry it forward?
Either looks nice:
- consumes more gas if you need just to carry it, due to branching at serialization back
- parsing does not check, that after bit ‘1’, no extra data is left
loadMaybeRef() is discouraged.
The preferred solution is:
slice, holding an encoded union.
For example, creating a “ref payload” from code having a cell requires manual work:
RemainingBitsAndRefs | cell is way more convenient for assignment, but as discussed above,
has its own disadvantages.
The conclusion: payloads are cumbersome, and the solution depends on particular requirements.
How to describe “address or none” in a field
A nullable address —address? — is a pattern to say “potentially missing address”, sometimes called “maybe address”.
nullis “none”, serialized as ‘00’ (two zero bits)addressis “internal”, serialized as 267 bits: ‘100’ + workchain + hash
How to calculate crc32/sha256 at compile-time
Several built-in functions operate on strings and work at compile-time: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.- Fixed-size strings via
bitsN— possible if the size is predefined. - Snake strings: portion of data → the rest in a ref cell, recursively.
- Variable-length encoding via custom serializers.
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
Use Tolk as intended — gas will take care of itself.
But if the logic is hard to follow — it’s where the inefficiency hides.
But if the logic is hard to follow — it’s where the inefficiency hides.