diff --git a/pages/cookbook/jettons.mdx b/pages/cookbook/jettons.mdx index c1761d5d..6970095d 100644 --- a/pages/cookbook/jettons.mdx +++ b/pages/cookbook/jettons.mdx @@ -2,9 +2,37 @@ import { Callout } from 'nextra-theme-docs'; +## Overview + This page lists common examples of working with [jettons](https://docs.ton.org/develop/dapps/asset-processing/jettons). -## Accepting jetton transfer +Jettons are token standards on the TON (The Open Network) blockchain, designed for creating 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 specific token. 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 handles the creation process and distributes them to the relevant wallets. Similarly, it manages burning (destruction) of tokens when required. +* Supply Management: The Jetton Master keeps track of the total supply of the token, ensuring proper accounting for 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 their own unique Jetton Wallet Contract. Key features of the Jetton Wallet Contract include: + +* Balance Tracking: The wallet contract stores the token balance of the user. +* Token Transfers: The wallet is responsible for handling token transfers between users. When a user sends jettons, the Jetton Wallet Contract ensures the proper transfer and communication with the recipient's wallet. Jetton Master not included in this activity and does not create a bottleneck. Wallets can use TON sharding ability 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 only the owner of the wallet can initiate transfers or other token operations. + +## Examples + +### Accepting jetton transfer Transfer notification message have the following structure. @@ -25,12 +53,36 @@ Use [receiver](/book/receive) function to accept token notification message. -Validation can be done using jetton wallet state init and calculating jetton address. -Note, that notifications are coming from YOUR contract's jetton wallet, so [`myAddress()`](/ref/core-common#myaddress) should be used in owner address field. -Wallet initial data layout is shown below, but sometimes it can differ. -Note that `myJettonWalletAddress` may also be stored in contract storage to use less gas in every transaction. +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. + +```mermaid +graph LR +A[Sender] --1--> B(Sender's jetton wallet) +B --2--> C(Contract's jetton wallet) +C --3, 4--> D[Contract] +``` +1. Sender send message with `op::transfer` to his jetton wallet. +2. Jetton wallet transfer 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 jetton message. + +The calculation of the contract’s jetton wallet is done using the [`contractAddress(){:tact}`](/ref/core-common#contractaddress) function, which helps determine the address of the contract's jetton wallet. +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](#usdt-jetton-operations). + +Since notifications originate from your contract's jetton wallet, as illustrated in the diagram, the function [`myAddress(){:tact}`](/ref/core-common#myaddress) should be used in `ownerAddress{:tact}` field. + + + + Sender of transfer notification must be validated! + + ```tact +import "@stdlib/deploy"; + struct JettonWalletData { balance: Int as coins; ownerAddress: Address; @@ -49,30 +101,41 @@ fun calculateJettonWalletAddress(ownerAddress: Address, jettonMasterAddress: Add return contractAddress(StateInit{code: jettonWalletCode, data: initData.toCell()}); } -contract Sample { - jettonWalletCode: Cell; - jettonMasterAddress: Address; +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.jettonWalletCode = jettonWalletCode; - self.jettonMasterAddress = jettonMasterAddress; + self.myJettonWalletAddress = calculateJettonWalletAddress(myAddress(), jettonMasterAddress, jettonWalletCode); } receive(msg: JettonTransferNotification) { - let myJettonWalletAddress = calculateJettonWalletAddress(myAddress(), self.jettonMasterAddress, self.jettonWalletCode); - require(sender() == myJettonWalletAddress, "Notification not from your jetton wallet!"); + require(sender() == self.myJettonWalletAddress, "Notification not from your jetton wallet!"); - // your logic of processing token notification + self.myJettonAmount += msg.amount; + + // return excesses + self.forward(msg.sender, null, false, null); } } ``` -## Sending jetton transfer +### Sending jetton transfer + +A jetton transfer is the process of sending a specified amount of jettons (fungible tokens) from one wallet (contract) to another. To send jetton transfer use [`send(){:tact}`](/book/send) function. -Note that `myJettonWalletAddress` may also be stored in contract storage to use less gas in every transaction. ```tact +import "@stdlib/deploy"; + message(0xf8a7ea5) JettonTransfer { queryId: Int as uint64; amount: Int as coins; @@ -83,27 +146,65 @@ message(0xf8a7ea5) JettonTransfer { forwardPayload: Slice as remaining; } -receive("send") { - let myJettonWalletAddress = calculateJettonWalletAddress(myAddress(), self.jettonMasterAddress, self.jettonWalletCode); - send(SendParameters{ - to: myJettonWalletAddress, - value: ton("0.05"), - body: JettonTransfer{ - queryId: 42, - amount: jettonAmount, // jetton amount you want to transfer - destination: msg.userAddress, // address you want to transfer jettons. Note that this is address of jetton wallet owner, not jetton wallet itself - responseDestination: msg.userAddress, // address where to send a response with confirmation of a successful transfer and the rest of the incoming message Toncoins - customPayload: null, // in most cases will be null and can be omitted. Needed for custom logic on Jetton Wallets itself - forwardTonAmount: 1, // amount that will be transferred with JettonTransferNotification. Needed for custom logic execution like in example below. If the amount is 0 notification won't be sent - forwardPayload: rawSlice("F") // precomputed beginCell().storeUint(0xF, 4).endCell().beginParse(). This works for simple transfer, if needed any struct can be used as `forwardPayload` - }.toCell(), - }); +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{ + queryId: 42, + amount: msg.amount, // jetton amount you want to transfer + destination: sender(), // address you want to transfer jettons. Note that this is address of jetton wallet owner, not jetton wallet itself + responseDestination: sender(), // address where to send a response with confirmation of a successful transfer and the rest of the incoming message Toncoins + customPayload: null, // in most cases will be null and can be omitted. Needed for custom logic on Jetton Wallets itself + forwardTonAmount: 1, // amount that will be transferred with JettonTransferNotification. Needed for custom logic execution like in example below. If the amount is 0 notification won't be sent + forwardPayload: rawSlice("F") // precomputed beginCell().storeUint(0xF, 4).endCell().beginParse(). This works for simple transfer, if needed any struct can be used as `forwardPayload` + }.toCell(), + }); + } } ``` -## Burning jetton +### Burning jetton + +Jetton burning is the process of permanently removing a specified amount of jettons (fungible tokens) from circulation, with no possibility of recovery. ```tact +import "@stdlib/deploy"; + message(0x595f07bc) JettonBurn { queryId: Int as uint64; amount: Int as coins; @@ -111,23 +212,58 @@ message(0x595f07bc) JettonBurn { customPayload: Cell? = null; } -receive("burn") { - let myJettonWalletAddress = calculateJettonWalletAddress(myAddress(), self.jettonMasterAddress, self.jettonWalletCode); - send(SendParameters{ - to: myJettonWalletAddress, - body: JettonBurn{ - queryId: 42, - amount: jettonAmount, // jetton amount you want to burn - responseDestination: someAddress, // address where to send a response with confirmation of a successful burn and the rest of the incoming message coins - customPayload: null, // in most cases will be null and can be omitted. Needed for custom logic on jettons itself - }.toCell(), - }); +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{ + queryId: 42, + amount: msg.amount, // jetton amount you want to burn + responseDestination: sender(), // address where to send a response with confirmation of a successful burn and the rest of the incoming message coins + customPayload: null, // in most cases will be null and can be omitted. Needed for custom logic on jettons itself + }.toCell(), + }); + } } ``` -## USDT jetton operations +### USDT jetton operations -Operations with USDT (on TON) remain the same, except that the `JettonWalletData` will have the following structure: +Operations with USDT (on TON) remain the same, except that the `JettonWalletData` will have the following structure: ```tact struct JettonWalletData {