К основному содержанию
T TON Adoption
Основы DEV · 2026

Tolk: новый язык смарт-контрактов TON в 2026

Tolk — преемник FunC и нативный язык Acton. TypeScript-подобный синтаксис, сильная типизация, pattern matching.

Автор
TON Adoption Team · исследовательская группа проекта
Опубликовано
9 мин. чтения

TL;DR. Tolk — это следующее поколение языка смарт-контрактов TON, нативный язык Acton v1.0 (релиз 11 мая 2026). Он наследует семантику FunC и компилируется напрямую в TVM-байткод, но синтаксически ближе к TypeScript: structs, enums, pattern matching, нормальная типизация. Цель — закрыть зазор между низкоуровневым FunC и высокоуровневым Tact: дать читаемый код без потери контроля над gas и cell-layout. Эта статья — карта местности для разработчика, который знает FunC или Tact и хочет понять, что нового в Tolk и стоит ли мигрировать.

Краткая история: как мы пришли к Tolk

TON-стек прошёл три поколения языков:

  • FunC — первый язык, написан командой Николая Дурова. Низкоуровневый, статически типизированный, очень близко к TVM. Контроль над стеком — почти ручной. Большинство существующих mainnet-контрактов (jetton-минтеры, NFT-коллекции, AMM-пары STON.fi и DeDust) написаны на FunC.
  • Tact — появился в 2023. Высокоуровневый, синтаксис похож на TypeScript/Kotlin, OOP-стиль, автоматическая сериализация. Цель — дать «Solidity для TON» и снизить порог входа. Платой стало частичное скрытие модели TVM: разработчик меньше знает, что именно происходит на уровне cell/slice.
  • Tolk — разработан TON Core team как следующая итерация FunC. Сохраняет прямую компиляцию в TVM-байткод и низкоуровневую модель, но добавляет современный синтаксис, паттерн-матчинг и сильную типизацию. По данным TON Foundation, gas-оверхед на типичных контрактах падает на 30-50% по сравнению с эквивалентным FunC.

Tolk не отменяет FunC — он его развивает. Это не отдельный compiler-target, а скорее «FunC 2.0» с человеческим синтаксисом.

Зачем ещё один язык

Чего не дали FunC + Tact:

  1. FunC слишком низкоуровневый. Простой counter-контракт на FunC — это ~30 строк со stack-manipulation, ручной сериализацией и begin_parse() / load_uint(32) для каждого поля. Новому разработчику нужен месяц только чтобы перестать пугаться.
  2. Tact слишком далеко от TVM. Автоматический сериализационный слой и OOP-абстракции экономят строки, но прячут реальную стоимость операций. Edge-case баги в gas-расходе или в encoding’е сообщений ловить сложнее.
  3. Нет общего стандарта. До Tolk проект на FunC и проект на Tact — это два разных мира с разной тулзой, разным дебагом, разной библиотечной экосистемой.

Tolk берёт читаемость Tact (structs, pattern matching, нормальные типы) и оставляет прозрачность FunC (явные cell/slice, контролируемый gas, прямая компиляция в TVM). Это компромисс, но компромисс полезный.

Синтаксис Tolk: ключевые элементы

Типы данных

Базовые TVM-типы доступны напрямую и пишутся в нижнем регистре (как примитивы TypeScript):

  • int — знаковое число (по умолчанию 257 бит, можно указать int32, int64, uint32 и т.д.).
  • coins — специальный тип для TON-сумм с собственной сериализацией.
  • address — TON-адрес (MsgAddress в TL-B).
  • cell — корневая ячейка TON-данных.
  • slice — указатель на данные внутри ячейки (для чтения).
  • builder — конструктор новой ячейки (для записи).

Composite-типы:

struct Storage {
    id: uint32
    owner: address
    counter: int32
}

enum MessageType {
    Increase = 0x12345678
    Decrease = 0x87654321
    Reset    = 0xAABBCCDD
}

struct и enum — главные «лёгкие» абстракции. Они компилируются в обычную cell-сериализацию, но в коде вы работаете с ними как с обычными типами.

Функции и контракты

Функции объявляются с ключевым словом fun, тип возвращаемого значения после двоеточия:

@inline
fun loadStorage(): Storage {
    return Storage.fromCell(contract.getData())
}

