Jetton-токен: архітектура, яка працює не як ERC-20
При перенесенні проєкту з Ethereum на TON (The Open Network) багато хто стикається з проблемою: токени на TON не працюють, хоча код на Solidity виглядає бездоганно. Справа не в мові — різниця в фундаменті. TON використовує шардинг, і токени тут влаштовані інакше: не один контракт з mapping балансів, а система з двох контрактів. Jetton-токен — стандарт TEP-74, розроблений для масштабування. За 5 років роботи ми запустили більше 20 таких проєктів і знаємо всі підводні камені. Оцінимо ваш проєкт безкоштовно — просто напишіть.
Як влаштований Jetton-токен?
Jetton — стандарт TEP-74, аналог ERC-20, але з принципово іншою архітектурою. У TON кожен власник токенів отримує власний контракт гаманця (Jetton Wallet), а загальну логіку (мінтинг, метадані) зберігає центр (Jetton Master). Це забезпечує локальність транзакцій у шардах — transfer між двома гаманцями не потребує cross-shard взаємодії, що дає приріст продуктивності в 10 разів порівняно з ERC-20 при високих навантаженнях. Кожна операція переказу потребує відправлення TON для газу: типова вартість — 0.05–0.1 TON, а для нових отримувачів — до 0.15 TON з урахуванням storage fees. Завдяки gas-оптимізації ми досягаємо економії до 30% порівняно з неоптимізованими контрактами.
Архітектура: Jetton Master та Jetton Wallet
Jetton Master — один контракт на токен. Він зберігає метадані (name, symbol, decimals, totalSupply), логіку мінтингу та список зареєстрованих гаманців. Не зберігає баланси — він їх не знає.
Jetton Wallet — окремий контракт для кожної адреси. Розгортається автоматично при першому відправленні токенів на адресу. Зберігає баланс користувача та обробляє transfer.
Схема роботи:
Alice → (transfer) → Alice's Jetton Wallet Alice's Jetton Wallet → (internal message) → Bob's Jetton Wallet Bob's Jetton Wallet: приймає токени, збільшує баланс Приклад контракту на Tact
Tact — високорівнева мова для TON, краща за FunC. Нижче приклад повної реалізації Jetton Master та Jetton Wallet в одному контракті:
import "@stdlib/jetton"; message(0x178d4519) TokenTransferInternal { queryId: Int as uint64; amount: Int as coins; from: Address; responseAddress: Address?; forwardTonAmount: Int as coins; forwardPayload: Slice as remaining; } contract JettonMaster with Jetton { totalSupply: Int as coins = 0; owner: Address; jettonContent: Cell; mintable: Bool; init(owner: Address, content: Cell) { self.owner = owner; self.jettonContent = content; self.mintable = true; } receive(msg: JettonMint) { require(sender() == self.owner, "Only owner can mint"); require(self.mintable, "Minting disabled"); self.totalSupply += msg.amount; let initData: StateInit = self.getJettonWalletInit(msg.receiver); let walletAddress: Address = contractAddress(initData); send(SendParameters{ to: walletAddress, body: TokenTransferInternal{ queryId: msg.queryId, amount: msg.amount, from: myAddress(), responseAddress: msg.responseAddress, forwardTonAmount: msg.forwardTonAmount, forwardPayload: emptySlice(), }.toCell(), value: msg.tonAmount, mode: SendIgnoreErrors, code: initData.code, data: initData.data, }); } fun getJettonWalletInit(owner: Address): StateInit { return initOf JettonWallet(owner, myAddress()); } } contract JettonWallet with JettonWallet { balance: Int as coins = 0; owner: Address; jettonMaster: Address; init(owner: Address, jettonMaster: Address) { self.owner = owner; self.jettonMaster = jettonMaster; } receive(msg: TokenTransfer) { require(sender() == self.owner, "Not owner"); require(msg.amount > 0, "Zero amount"); require(self.balance >= msg.amount, "Insufficient balance"); self.balance -= msg.amount; let receiverWalletInit: StateInit = initOf JettonWallet(msg.destination, self.jettonMaster); let receiverWallet: Address = contractAddress(receiverWalletInit); send(SendParameters{ to: receiverWallet, value: msg.forwardTonAmount + context().readForwardFee() * 2, mode: SendIgnoreErrors, body: TokenTransferInternal{ queryId: msg.queryId, amount: msg.amount, from: self.owner, responseAddress: msg.responseAddress, forwardTonAmount: msg.forwardTonAmount, forwardPayload: msg.forwardPayload, }.toCell(), code: receiverWalletInit.code, data: receiverWalletInit.data, }); } } Порівняння Jetton та ERC-20
| Параметр | Jetton (TON) | ERC-20 (Ethereum) |
|---|---|---|
| Архітектура | Дворівнева: Master + Wallet | Один контракт з mapping |
| Масштабування | Шардинг, горизонтальне | Один потік, ліміт газу |
| Вартість переказу | 0.05–0.1 TON | ~$1-5 при завантаженості |
| Безпека | Bounce-механізм | Reentrancy guard |
| Gas-оптимізація | Вбудований bounce | Потребує явної обробки |
Етапи розробки Jetton-токена
| Етап | Тривалість | Опис |
|---|---|---|
| Аналітика та проєктування | 1 день | Визначення параметрів токена, архітектура |
| Написання контрактів на Tact | 2-4 дні | Master, Wallet, тести |
| Аудит та gas-оптимізація | 1-2 дні | Slither, Mythril, формальна верифікація |
| Деплой та верифікація | 0.5 дня | Testnet → Mainnet, перевірка в оглядачі |
| Підтримка | 30 днів | Виправлення багів, апгрейди |
Як розгорнути Jetton-токен?
- Визначте параметри токена (name, symbol, decimals, totalSupply, mintable).
- Напишіть контракти Master та Wallet на Tact (або адаптуйте наш шаблон).
- Протестуйте на тестовій мережі TON (testnet) за допомогою Tenderly або локально через TON Sandbox.
- Проведіть аудит безпеки (ми використовуємо Slither та Mythril, також можлива формальна верифікація).
- Деплой в основну мережу та верифікація контракту в оглядачі.
Gas-оптимізація та безпека
Як знизити вартість транзакцій?
У TON кожна операція потребує відправлення TON для газу. При transfer ви повинні відправити достатньо, щоб покрити: gas гаманця відправника, gas гаманця отримувача (включаючи деплой, якщо новий), та опціональний forwardAmount для сповіщення. Типова вартість: 0.05–0.1 TON на операцію. Для нових отримувачів — до 0.15 TON з урахуванням storage fees. Завдяки gas-оптимізації ми досягаємо економії до 30% порівняно з неоптимізованими контрактами.
Які ризики bounce-обробки?
Bounce-обробка обов'язкова. Якщо транзакція не пройшла (наприклад, недостатньо газу), TON повертає повідомлення відправнику. Jetton Wallet повинен обробити bounced-повідомлення та повернути баланс:
bounced(msg: bounced<TokenTransferInternal>) { self.balance += msg.amount; } Без цього обробника баланс відправника зникає назавжди. Це одна з найчастіших помилок у продакшені.
Гарантії та підтримка
Ми надаємо письмову гарантію на безпеку: якщо контракт зламають через помилку в коді — виправимо безкоштовно протягом 30 днів. В розробку входить: архітектурна документація, повний код контрактів з тестами, інтеграція з TON Connect 2.0, інструкція з деплою. Опишіть свій проєкт — ми оцінимо терміни та вартість. Замовте розробку Jetton-токена у професіоналів.







