Skip to content

Fungible Tokens (Jettons)

This page lists common examples of working with Fungible Tokens (Jettons).

Jettons are token standards on the TON Blockchain, designed to create fungible tokens (similar to ERC-20 on Ethereum) with a decentralized approach. They are implemented as a pair of smart contracts, typically consisting of two core components:

  • Jetton Master Contract (Jetton master)
  • Jetton Wallet Contract (Jetton wallet)

These contracts interact with each other to manage token supply, distribution, transfers, and other operations related to the Jetton.

Jetton Master Contract

The Jetton Master Contract serves as the central entity for a given Jetton. It maintains critical information about the Jetton itself. Key responsibilities and data stored in the Jetton Master Contract include:

  • Jetton metadata: Information such as the token’s name, symbol, total supply, and decimals.

  • Minting and Burning: When new Jettons are minted (created), the Jetton Master manages the creation process and distributes them to the appropriate wallets. It also manages the burning (destruction) of tokens as needed.

  • Supply Management: The Jetton Master keeps track of the total supply of Jettons and ensures proper accounting of all issued Jettons.

Jetton Wallet Contract

The Jetton Wallet Contract represents an individual holder’s token wallet and is responsible for managing the balance and token-related operations for a specific user. Each user or entity holding Jettons will have its own unique Jetton Wallet Contract. Key features of the Jetton Wallet Contract include:

  • Balance tracking: The wallet contract stores the user’s token balance.

  • Token Transfers: The wallet is responsible for handling token transfers between users. When a user sends Jettons, the Jetton Wallet Contract ensures proper transfer and communication with the recipient’s wallet. The Jetton Master is not involved in this activity and does not create a bottleneck. Wallets can use TON’s sharding capability in a great way

  • Token burning: The Jetton Wallet interacts with the Jetton Master to burn tokens.

  • Owner control: The wallet contract is owned and controlled by a specific user, meaning that only the owner of the wallet can initiate transfers or other token operations.

Examples

Common examples of working with Jettons.

Accepting Jetton transfer

Transfer notification message have the following structure.

message(0x7362d09c) JettonTransferNotification {
queryId: Int as uint64;
amount: Int as coins;
sender: Address;
forwardPayload: Slice as remaining;
}
▶️ Open in Web IDE

Use receiver function to accept token notification message.

The sender of a transfer notification must be validated because malicious actors could attempt to spoof notifications from an unauthorized account. If this validation is not done, the contract may accept unauthorized transactions, leading to potential security vulnerabilities.

Validation is done using the Jetton address from the contract:

  1. Sender sends message with 0xf8a7ea5 as its 32-bit header (opcode) to his Jetton wallet.
  2. Jetton wallet transfers funds to contract’s Jetton wallet.
  3. After successful transfer accept, contract’s Jetton wallet sends transfer notification to his owner contract.
  4. Contract validates the Jetton message.

You may obtain contract’s Jetton wallet is done using the contractAddress() function or calculate this address offchain.

To obtain the Jetton wallet’s state init, you need the wallet’s data and code. While there is a common structure for the initial data layout, it may differ in some cases, such as with USDT.

Since notifications originate from your contract’s Jetton wallet, the function myAddress() should be used in ownerAddress field.

import "@stdlib/deploy";
struct JettonWalletData {
balance: Int as coins;
ownerAddress: Address;
jettonMasterAddress: Address;
jettonWalletCode: Cell;
}
fun calculateJettonWalletAddress(
ownerAddress: Address,
jettonMasterAddress: Address,
jettonWalletCode: Cell
): Address {
let initData = JettonWalletData{
balance: 0,
ownerAddress,
jettonMasterAddress,
jettonWalletCode,
};
return contractAddress(StateInit{
code: jettonWalletCode,
data: initData.toCell(),
});
}
message(0x7362d09c) JettonTransferNotification {
queryId: Int as uint64;
amount: Int as coins;
sender: Address;
forwardPayload: Slice as remaining;
}
contract Example with Deployable {
myJettonWalletAddress: Address;
myJettonAmount: Int as coins = 0;
init(jettonWalletCode: Cell, jettonMasterAddress: Address) {
self.myJettonWalletAddress = calculateJettonWalletAddress(
myAddress(),
jettonMasterAddress,
jettonWalletCode,
);
}
receive(msg: JettonTransferNotification) {
require(
sender() == self.myJettonWalletAddress,
"Notification not from your jetton wallet!",
);
self.myJettonAmount += msg.amount;
// Forward excesses
self.forward(msg.sender, null, false, null);
}
}
▶️ Open in Web IDE

Sending Jetton transfer

A Jetton transfer is the process of sending a specified amount of Jettons from one wallet (contract) to another.

To send Jetton transfer use send() function.