@inline
fun saveStorage(storage: Storage) {
    contract.setData(storage.toCell())
}

Контракт — это набор top-level функций плюс хендлеры сообщений (onInternalMessage, onBouncedMessage, get-методы). Никакого отдельного contract MyContract { ... }-блока, как в Tact: контракт — это файл.

Сообщения и pattern matching

Входящие сообщения объявляются как struct с static OPCODE:

struct IncreaseCounter {
    static OPCODE = 0x12345678
    queryId: uint64
    delta: int32
}

Внутри хендлера используется matches<T>() для проверки типа и parseAs<T>() для разбора:

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)
    }
}

Это заменяет всю простыню begin_parse() / load_uint(32) / load_uint(64) / load_int(32), которую во FunC писали руками для каждого сообщения.

Storage

Storage сериализуется прямой парой toCell() / fromCell() на struct. Контракт читает состояние через contract.getData(), пишет — через contract.setData(cell). Никакого скрытого слоя, как в Tact: вы видите ровно ту ячейку, которая попадёт в c4.

Tolk vs FunC: что упрощено

Что во FunCЧто в Tolk
Ручной begin_parse() / load_uint(N) для каждого поляbody.parseAs<MyStruct>() одной строкой
throw_if(err, condition)assert(!condition) throw err
() как unit-тип в сигнатурахПросто отсутствие возвращаемого типа
~load_msg_addr() с тильдой-мутациейslice.loadAddress() обычным dot-вызовом
TVM opcodes как магические числаUPPER_SNAKE_CASE константы
Имена типа storage::owner_addressПоддерживаются через backtick-escape, но обычно — camelCase

Tolk vs Tact: что осталось низкоуровневым

Что в TactЧто в Tolk
Автоматический сериализационный слой со скрытыми allocationsЯвные toCell() / fromCell(), видимые в коде
receive("message")-обработчикиmatches<T>() по OPCODE — ближе к TVM-логике
self.field в стиле OOPЯвная struct-переменная Storage, передаваемая в функции
as uint32 после имени поляТип сразу: counter: int32
Скрытый gas-costПолный контроль над cell-layout и операциями

Это не «Tact с другим синтаксисом» — это другая модель. Tolk не прячет TVM, он делает работу с ним приятной.

Пример: counter-контракт целиком

Канонический counter из шаблона Acton (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
}

Что здесь происходит, по строкам:

  • Storage — три поля, сериализуются в одну cell.
  • loadStorage / saveStorage — синтаксический сахар над contract.getData() и setData(). @inline подсказывает компилятору заинлайнить и сэкономить gas.
  • IncreaseCounter / ResetCounter — две формы сообщений с уникальными opcodes.
  • onInternalMessage — единая точка входа, внутри которой matches<T>() разводит ветки.
  • get fun — get-метод, доступный через внешние RPC-запросы без отправки транзакции.

Эквивалент на FunC занял бы 30-40 строк со stack-magic. На Tact — примерно столько же строк, но с скрытыми allocations и без явного контроля над cell-layout.

Линт-правила Tolk: 29 проверок из коробки

В Acton встроен acton check — линтер, который покрывает security- и quality-классы багов. Особенно полезны для аудита и code review:

  • E007 no-bounce-handler — контракт без onBouncedMessage (классический источник тихих потерь).
  • E013 unauthorized-accessstorage.save() без предшествующей проверки sender’а. Использует CFG + dataflow analysis уровня Slither/Mythril.
  • E018 random-requires-initialization — вызов random() без randomize_lt().
  • E019 divide-before-multiply — паттерн (a / b) * c, типичный источник precision loss.
  • E007/E016 — dangerous send-modes (DESTROY_IF_ZERO, CARRY_ALL_BALANCE) без safety-комментария.

Команда:

acton check
acton check --explain E013    # подробное описание правила
acton check --fix             # авто-фикс простых случаев

29 правил — не маркетинг. Каждое реально срабатывает на production-коде. На официальном counter-template TON Foundation линтер чистый, но как только начинается реальный код — warnings появляются быстро.

Тестирование: unit + coverage + fuzz + mutation

Тесты пишутся на самом Tolk, используют @acton/testing. Базовый паттерн:

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)
})

