Розроблення смарт-контрактів на Tact (TON)
TON — це не ще один EVM-клон. Асинхронна модель повідомлень, actor model, нескінченне шардування — все це робить розроблення контрактів принципово іншою задачею порівняно з Solidity. Tact з'явився як спроба зробити цю розроблення більш доступною: сувора типізація, знайомий синтаксис, вбудовані safety-примітиви. Але за зручністю приховуються специфічні паттерни, незнання яких веде до втрати коштів або неробочої логіки.
Асинхронність як головна архітектурна проблема
В EVM виклик контракту — це синхронна операція. В TON — кожна взаємодія між контрактами це окреме повідомлення, яке обробляється в наступному блоці. Контракт A відправляє повідомлення контракту B, отримує відповідь через один або кілька блоків. У проміжку стан A може змінитись.
Це породжує паттерн, який розробники з EVM-фоном пропускають: optimistic state update. Логіка така: контракт A оновлює своє стан до отримання підтвердження від B — іначе при паралельних викликах виникає race condition. Якщо B повернеме помилку, A повинен откатити стан через обробник bounced message.
У Tact bounce-handler виглядає так:
bounced(msg: bounced<TokenTransfer>) {
self.balance += msg.amount; // откатуємо списання
}
Не реалізувати bounce-handler — означає втратити кошти при будь-якому відмові дочірнього контракту.
Управління газом: forward_ton та carry-value паттерн
В TON газ оплачується в TON (nanoton). При відправці повідомлення між контрактами потрібно явно вказати, скільки TON пересилається разом з повідомленням на оплату газу наступного контракту. У Tact це параметр value у send().
Типова помилка: відправити повідомлення з value: 0 — контракт-отримувач не зможе його обробити через нестачу газу. Повідомлення зависає або bounced. Правильний паттерн — carry-value: пересилати достатньо TON через ланцюг контрактів, розраховуючи газ на кожен крок.
Tact спрощує це через SendRemainingValue mode — залишок від вхідного повідомлення пересилається далі:
send(SendParameters{
to: nextContract,
value: 0,
mode: SendRemainingValue + SendIgnoreErrors,
body: NextMessage{...}.toCell()
});
Але SendIgnoreErrors — небезпечний флаг. Він ігнорує помилки при відправці, що може привести до silent failure. Використовуємо його тільки там, де втрата повідомлення некритична.
Як будуємо TON-контракти на Tact
Стек: Tact 1.x, Blueprint (тестовий фреймворк від TON Foundation), sandbox для локального запуску, @ton/core для роботи з cells/slices у тестах. TypeScript для тест-коду та скриптів деплоя.
Структура проекту слідує Blueprint-конвенціям: контракти в contracts/, тести в tests/, скрипти деплоя в scripts/. Кожен контракт — окремий файл з явним указанням contract MyContract with Deployable.
Тести через sandbox покривають:
- нормальний flow (happy path)
- bounce scenarios (що сталось при revert дочірнього контракту)
- граничні значення газу (достатньо ли value на кожен крок)
- паралельні виклики (не виникає ли race condition у стані)
Верифікація. TON верифікує контракти через ton-verify — порівнює хеш скомпільованого байткоду з тим, що розгорнуто. Верифікація на tonscan.org та tonviewer.com — стандарт для будь-якого публічного контракту.
Порівняння FunC та Tact
| Критерій | FunC | Tact |
|---|---|---|
| Синтаксис | Низькорівневий, C-подібний | Високорівневий, TypeScript-подібний |
| Безпека типів | Ручна | Вбудована |
| Швидкість розроблення | Повільно | Швидко |
| Контроль над ячейками | Повний | Обмежений |
| Оптимізація газу | Ручна, максимальна | Автоматична, достатня |
Для більшості задач — DeFi примітиви, NFT контракти, jetton стандарт — Tact достатній та безпечніший. FunC використовуємо коли потрібна максимальна оптимізація газу або нестандартна робота з cell layout.
Процес роботи
Починаємо з проектування message flow: рисуємо граф контрактів та повідомлень між ними. На TON архітектурні рішення на цьому етапі дорожче переробок, ніж на EVM — тому що async model впливає на всі паттерни.
Розроблення займає 3-5 днів для одного контракту середної складності, до 2 тижнів для системи з кількох взаємодіючих контрактів. Деплой у mainnet — через Blueprint npx blueprint run, з перевіркою стану через tonapi.io.







