Tolk: TON smart contract language — intro for FunC/Tact devs
Tolk is the next-gen TON smart contract language and Acton's default. TypeScript-style syntax, strong typing, pattern matching.
- Author
- TON Adoption Team · research desk
- Published
Contents22sections
- A short history: how we got to Tolk
- Why another language
- Tolk syntax: the key elements
- Types
- Functions and contracts
- Messages and pattern matching
- Storage
- Tolk vs FunC: what was simplified
- Tolk vs Tact: what stayed low-level
- Example: a full counter contract
- Tolk lint rules: 29 checks out of the box
- Testing: unit + coverage + fuzz + mutation
- Fuzzing via @test.fuzz
- Mutation testing
- Deploying a Tolk contract
- Compatibility: mixing Tolk and FunC
- acton func2tolk: automatic conversion
- Mixed-language projects
- When to pick what: a decision matrix
- Real experience from the first weeks
- What is on the Tolk roadmap
- Bottom line
TL;DR. Tolk is the next generation of TON’s smart contract language, the native language of Acton v1.0 (released 11 May 2026). It inherits FunC’s semantics and compiles directly to TVM bytecode, but the syntax is closer to TypeScript: structs, enums, pattern matching, normal typing. The goal is to bridge low-level FunC and high-level Tact — readable code without losing control over gas and cell layout. This article is a map of the terrain for a developer who already knows FunC or Tact and wants to understand what is new in Tolk and whether to migrate.
A short history: how we got to Tolk
The TON stack went through three generations of languages:
- FunC — the first language, written by Nikolai Durov’s team. Low-level, statically typed, very close to TVM. Stack control is nearly manual. The majority of existing mainnet contracts (jetton minters, NFT collections, STON.fi and DeDust AMM pairs) are written in FunC.
- Tact — appeared in 2023. High-level, syntax close to TypeScript/Kotlin, OOP style, automatic serialisation. The goal was “Solidity for TON” and a lower entry barrier. The cost was a partial hiding of the TVM model: the developer knows less about what happens at the cell/slice level.
- Tolk — designed by the TON Core team as the next iteration of FunC. Keeps direct compilation to TVM bytecode and the low-level model, but adds a modern syntax, pattern matching, and strong typing. According to TON Foundation, gas overhead on typical contracts drops by 30-50% compared to equivalent FunC.
Tolk does not deprecate FunC — it evolves it. It is not a separate compiler target; think of it as “FunC 2.0” with a human syntax.
Why another language
What FunC + Tact did not deliver:
- FunC is too low-level. A simple counter contract in FunC takes around 30 lines of stack manipulation, manual serialisation, and a
begin_parse() / load_uint(32)chain for every field. New developers need a month just to stop being scared. - Tact is too far from TVM. The automatic serialisation layer and OOP abstractions save lines but hide the real cost of operations. Edge-case bugs in gas spend or message encoding become harder to catch.
- No common standard. Before Tolk, a FunC project and a Tact project were two different worlds — different tooling, different debugging, different library ecosystems.
Tolk borrows readability from Tact (structs, pattern matching, normal types) while keeping the transparency of FunC (explicit cell/slice, controlled gas, direct TVM compilation). It is a compromise — and a useful one.
Tolk syntax: the key elements
Types
The base TVM types are available directly and written in lowercase (like TypeScript primitives):
int— signed integer (257 bits by default;int32,int64,uint32etc. for narrower widths).coins— a dedicated TON-amount type with its own serialisation.address— TON address (MsgAddressin TL-B).cell— the root cell of TON data.slice— a read pointer into a cell.builder— a write accumulator for a new cell.
Composite types:
struct Storage {
id: uint32
owner: address
counter: int32
}
enum MessageType {
Increase = 0x12345678
Decrease = 0x87654321
Reset = 0xAABBCCDD
}
struct and enum are the main lightweight abstractions. They compile to ordinary cell serialisation, but in code you treat them as regular typed values.
Functions and contracts
Functions are declared with fun, return type after a colon:
@inline
fun loadStorage(): Storage {
return Storage.fromCell(contract.getData())
}
@inline
fun saveStorage(storage: Storage) {
contract.setData(storage.toCell())
}
A contract is a set of top-level functions plus message handlers (onInternalMessage, onBouncedMessage, get methods). There is no separate contract MyContract { ... } block like in Tact: the contract is the file.
Messages and pattern matching
Incoming messages are declared as a struct with a static OPCODE:
struct IncreaseCounter {
static OPCODE = 0x12345678
queryId: uint64
delta: int32
}
Inside the handler you use matches<T>() for the type check and parseAs<T>() for decoding:
fun onInternalMessage(in: InMessage) {
val storage = loadStorage()
val body = in.body
if (body.matches<IncreaseCounter>()) {
val msg = body.parseAs<IncreaseCounter>()
assert (in.senderAddress == storage.owner) throw Errors.NotOwner
storage.counter += msg.delta
saveStorage(storage)
}
}
This replaces the whole begin_parse() / load_uint(32) / load_uint(64) / load_int(32) sequence you used to write by hand for every message in FunC.
Storage
Storage is serialised by a direct pair of toCell() / fromCell() on a struct. The contract reads state via contract.getData(), writes via contract.setData(cell). No hidden layer like in Tact: you see exactly the cell that lands in c4.
Tolk vs FunC: what was simplified
| FunC | Tolk |
|---|---|
Manual begin_parse() / load_uint(N) for every field | body.parseAs<MyStruct>() in one line |
throw_if(err, condition) | assert(!condition) throw err |
() as unit type in signatures | Just omit the return type |
~load_msg_addr() with tilde-mutation | slice.loadAddress() via a plain dot call |
| TVM opcodes as magic numbers | UPPER_SNAKE_CASE constants |
Names like storage::owner_address | Supported via backtick escape, but camelCase is idiomatic |
Tolk vs Tact: what stayed low-level
| Tact | Tolk |
|---|---|
| Auto-serialisation layer with hidden allocations | Explicit toCell() / fromCell(), visible in code |
receive("message") handlers | matches<T>() by OPCODE — closer to TVM logic |
self.field in OOP style | Explicit Storage struct passed into functions |
as uint32 after the field name | Type inline: counter: int32 |
| Hidden gas cost | Full control over cell layout and operations |
This is not “Tact with different syntax” — it is a different model. Tolk does not hide TVM; it makes working with it pleasant.
Example: a full counter contract
The canonical counter from the Acton template (acton new my_counter --template counter):
import "@stdlib/tvm-dicts"
struct Storage {
id: uint32
owner: address
counter: int32
}
@inline
fun loadStorage(): Storage {
return Storage.fromCell(contract.getData())
}
@inline
fun saveStorage(storage: Storage) {
contract.setData(storage.toCell())
}
struct IncreaseCounter {
static OPCODE = 0x12345678
queryId: uint64
delta: int32
}
struct ResetCounter {
static OPCODE = 0xAABBCCDD
queryId: uint64
}
fun onInternalMessage(in: InMessage) {
val storage = loadStorage()
val body = in.body
if (body.matches<IncreaseCounter>()) {
val msg = body.parseAs<IncreaseCounter>()
assert (in.senderAddress == storage.owner) throw Errors.NotOwner
storage.counter += msg.delta
saveStorage(storage)
} else if (body.matches<ResetCounter>()) {
assert (in.senderAddress == storage.owner) throw Errors.NotOwner
storage.counter = 0
saveStorage(storage)
}
}
get fun currentValue(): int32 {
return loadStorage().counter
}
Walking through it:
- Storage — three fields, serialised into one cell.
loadStorage/saveStorage— sugar overcontract.getData()andsetData().@inlinelets the compiler inline them and save gas.IncreaseCounter/ResetCounter— two message shapes with unique opcodes.onInternalMessage— a single entry point;matches<T>()routes to branches.get fun— a get method exposed via external RPC queries with no transaction.
The FunC equivalent would take 30-40 lines of stack magic. Tact would take roughly the same line count, but with hidden allocations and no explicit control over cell layout.
Tolk lint rules: 29 checks out of the box
Acton ships with acton check — a linter that covers security and quality bug classes. The ones most useful for audit and code review:
- E007
no-bounce-handler— a contract with noonBouncedMessage(a classic source of silent losses). - E013
unauthorized-access—storage.save()without a preceding sender check. Uses CFG + dataflow analysis at the level of Slither/Mythril. - E018
random-requires-initialization— a call torandom()withoutrandomize_lt(). - E019
divide-before-multiply— the(a / b) * cpattern, a typical source of precision loss. - E007/E016 — dangerous send modes (
DESTROY_IF_ZERO,CARRY_ALL_BALANCE) without a safety comment.
Commands:
acton check
acton check --explain E013 # rule details
acton check --fix # auto-fix simple cases
29 rules is not marketing. Each one fires on real production code. The official counter template from TON Foundation is lint-clean, but as soon as real code starts, warnings appear quickly.
Testing: unit + coverage + fuzz + mutation
Tests are written in Tolk itself using @acton/testing. The basic pattern:
import "@acton/testing/expect"
import "@acton/testing/blockchain"
test("counter increases by sender owner", () => {
val blockchain = TestBlockchain.create()
val deployer = blockchain.treasury("deployer")
val counter = blockchain.deploy(Counter, deployer.address)
val res = counter.sendIncrease(deployer, 5)
expect(res).toHaveSuccessfulTx({
from: deployer.address,
to: counter.address,
success: true
})
expect(counter.currentValue()).toEqual(5)
})
Run with acton test. On the canonical counter project, 8 tests pass in under 30 ms — about two orders of magnitude faster than @ton/sandbox on Node.js.
Fuzzing via @test.fuzz
An annotation over a get function turns it into a fuzz test:
import "@acton/testing/expect"
import "@acton/testing/fuzz"
@test.fuzz
get fun `test balance stays bounded`(value: int) {
val bounded = fuzz.bound(value, 0, 100)
fuzz.assume(bounded != 13) // discard input 13
expect(bounded >= 0).toBeTrue()
}
Helpers:
fuzz.bound(value, min, max)— clamp into range (the Foundryvm.assumeanalogue).fuzz.assume(condition)— discard the input if the condition is false.
Used to check invariants like “total_supply == sum(balances)” or “no overflow in the swap function”.
Mutation testing
The main weapon for audit:
acton test --mutate --mutate-contract Counter
Acton automatically mutates the code (swaps += for -=, == for !=, removes assert, flips boundary conditions) and runs the test suite for each mutation. If all tests pass — the mutation survived — coverage at that point is insufficient.
On the official TON Foundation counter template, mutation testing finds 3 survivors out of 15 — even the TVM vendors have gaps. For bug-hunting this means: every survivor is a candidate bug vector.
Deploying a Tolk contract
Deployment is written as a Tolk script via acton script:
// contracts/scripts/deploy.tolk
import "@acton/emulation/scripts"
import "@wrappers/Counter.gen"
fun main() {
val deployer = scripts.wallet("deployer")
val counter = Counter.create({ owner: deployer.address, counter: 0 })
counter.deploy(deployer.address, { value: 0.05 TON }).waitForFirstTransaction()
println("Deployed at: {}", counter.address)
}
Running it:
acton script contracts/scripts/deploy.tolk # emulation (no send)
acton script contracts/scripts/deploy.tolk --fork-net testnet # emulation with live testnet state
acton script contracts/scripts/deploy.tolk --net testnet # real send
acton script contracts/scripts/deploy.tolk --net testnet --tonconnect # via Tonkeeper
TypeScript wrapper generation for the frontend:
acton wrapper Counter --ts
Generates wrappers-ts/Counter.gen.ts — a typed wrapper you import into a React/Vite app and call contract methods with full IDE autocomplete.
Compatibility: mixing Tolk and FunC
Yes — and this is a critical feature for migrating off legacy code.
acton func2tolk: automatic conversion
acton func2tolk path/to/vault.fc
Under the hood: npx @ton/convert-func-to-tolk@1.0.0. Input .fc, output .tolk:
() storage::load() impure inline { ... }→@inline fun `storage::load`() { ... }ds~load_msg_addr()→ds.loadAddress()throw_if(err, condition)→assert(!condition) throw err- TVM opcodes auto-promoted to UPPER_SNAKE_CASE constants
Names with :: are preserved via backtick-escaped identifiers. The conversion is lexical, not semantic — you still need to run acton check and the tests afterwards, but 80% of the work is automated.
Mixed-language projects
A single Acton.toml can contain both FunC and Tolk modules. This lets you:
- Keep an audited FunC core (e.g. STON.fi v2 AMM pairs) untouched.
- Write new modules in Tolk.
- Convert old modules gradually via
func2tolkas you find time.
When to pick what: a decision matrix
| Scenario | Recommended language | Why |
|---|---|---|
| New project, team with no TON experience | Tolk | Lowest barrier, modern syntax, native in Acton |
| New project, team with FunC experience | Tolk | TVM mental model is preserved, syntax is simpler |
| Plain business-logic contract, no hard gas constraints | Tact | Highest abstraction level, fastest to write |
| Financial core (AMM, lending, vault) with critical gas | Tolk or FunC | Transparent gas cost, auditable model |
| Extension of an existing FunC project | FunC + targeted Tolk | Don’t break working code |
| Legacy FunC migration | func2tolk + manual cleanup | Auto-convert → tests → lint |
| Learning project | Tolk | Acton templates are Tolk, docs are fresh |
Short rule: Tolk by default, keep FunC for legacy and the most gas-critical paths, use Tact for prototypes without production load.
Real experience from the first weeks
Tolk + Acton became publicly available on 11 May 2026 — the release is fresh and the ecosystem is still forming. Observed so far:
- Conversion works. Translating real FunC contracts (the 84-line
vault.fcfrom STON.fi) viafunc2tolkruns through with no manual fixes. The output reads cleanly. - Mutation testing is productive. Even on the official TON Foundation template, survivors show up — missing sender checks in Decrease/Reset handlers, an off-by-one with
>=vs>. - Linter catches real bugs. On production FunC code passed through the converter,
acton checkfires on E013 (unauthorized-access) and E007 (no-bounce-handler). - Linux Node.js is required for
func2tolk— Windows-native nodejs trips on WSL UNC paths. Workaround: install~/.local/nodefrom a tarball manually.
These are first-week impressions. In 3-6 months the library ecosystem, educational content, and production projects will grow — but the first-mover window is open right now.
What is on the Tolk roadmap
Public communications from TON Foundation and the Acton team mention:
- An expanded standard library (
@stdlib/*) — more ready-made helpers for jetton/NFT/DNS patterns. - Generic functions — typed templates without code duplication.
- Richer pattern matching — switch expressions, exhaustiveness checks.
- On-chain library integration via
acton library publish— code reuse between contracts at the TVM level.
No firm dates. Tolk evolves through Acton CLI minor releases: acton up pulls the latest compiler, stdlib, and linter. Track via the CHANGELOG in github.com/ton-blockchain/acton.
Bottom line
Tolk is the inflection point where the TON stack matures into a real smart contract language. It keeps FunC’s transparency and takes Tact’s readability without becoming either. For a new project in 2026 it is the rational default. For legacy FunC codebases it offers a gradual migration via func2tolk without a full rewrite.
Tolk development only makes sense paired with Acton — the toolchain delivers lint, mutation testing, debugger, and deploy. Installation and quickstart are covered there.
Frequently asked
How is Tolk different from FunC?
How is Tolk different from Tact?
Why yet another language when FunC and Tact already exist?
Can Tolk and FunC be mixed in one project?
What is the Tolk syntax for incoming messages?
How many lint rules ship with Tolk out of the box?
How do you test Tolk contracts?
What is on the Tolk roadmap after v1.0?
Related
- BasicsMay 14, 2026
Acton v1.0 — Foundry for TON: the complete guide (2026)
Acton v1.0 from TON Foundation shipped on May 11, 2026: a single Rust CLI that replaces blueprint, sandbox, Misti and func.
- BasicsDec 23, 2025
TON for developers: FunC, Tact and Tolk in 2026
The smart contract languages used on TON in 2026 — FunC, Tact, Tolk. Which to pick as a beginner, how they differ, what tooling you need and where to learn.
- BasicsFeb 16, 2026
TON mainnet vs testnet: what they are and how to switch
How TON mainnet differs from testnet, how to switch Tonkeeper to testnet, where to get free testnet TON and why developers and curious users care about the.
- BasicsJan 17, 2026
Why TON is not EVM-compatible and what it means for users
TON uses TVM instead of the Ethereum Virtual Machine — why it was designed that way, what it costs and gains the user, and which TON-EVM bridges exist in 2026.
- NewsMar 23, 2026
TON Foundation: who actually runs the blockchain in 2026
A breakdown of TON Foundation — structure, key people, the split from Telegram, the roles of Max Crown and Steve Yun, the rise of TON Strategy Co.