Запускается через acton test. На каноническом counter-проекте проходит 8 тестов за менее 30 мс — порядок на два быстрее, чем @ton/sandbox на Node.js.

Fuzzing через @test.fuzz

Аннотация над get-функцией превращает её в fuzz-тест:

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()
}

Хелперы:

  • fuzz.bound(value, min, max) — clamp значения в диапазон (аналог vm.assume в Foundry).
  • fuzz.assume(condition) — отбросить input, если условие не выполнено.

Используется для проверки инвариантов вида «total_supply == sum(balances)» или «overflow на swap-функции не возможен».

Mutation testing

Главное оружие для аудита:

acton test --mutate --mutate-contract Counter

Acton автоматически модифицирует код (меняет += на -=, == на !=, убирает assert, переворачивает граничные условия) и прогоняет тесты на каждой мутации. Если все тесты прошли — мутация survived — значит покрытие в этой точке недостаточно.

На официальном counter-template TON Foundation mutation testing находит 3 survivors из 15 — даже у вендоров TVM есть пробелы. Для bug-hunting’а это означает: каждый survivor = потенциальный bug-вектор.

Деплой Tolk-контракта

Деплой пишется как Tolk-скрипт через 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)
}

Запуск:

acton script contracts/scripts/deploy.tolk                       # эмуляция (без отправки)
acton script contracts/scripts/deploy.tolk --fork-net testnet    # эмуляция с live testnet state
acton script contracts/scripts/deploy.tolk --net testnet         # реальный send
acton script contracts/scripts/deploy.tolk --net testnet --tonconnect   # через Tonkeeper

Авто-генерация TypeScript-обёрток для frontend:

acton wrapper Counter --ts

Создаёт wrappers-ts/Counter.gen.ts — типизированный wrapper, который вы импортируете в React/Vite-приложение и вызываете методы контракта с полной IDE-подсказкой.

Совместимость: можно ли мешать Tolk и FunC

Да, и это важная фича для миграции с legacy-кодовой базы.

acton func2tolk: автоматическая конвертация

acton func2tolk path/to/vault.fc

Под капотом — npx @ton/convert-func-to-tolk@1.0.0. На вход .fc, на выход .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 автоматически в UPPER_SNAKE_CASE-константы

Имена с :: сохраняются через backtick-escaped identifiers. Конвертация лексическая, не семантическая — после неё всё ещё нужно прогнать acton check и тесты, но 80% работы делается автоматически.

Mixed-language проекты

Один Acton.toml может содержать одновременно FunC и Tolk модули. Это позволяет:

  • Оставить аудированное FunC-ядро (например, AMM-пары STON.fi v2) нетронутым.
  • Новые модули писать на Tolk.
  • Постепенно конвертировать старые модули через func2tolk по мере появления времени.

Когда что выбирать: матрица решения

СценарийРекомендуемый языкПочему
Новый проект, команда без TON-опытаTolkСамый низкий порог входа, современный синтаксис, нативный в Acton
Новый проект, команда с FunC-опытомTolkСохраняется ментальная модель TVM, синтаксис проще
Простой бизнес-логичный контракт без жёстких gas-ограниченийTactСамый высокий уровень абстракции, быстро
Финансовое ядро (AMM, lending, vault) с критичным gasTolk или FunCПрозрачный gas-cost, аудируемая модель
Расширение существующего FunC-проектаFunC + точечно TolkНе ломать рабочий код
Миграция legacy FunCfunc2tolk + ручной cleanupАвтоконвертация → тесты → линт
Учебный проектTolkActon-шаблоны на Tolk, документация свежая

Краткое правило: по умолчанию Tolk, FunC оставляйте для legacy и максимально критичного gas-кода, Tact — для прототипов без production-нагрузки.

Реальный опыт первых недель

Tolk + Acton доступны публично с 11 мая 2026 — релиз свежий, экосистема ещё формируется. Из наблюдаемого:

  • Conversion работает. Перевод реальных FunC-контрактов (vault.fc STON.fi на 84 строки) через func2tolk проходит без ручных правок. Синтаксис на выходе читается.
  • Mutation testing продуктивен. Даже на официальном template TON Foundation находятся survivors (отсутствие проверки sender’а в Decrease/Reset-хендлерах, граничное условие >= vs >).
  • Линт ловит реальные баги. На production-FunC коде через прослойку конвертации acton check срабатывает на E013 (unauthorized-access) и E007 (no-bounce-handler).
  • Linux Node.js обязателен для func2tolk — Windows-native nodejs ломается на WSL UNC-путях. Решение: ~/.local/node через ручную установку tarball’а.