import "@stdlib/deploy";
message(0xf8a7ea5) JettonTransfer {
queryId: Int as uint64;
amount: Int as coins;
destination: Address;
responseDestination: Address?;
customPayload: Cell? = null;
forwardTonAmount: Int as coins;
forwardPayload: Slice as remaining;
}
const JettonTransferGas: Int = ton("0.05");
struct JettonWalletData {
balance: Int as coins;
ownerAddress: Address;
jettonMasterAddress: Address;
jettonWalletCode: Cell;
}
fun calculateJettonWalletAddress(
ownerAddress: Address,
jettonMasterAddress: Address,
jettonWalletCode: Cell,
): Address {
let initData = JettonWalletData{
balance: 0,
ownerAddress,
jettonMasterAddress,
jettonWalletCode,
};
return contractAddress(StateInit{
code: jettonWalletCode,
data: initData.toCell(),
});
}
message Withdraw {
amount: Int as coins;
}
contract Example with Deployable {
myJettonWalletAddress: Address;
myJettonAmount: Int as coins = 0;
init(jettonWalletCode: Cell, jettonMasterAddress: Address) {
self.myJettonWalletAddress = calculateJettonWalletAddress(
myAddress(),
jettonMasterAddress,
jettonWalletCode,
);
}
receive(msg: Withdraw) {
require(
msg.amount <= self.myJettonAmount,
"Not enough funds to withdraw"
);
send(SendParameters{
to: self.myJettonWalletAddress,
value: JettonTransferGas,
body: JettonTransfer{
// To prevent replay attacks
queryId: 42,
// Jetton amount to transfer
amount: msg.amount,
// Where to transfer Jettons:
// this is an address of the Jetton wallet
// owner and not the Jetton wallet itself
destination: sender(),
// Where to send a confirmation notice of a successful transfer
// and the rest of the incoming message value
responseDestination: sender(),
// Can be used for custom logic of Jettons themselves,
// and without such can be set to null
customPayload: null,
// Amount to transfer with JettonTransferNotification,
// which is needed for the execution of custom logic
forwardTonAmount: 1, // if its 0, the notification won't be sent!
// Compile-time way of expressing:
// beginCell().storeUint(0xF, 4).endCell().beginParse()
// For more complicated transfers, adjust accordingly
forwardPayload: rawSlice("F")
}.toCell(),
});
}
}
▶️ Open in Web IDE

Burning Jetton

Jetton burning is the process of permanently removing a specified amount of Jettons from circulation, with no possibility of recovery.

import "@stdlib/deploy";
message(0x595f07bc) JettonBurn {
queryId: Int as uint64;
amount: Int as coins;
responseDestination: Address?;
customPayload: Cell? = null;
}
const JettonBurnGas: Int = ton("0.05");
struct JettonWalletData {
balance: Int as coins;
ownerAddress: Address;
jettonMasterAddress: Address;
jettonWalletCode: Cell;
}
fun calculateJettonWalletAddress(
ownerAddress: Address,
jettonMasterAddress: Address,
jettonWalletCode: Cell,
): Address {
let initData = JettonWalletData{
balance: 0,
ownerAddress,
jettonMasterAddress,
jettonWalletCode,
};
return contractAddress(StateInit{
code: jettonWalletCode,
data: initData.toCell(),
});
}
message ThrowAway {
amount: Int as coins;
}
contract Example with Deployable {
myJettonWalletAddress: Address;
myJettonAmount: Int as coins = 0;
init(jettonWalletCode: Cell, jettonMasterAddress: Address) {
self.myJettonWalletAddress = calculateJettonWalletAddress(
myAddress(),
jettonMasterAddress,
jettonWalletCode,
);
}
receive(msg: ThrowAway) {
require(
msg.amount <= self.myJettonAmount,
"Not enough funds to throw away",
);
send(SendParameters{
to: self.myJettonWalletAddress,
value: JettonBurnGas,
body: JettonBurn{
// To prevent replay attacks
queryId: 42,
// Jetton amount you want to burn
amount: msg.amount,
// Where to send a confirmation notice of a successful burn
// and the rest of the incoming message value
responseDestination: sender(),
// Can be used for custom logic of Jettons themselves,
// and without such can be set to null
customPayload: null,
}.toCell(),
});
}
}
▶️ Open in Web IDE

USDT Jetton operations

Operations with USDT (on TON) remain the same, except that the JettonWalletData will have the following structure:

struct JettonWalletData {
status: Int as uint4;
balance: Int as coins;
ownerAddress: Address;
jettonMasterAddress: Address;
}
// And the function to calculate the wallet address may look like this:
fun calculateJettonWalletAddress(
ownerAddress: Address,
jettonMasterAddress: Address,
jettonWalletCode: Cell
): Address {
let initData = JettonWalletData{
status: 0,
balance: 0,
ownerAddress,
jettonMasterAddress,
};
return contractAddress(StateInit{
code: jettonWalletCode,
data: initData.toCell(),
});
}
▶️ Open in Web IDE