Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enhance jetton page #403

Closed
wants to merge 4 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 180 additions & 44 deletions pages/cookbook/jettons.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
a-bahdanau marked this conversation as resolved.
Show resolved Hide resolved

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.

Expand All @@ -25,12 +53,36 @@ Use [receiver](/book/receive) function to accept token notification message.

</Callout>

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.

<Callout type="warning" emoji="⚠️">

Sender of transfer notification must be validated!

</Callout>

```tact
import "@stdlib/deploy";

struct JettonWalletData {
balance: Int as coins;
ownerAddress: Address;
Expand All @@ -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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as I already said before in previous reviews, in most cases the solution for verification is to calculate that address offchain and then just add it into the stateinit of the contract itself.

calculating the address on-chain will only work if you're 100% sure in the jetton code and data structure, or if you take them as variable parameters in the stateinit

}

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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also highlight that these notifications are not guaranteed to be sent. The default implementation doesn't send notification if forwardAmount is zero, so in such cases you can't really rely on them.


// 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;
Expand All @@ -83,51 +146,124 @@ 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;
responseDestination: Address?;
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 {
Expand Down
Loading