Это — первые недели. Через 3-6 месяцев экосистема библиотек, обучающих материалов и production-проектов вырастет — но окно first-mover прямо сейчас.

Будущее Tolk: что обещано

В публичных коммуникациях TON Foundation и команды Acton упоминается:

  • Расширение стандартной библиотеки (@stdlib/*) — больше готовых helpers’ов для jetton/NFT/DNS-паттернов.
  • Generic-функции — типовые шаблоны без дублирования кода.
  • Более богатый pattern matching — switch-выражения, exhaustiveness check.
  • Интеграция on-chain библиотек через acton library publish — переиспользование кода между контрактами на уровне TVM.

Конкретных дат roadmap нет. Tolk развивается через минорные релизы Acton CLI: acton up подтягивает свежую версию компилятора, stdlib и линтера. Следить лучше через CHANGELOG в github.com/ton-blockchain/acton.

Итог

Tolk — это точка перехода TON-стека к зрелому языку смарт-контрактов. Он сохраняет прозрачность FunC и берёт читаемость от Tact, не становясь полностью ни тем, ни другим. Для нового проекта в 2026 году выбор Tolk — рациональный дефолт. Для legacy FunC-кодовых баз — постепенная миграция через func2tolk без переписывания с нуля.

Сама по себе Tolk-разработка имеет смысл только в связке с Acton — toolchain даёт линт, mutation testing, debugger и deploy. Установка и quickstart описаны там же.

Частые вопросы

Tolk компилируется напрямую в TVM-байткод, как FunC, но синтаксис ближе к TypeScript: structs, enums, pattern matching, normalная типизация без явной работы со стеком. Boilerplate, который во FunC занимал десятки строк stack-manipulation, в Tolk заменяется одной декларацией структуры и matches-проверкой.
Tact — высокоуровневый, OOP-стилевой язык со скрытой работой над cells/slices. Tolk остаётся ближе к TVM: явные cell/slice/builder, контролируемый gas, нет автоматического сериализационного слоя. Tolk это middle-ground: дружелюбный синтаксис, но без потери низкоуровневого контроля.
FunC слишком низкоуровневый — high friction для новых разработчиков. Tact слишком далеко от TVM — теряется контроль над gas и есть удивления в edge-cases. Tolk закрывает эту нишу: читаемый код плюс прозрачная модель исполнения. TON Foundation позиционирует Tolk как рекомендуемый дефолт для новых проектов.
Да. Acton умеет компилировать оба языка в рамках одного Acton.toml. Плюс есть команда acton func2tolk, которая автоматически конвертирует .fc-файлы в .tolk через npx @ton/convert-func-to-tolk. Можно мигрировать постепенно: оставить ядро на FunC, новые модули писать на Tolk.
Сообщения объявляются как struct с константой OPCODE. Внутри onInternalMessage используется body.matches<MyMessage>() для проверки и body.parseAs<MyMessage>() для разбора. Это заменяет ручной begin_parse + load_uint(32), который во FunC писали руками.
29 встроенных правил (E001-E030 минус один) в линтере Acton. Часть из них security-критичные: E007 (no-bounce-handler), E013 (unauthorized-access на основе CFG + dataflow), E018 (random без randomize_lt), E019 (divide-before-multiply). Остальное — quality- и style-правила.
Тесты пишутся на самом Tolk в contracts/tests/*.test.tolk, используют пакет @acton/testing. Поддерживаются обычные unit-тесты, coverage, mutation testing (acton test --mutate) и fuzzing через аннотацию @test.fuzz с хелперами fuzz.bound и fuzz.assume.
TON Foundation в публичных коммуникациях упоминает расширение стандартной библиотеки, generic-функции, более богатый pattern matching и интеграцию on-chain библиотек. Конкретных дат нет — Tolk развивается параллельно с Acton, апдейты ожидаются через минорные релизы CLI.

Похожие материалы