Skip to content
Open
Changes from 1 commit
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
148 changes: 97 additions & 51 deletions contract-dev/techniques/on-chain-jetton-processing.mdx
Original file line number Diff line number Diff line change
@@ -1,38 +1,52 @@
---
title: "On-chain Jetton processing"
title: "On-chain jetton processing"
sidebarTitle: "Jetton processing"
Comment on lines 1 to 3
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[HIGH] Page title lowercases proper noun “Jetton”

In the frontmatter, the updated title uses “On-chain jetton processing”, which lowercases “Jetton” even though it is treated as a canonical proper noun in the docs and terminology. This violates the style guide’s requirement to preserve capitalization for named standards and branded terms while otherwise using sentence case for headings. Keeping “Jetton” capitalized in the page title preserves consistency with the terminology section and with other occurrences in this document.

Please leave a reaction 👍/👎 to this suggestion to improve future reviews for everyone!

---

import { Aside } from '/snippets/aside.jsx';

<Aside>
Read this article after understanding the [Jetton](/standard/tokens/jettons/overview) architecture.
<Aside type="note">
Understand [the jetton architecture](/standard/tokens/jettons/overview) before reading this article.
</Aside>

Contracts routinely process jettons for DEX, escrow, and lending protocols. Each contract must determine which jetton type arrived when a `JettonNotify` message shows up.
Jetton contracts use the `JettonNotify` message to notify receiving contracts of incoming jetton transfers, allowing them to react by crediting user accounts or triggering other actions. However, since anyone can send a `JettonNotify` message, a contracts must verify that the message comes from the correct jetton wallet address.
Comment thread
kay-is marked this conversation as resolved.
Outdated
Comment thread
kay-is marked this conversation as resolved.
Outdated
Comment thread
kay-is marked this conversation as resolved.
Outdated

```mermaid
graph LR
A((Jetton Wallet)) -->|JettonNotify 0x7362d09c| B((Contract))
C((Attacker)) --x|JettonNotify 0x7362d09c| B((Contract))
Comment thread
kay-is marked this conversation as resolved.
Outdated
```

## Known Jetton or trusted deployer
## Comparison of approaches

When someone does a Jetton transfer, they deploy a new `JettonWallet`, that has an address depending on the address of `JettonMinter` and the address of the receiver, and then send a message with a type `JettonNotify` to the regular wallet (or other kind of contract) that tells the transfer succeeded. The attacker might send a `JettonNotify` message from a contract that is not a valid `JettonWallet`, so handling it requires care.
There are three approaches to verify `JettonNotify` messages, depending on the level of trust in the contract deployer and the jetton's compatibility with TEP-89.

