Skip to main content
T TON Adoption
Basics DEV · 2026

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
10 min read

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:

  1. 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.
  2. 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.
  3. 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, uint32 etc. for narrower widths).
  • coins — a dedicated TON-amount type with its own serialisation.
  • address — TON address (MsgAddress in 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

FunCTolk
Manual begin_parse() / load_uint(N) for every fieldbody.parseAs<MyStruct>() in one line
throw_if(err, condition)assert(!condition) throw err
() as unit type in signaturesJust omit the return type
~load_msg_addr() with tilde-mutationslice.loadAddress() via a plain dot call
TVM opcodes as magic numbersUPPER_SNAKE_CASE constants
Names like storage::owner_addressSupported via backtick escape, but camelCase is idiomatic

Tolk vs Tact: what stayed low-level

TactTolk
Auto-serialisation layer with hidden allocationsExplicit toCell() / fromCell(), visible in code
receive("message") handlersmatches<T>() by OPCODE — closer to TVM logic
self.field in OOP styleExplicit Storage struct passed into functions
as uint32 after the field nameType inline: counter: int32
Hidden gas costFull 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 over contract.getData() and setData(). @inline lets 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 no onBouncedMessage (a classic source of silent losses).
  • E013 unauthorized-accessstorage.save() without a preceding sender check. Uses CFG + dataflow analysis at the level of Slither/Mythril.
  • E018 random-requires-initialization — a call to random() without randomize_lt().
  • E019 divide-before-multiply — the (a / b) * c pattern, 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 Foundry vm.assume analogue).
  • 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 func2tolk as you find time.

When to pick what: a decision matrix

ScenarioRecommended languageWhy
New project, team with no TON experienceTolkLowest barrier, modern syntax, native in Acton
New project, team with FunC experienceTolkTVM mental model is preserved, syntax is simpler
Plain business-logic contract, no hard gas constraintsTactHighest abstraction level, fastest to write
Financial core (AMM, lending, vault) with critical gasTolk or FunCTransparent gas cost, auditable model
Extension of an existing FunC projectFunC + targeted TolkDon’t break working code
Legacy FunC migrationfunc2tolk + manual cleanupAuto-convert → tests → lint
Learning projectTolkActon 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.fc from STON.fi) via func2tolk runs 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 check fires 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/node from 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

Tolk compiles directly to TVM bytecode like FunC, but the syntax is closer to TypeScript: structs, enums, pattern matching, normal typing with no explicit stack work. Boilerplate that took tens of stack-manipulation lines in FunC collapses into a single struct declaration plus a matches-check in Tolk.
Tact is a high-level, OOP-style language with a hidden cells/slices layer. Tolk stays closer to TVM: explicit cell/slice/builder, controllable gas, no automatic serialisation layer. Tolk is the middle ground: friendly syntax without losing low-level control.
FunC is too low-level — high friction for new developers. Tact is too far from TVM — you lose gas control and meet edge-case surprises. Tolk closes that gap: readable code plus a transparent execution model. TON Foundation positions Tolk as the recommended default for new projects.
Yes. Acton can compile both languages inside the same Acton.toml. There is also acton func2tolk, which converts .fc files to .tolk via npx @ton/convert-func-to-tolk. You can migrate gradually: keep the core in FunC and write new modules in Tolk.
Messages are declared as a struct with a static OPCODE constant. Inside onInternalMessage you use body.matches<MyMessage>() to test and body.parseAs<MyMessage>() to decode. This replaces the manual begin_parse + load_uint(32) chains you used to write by hand in FunC.
29 built-in rules (E001-E030 minus one) in the Acton linter. Several are security-critical: E007 (no-bounce-handler), E013 (unauthorized-access via CFG + dataflow), E018 (random without randomize_lt), E019 (divide-before-multiply). The rest are quality and style rules.
Tests are written in Tolk itself under contracts/tests/*.test.tolk using the @acton/testing package. Plain unit tests, coverage, mutation testing (acton test --mutate) and fuzzing via the @test.fuzz annotation with fuzz.bound and fuzz.assume helpers are all supported.
Public communications from TON Foundation and the Acton team mention an expanded standard library, generic functions, richer pattern matching, and tighter on-chain library integration. No fixed dates — Tolk evolves through Acton minor releases.

Related