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 { // Unique identifier used to trace transactions across multiple contracts // Defaults to 0, which means we don't mark messages to trace their chains queryId: Int as uint64 = 0;
// Amount of Jettons transferred amount: Int as coins;
// Address of the sender of the Jettons sender: Address;
// Optional custom payload forwardPayload: Slice as remaining;}
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:
- Sender sends message with
0xf8a7ea5
as its 32-bit header (opcode) to his Jetton wallet. - Jetton wallet transfers funds to contract’s Jetton wallet.
- After successful transfer accept, contract’s Jetton wallet sends transfer notification to his owner contract.
- 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); }}
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{ // Unique identifier used to trace transactions across multiple contracts 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(), }); }}
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{ // Unique identifier used to trace transactions across multiple contracts 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(), }); }}
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(), });}
Onchain metadata creation
/// https://github.com/ton-blockchain/TEPs/blob/master/text/0064-token-data-standard.md#jetton-metadata-example-offchainfun composeJettonMetadata( name: String, // full name description: String, // text description of the Jetton symbol: String, // "stock ticker" symbol without the $ prefix, like USDT or SCALE image: String, // link to the image // There could be other data, see: // https://github.com/ton-blockchain/TEPs/blob/master/text/0064-token-data-standard.md#jetton-metadata-attributes): Cell { let dict: map<Int as uint256, Cell> = emptyMap(); dict.set(sha256("name"), name.asMetadataCell()); dict.set(sha256("description"), description.asMetadataCell()); dict.set(sha256("symbol"), symbol.asMetadataCell()); dict.set(sha256("image"), image.asMetadataCell());
return beginCell() .storeUint(0, 8) // a null byte prefix .storeMaybeRef(dict.asCell()!!) // 1 as a single bit, then a reference .endCell();}
// Taking flight!fun poorMansLaunchPad() { let jettonMetadata = composeJettonMetadata( "Best Jetton", "A very descriptive description describing the jetton descriptively", "JETTON", "...link to ipfs or somewhere trusted...", );}
// Prefixes the String with a single null byte and converts it to a Cell// The null byte prefix is used to express metadata in various standards, like NFT or Jettoninline extends fun asMetadataCell(self: String): Cell { return beginTailString().concat(self).toCell();}