To handle the `JettonNotify` message, it's preferable to precompute the address of the `JettonWallet` for a specific minter (i.e. Jetton type) in advance, so that the validation only consists of checking two addresses for equality.
| Factor | [Manual management](#manual-wallet-management) | [Automatic discovery](#automatic-wallet-discovery) | [Get-method emulation](#on-chain-get-method-emulation) |
| -------------------- | ---------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------ |
| Jetton type known | Yes | No | No |
| Trust in deployer | High | Medium | Low |
| TEP-89 compatibility | No | Yes | No |
| Complexity | Low | Medium | High |
Comment thread
kay-is marked this conversation as resolved.
Outdated

Here is an example of a contract that handles `JettonNotify` messages. It's deployed along with a `SetTrustedJettonWallet` message with an address of `JettonWallet` that is owned by this same contract.
## Manual wallet management

When deploying with this pattern, [calculate](/standard/tokens/jettons/find) the Jetton wallet address that must receive top-ups. It is impossible to hardcode the jetton wallet address in the contract's [`StateInit`](/foundations/messages/deploy), because the wallet address depends on the contract address and creates a circular dependency.
If the receiving contract knows the jetton type in advance, it must only check that the message comes from the correct jetton wallet address. This is the most efficient approach, because it only requires one address comparison.

To handle several Jetton types with a single contract, `trustedJettonWallet` should be modified to store several addresses.
The following example of a receiving contract handles `JettonNotify` messages for a trusted jetton wallet address that can be set by the owner of the contract.

The `jettonMinter` address is included in `Storage`, because otherwise every instance of this contract owned by the same owner will deploy to the same address, even if there was an intent to have them with different `trustedJettonWallet`s.

This approach is used by [Bidask](https://bidask.finance/en/).
```mermaid
sequenceDiagram
participant Owner
participant Contract
participant JettonWallet

Owner-->>Contract: Deploy with minter address
JettonWallet-xContract: JettonNotify
Owner-)Contract: SetTrustedJettonWallet
JettonWallet-)Contract: JettonNotify
Comment thread
kay-is marked this conversation as resolved.
```

```tolk expandable
```tolk title="Tolk" expandable
tolk 1.2;

struct Storage {
Expand Down Expand Up @@ -84,31 +98,44 @@ fun onInternalMessage(in: InMessage) {
}
```

## Unknown TEP-89 Jetton or untrusted deployer
This contract uses `SetTrustedJettonWallet` messages to set a trusted jetton wallet address that shares an owner with the receiving contract.
Comment thread
kay-is marked this conversation as resolved.
Outdated

When deploying with this pattern, [calculate the jetton wallet address](/standard/tokens/jettons/find) that must receive top-ups. It is impossible to hardcode the jetton wallet address in the contract's [`StateInit`](/foundations/messages/deploy), because the wallet address depends on the contract address and creates a circular dependency.

Including `jettonMinter` in the contracts `Storage` ensures that each instance of the contract is unique to a specific jetton, and thus has a unique address. This allows deploying multiple instances of the contract for different jettons, even if they share the same owner. Each instance can then set its own `trustedJettonWallet` address corresponding to its `jettonMinter`.
Comment thread
kay-is marked this conversation as resolved.
Outdated

Modify `trustedJettonWallet` to store several addresses, to handle several jetton types with a single contract.

<Aside type="tip">
[The Bidask Finance DEX](https://bidask.finance/en/) uses manual wallet management.
</Aside>

## Automatic wallet discovery

In the previous example, addresses of `jettonMinter` and `trustedJettonWallet` are chosen off-chain by a deploying party. Someone who depends on this contract has to trust that the wallet belongs to this minter, which is not a problem, for example, for an exchange. If deployer might be monetarily interested to break this invariant, the previous approach is not suitable, and the address of the `trustedJettonWallet` address must be computed on-chain.
In the first approach, the deploying party chooses addresses of the jetton minter and wallet contracts off-chain. The contract's dependents have to trust that the wallet belongs to the corresponding minter, which is not a problem for an exchange. However, if the deployer is not trustworthy, the first approach is not suitable, and the contract must compute the `trustedJettonWallet` address on-chain.

Most modern Jettons implement [TEP-89](https://github.com/ton-blockchain/TEPs/blob/66675fc7ecda1e3dc1524159d6bfcaa2ed2372fe/text/0089-jetton-wallet-discovery.md), that defines a message `provide_wallet_address` that requests a Jetton wallet address for an arbitrary owner, and `take_wallet_address` message with a response.
Most modern jetton contracts implement [TEP-89](https://github.com/ton-blockchain/TEPs/blob/66675fc7ecda1e3dc1524159d6bfcaa2ed2372fe/text/0089-jetton-wallet-discovery.md), which defines a `provide_wallet_address` message that requests a jetton wallet address for an arbitrary owner, and a `take_wallet_address` message with a response.

The contract works similarly to the example above, except it's deployed along with a `InitContract` message, that asks a `JettonMinter` what `jettonWalletAddress` should be.
The receiving contract works similarly to the first approach, except it's deployed along with an `InitContract` message, that asks the jetton minter which jetton wallet address it should use.
Comment thread
kay-is marked this conversation as resolved.
Outdated

This approach is used by [DeDust](https://dedust.io/).
The following example of a receiving contract handles `JettonNotify` messages for a jetton wallet address that is unknown at the moment of deployment, and is set by the `JettonMinter` in response to the `ProvideWalletAddress` message sent during initialization.
Comment thread
kay-is marked this conversation as resolved.
Outdated

```mermaid
graph LR
A((Deployer))
B((Contract))
C((Jetton minter))
D((Contract))

A -->|InitContract| B
B -->|ProvideWalletAddress| C
C -->|TakeWalletAddress| D
sequenceDiagram
participant Owner
participant Contract
participant JettonWallet
participant JettonMinter

Owner-->>Contract: Deploy with minter address
JettonWallet-xContract: JettonNotify
Owner-)Contract: InitContract
Contract-)JettonMinter: ProvideWalletAddress
JettonMinter-)Contract: TakeWalletAddress
JettonWallet-)Contract: JettonNotify
Comment thread
kay-is marked this conversation as resolved.
```

The contract that initializes the jetton wallet with this pattern can look like this:

```tolk expandable
```tolk title="Tolk" expandable
tolk 1.2

struct Storage {
Expand Down Expand Up @@ -185,17 +212,36 @@ fun onInternalMessage(in: InMessage) {
}
```

