Prefer automatic serialization to manual one
Manual work with slices and builders is error-prone and tedious. By comparison, auto-serialization with structures helps express data with types and prevents many related bugs.Prefer typed cells with Cell<T>
All data in TON is stored in cells. To express data relation clearly and to aid in serialization, use cells with well-typed contents: Cell<T>.
Use lazy data loading
When reading data from cells, add thelazy keyword:
lazy SomeStruct.fromCell(c)overSomeStruct.fromCell(c)lazy typedCell.load()overtypedCell.load()
lazy, the compiler loads only the requested fields, skipping the rest. This reduces gas consumption and bytecode size:
Use type aliases to express custom serialization logic
Serialization may require custom rules which are not covered by existing types. Tolk allows defining custom serialization rules for type aliases:signature against it. A manual approach would be to read uint256, calculate the hash of the remaining slice, then read other fields and compare the signatures.
However, a better solution is to continue using auto-serialization by introducing a synthetic field populated only when loading a slice and never when composing a cell with a builder:
Use contract storage as a structure
Contract storage is a regularstruct, serialized into persistent on-chain data.
Add load and store methods for convenience:
Express messages as structs with 32-bit prefixes
By convention, every message in TON has an opcode: a unique 32-bit number. In Tolk, everystruct can have a serialization prefix of arbitrary length. Use 32-bit prefixes to express message opcodes.
Use unions to handle incoming messages
The suggested pattern:- Each incoming message is made a struct with an opcode.
- Structs are combined into a union type.
- Union is used to lazily load data from the message body slice.
- Finally, result is pattern matched over union variants.
lazy keyword works with unions and performs a lazy match by the slice prefix: a message opcode. This approach is more efficient than manual opcode parsing and branching via a series of if (op == TRANSFER_OP) statements.
Use structs to send messages
To send a message from contract A to contract B:- Declare a struct with an opcode and fields expected by the receiver.
- Use the
createMessage()function to compose a message, and thesend()method to send it.
When both contracts share the same codebase, a struct serves as an outgoing message for A and an incoming message for B.
Attach initial code and data to a message to deploy another contract
Contract deployment is performed by attaching the code and data of the future contract to a message sent to its soon-to-be-initialized address. That address is deterministically calculated from the attached code and data. A common case is when the jetton minter contract deploys a jetton wallet contract per user, knowing the future wallet’s initial state: code and data.StateInit generation to a separate function:
See the reference jetton contracts implementation in the
tolk-bench repository on GitHub.Target certain shards when deploying sibling contracts
Specify the prefix length and the contract address to aim for the same shard. For example, sharded jetton wallet must be deployed to the same shard as the owner’s wallet.Emit events and logs to off-chain world during development
External messages with a special addressnone are used to emit events and logs to the outer world. Indexers catch such messages and provide a picture of on-chain activity.
External messages cost less gas than internal ones and help track events during contract development. They provide a simple way to emit structured logs that indexers and debugging tools can consume.
To send an external log message:
- Create a
structto represent the message body. - Use
createExternalLogMessage()to compose a message and thesend()method to send it.
Return several state values as a structure from a get method
When a contract getter needs to return several values, introduce a structure. Avoid returning unnamed tensors like(int, int, int). Field names provide clear metadata for client wrappers and human readers.
Validate user input with assertions
After parsing an incoming message, validate required fields withassert:
Organize a project into several files
Consistent file structure across projects simplifies navigation:- Supply
errors.tolkwith constants or enums. - Supply
storage.tolkwith storage and helper methods. - Supply
messages.tolkwith incoming and outgoing messages. - Have
some-contract.tolkas an entrypoint. Keep entrypoint files minimal, use imports to bring all supplementary code.
SomeMessage, outgoing for contract A, can be incoming for contract B. In other cases, contract A must know B’s storage to assign the initial state for deployment.
Prefer methods to functions
All symbols across different files share the same namespace and must have unique names project-wide. There are no modules or exports. Use methods to avoid name collisions:obj.someMethod() reads better than someFunction(obj).
Auction.createFrom(...) reads better than createAuctionFrom(...).
A method without a self parameter is static:
blockchain.now() are static methods on an empty struct. This technique emulates namespaces:
Use optional addresses to have address defaults
A nullable addressaddress? is a pattern for an optional address, sometimes called “maybe address”:
nullrepresents the addressnone.addressrepresents an internal address.
Calculate CRC32 or SHA256 at compile-time
Several built-in functions operate on strings and work at compile-time:Encode slices as strings
TVM has no strings, 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
bitsNare possible if the size is predefined. - Snake strings place a portion of data in the current cell and the rest in a ref cell, recursively. Use custom serializers to enable construction and parsing of such variable-length strings.
Avoid micro-optimization
The compiler applies many optimizations: it automatically inlines functions, reduces stack allocations, and handles the underlying work. Attempts to outsmart the compiler yield negligible effects, either positive or negative. Prefer readability over manual optimizations:- Use one-line methods freely as they are auto-inlined.
- Use flat structures: they are as efficient as raw stack values.
- Extract standalone values into constants and variables.
- Avoid assembler functions unless necessary.