## Unknown TEP-74 Jetton and untrusted deployer
<Aside type="tip">
[The DeDust DEX](https://dedust.io/) uses automatic wallet discovery.
</Aside>

## On-chain get-method emulation

Jettons that do not have TEP-89 methods for computing a wallet address on-chain are rare. [TONCO DEX](https://tonco.io/) rejects them, while platforms such as [DeDust](https://dedust.io/) allow them after a manual approval flow. There are two approaches, both using get-method emulation.
If a jetton contract doesn't implement TEP-89, it is possible to compute the jetton wallet address on-chain by executing the `get_wallet_address` method of the jetton minter. This approach is more complex than the previous ones, but it doesn't require any trust in the jetton deployer and works for jettons that don't implement TEP-89.
Comment thread
kay-is marked this conversation as resolved.
Outdated

### Running get-method on-chain
<Aside type="note">
Jettons that do not implement the TEP-89 methods for computing a wallet address on-chain are rare. [TONCO DEX](https://tonco.io/) rejects them, while platforms such as [DeDust](https://dedust.io/) only allows them after a manual approval.
Comment thread
kay-is marked this conversation as resolved.
Outdated
Comment thread
kay-is marked this conversation as resolved.
Outdated
Comment thread
kay-is marked this conversation as resolved.
Outdated
</Aside>

With the state of a jetton minter, the [RUNVM](/tvm/instructions#db4-runvm) instruction can emulate the execution of the `get_wallet_address` [get-method](/tvm/get-method) to derive the wallet address for any owner.

Given a Jetton minter state, we can emulate execution of the minter on-chain via [RUNVM](/tvm/instructions#db4-runvm) instruction. During the emulation, it calls the `get_wallet_address` [get-method](/tvm/get-method) to derive the wallet address for any owner.
The following helper function uses [Fift](/languages/fift/overview), because it's impossible to assign a type to `c7`. It executes the `get_wallet_address` method of a jetton minter on-chain, and calculates the corresponding wallet address for a given owner.
Comment thread
kay-is marked this conversation as resolved.
Outdated

This helper uses [Fift](/languages/fift/overview), because it's impossible to assign a type to `c7`.
```mermaid
sequenceDiagram
participant Owner
participant Contract
participant JettonWallet
participant JettonMinter

Owner-->>Contract: deploy with minter address
JettonWallet-xContract: JettonNotify
Contract<<-->>JettonMinter: get_wallet_address(owner)
JettonWallet-)Contract: JettonNotify
Comment thread
kay-is marked this conversation as resolved.
```

```tolk expandable
```tolk title="Tolk/Fift" expandable
fun calculateJettonWallet(owner: address, jettonData: cell, jettonCode: cell, jettonMinter: address): address asm """
c7 PUSHCTR
// Unpack environment information from c7
Expand All @@ -222,27 +268,27 @@ fun calculateJettonWallet(owner: address, jettonData: cell, jettonCode: cell, je
// +4 = load c4 (persistent data) from stack and return its final value
// +16 = load c7 (smart-contract context)
// +256 = pop number N, return exactly N values from stack (only if res=0 or 1; if not enough then res=stk_und)
// Mode 256 is crucial, because it ignores all stack values except the first one, and protects us from stack poisoning
// Mode 256 is crucial, because it ignores all stack values except the first one, and protects from stack poisoning
Comment thread
kay-is marked this conversation as resolved.
277 RUNVM // address exit_code c4'"
2 BLKDROP // address";
"""
```

### Approach 1: non-vanity addresses
### Minters without vanity addresses

If the `JettonMinter` was not deployed with a [vanity](/contract-dev/vanity) contract, address and `StateInit` match. The logic for the contract that handles `JettonNotify` messages is thus
Minter address and `StateInit` will match if the jetton minter was not deployed with [a vanity contract](/contract-dev/vanity). The logic for the contract that handles `JettonNotify` messages is thus:

1. get the `StateInit` of `JettonMinter` off-chain;
1. deploy the contract with the address of `JettonMinter` in its data;
1. send a message to the contract with the `StateInit` of `JettonMinter`;
1. contract validates that this `StateInit` matches the address of the `JettonMinter`;
1. contract runs `calculateJettonWallet`
- `owner` - address of the contract (`contract.getAddress()`);
- `jettonData`, `jettonCode` - `StateInit` of `JettonMinter`;
- `jettonMinter` - address of `JettonMinter`.
1. Get the `StateInit` of the jetton minter off-chain.
1. Deploy the receiver contract with the address of the jetton minter in its data.
1. Send a message to the contract with the `StateInit` of the jetton minter.
1. Validate that `StateInit` matches the address of the jetton minter.
1. Run `calculateJettonWallet` with the following arguments:
- `owner`: address of the contract (e.g., `contract.getAddress()`).
- `jettonData` and `jettonCode`: `StateInit` of the jetton minter.
- `jettonMinter`: address of the jetton minter.
Comment thread
kay-is marked this conversation as resolved.

### Approach 2: where the previous method fails
### Minters with vanity addresses or invalid get-methods

If the `JettonMinter` was deployed with a vanity contract, or otherwise lacks `get_wallet_address` method, or `get_wallet_address` returns incorrect addresses, we have to use the current state of the contract instead.
If the jetton minter was deployed with a vanity contract, or otherwise lacks `get_wallet_address` method, or `get_wallet_address` returns incorrect addresses, use the current state of the contract instead.

To prove that the state is currently at the Jetton minter address, a full [state proof](/foundations/proofs/overview) is required.
A full [state proof](/foundations/proofs/overview) is required to prove that the state is currently at the jetton minter address.
Loading