Skip to content

DeDust.io

DeDust is a decentralized exchange (DEX) and automated market maker (AMM) built natively on TON Blockchain and DeDust Protocol 2.0. DeDust is designed with meticulous attention to user experience (UX), gas efficiency, and extensibility.

Before going further, familiarize yourself with the following:

Swaps

Read more about swaps in the DeDust documentation.

All kinds of swaps use the SwapStep and SwapParams structures:

/// https://docs.dedust.io/reference/tlb-schemes#swapstep
struct SwapStep {
// The pool that will perform the swap, e.g., pairs like TON/USDT or USDT/DUST
poolAddress: Address;
// The kind of swap to do: can only be 0 as of now
kind: Int as uint1 = 0;
// Minimum output of the swap
// If the actual value is less than specified, the swap will be rejected
limit: Int as coins = 0;
// Reference to the next step, which can be used for multi-hop swaps
// The type here is actually `SwapStep?`,
// but specifying recursive types isn't allowed in Tact yet
nextStep: Cell?;
}
/// https://docs.dedust.io/reference/tlb-schemes#swapparams
struct SwapParams {
// Specifies a deadline to reject the swap if it arrives at the pool late
// Accepts the number of seconds passed since the UNIX Epoch
// Defaults to 0, which removes the deadline
deadline: Int as uint32 = 0;
// Specifies an address where funds will be sent after the swap
// Defaults to `null`, causing the swap to use the sender's address
recipientAddress: Address? = null;
// Referral address required for the DeDust referral program
// Defaults to `null`
referralAddress: Address? = null;
// Custom payload that will be attached to the fund transfer upon a successful swap
// Defaults to `null`
fulfillPayload: Cell? = null;
// Custom payload that will be attached to the fund transfer upon a rejected swap
// Defaults to `null`
rejectPayload: Cell? = null;
}
▶️ Open in Web IDE

Swap Toncoin for any Jetton

/// https://docs.dedust.io/reference/tlb-schemes#message-swap
message(0xea06185d) NativeSwap {
// 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;
// Toncoin amount for the swap
amount: Int as coins;
// Inlined fields of SwapStep struct
poolAddress: Address;
kind: Int as uint1 = 0;
limit: Int as coins = 0;
nextStep: SwapStep? = null;
// Set of parameters relevant for the whole swap
swapParams: SwapParams;
}
// Let's say `swapAmount` is `ton("0.1")`, which is 10000000 nanoToncoins
fun swapToncoinForUSDT(swapAmount: Int) {
send(SendParameters{
// Address of the TON vault to send the message to
to: address("EQDa4VOnTYlLvDJ0gZjNYm5PXfSmmtL6Vs6A_CZEtXCNICq_"),
// Amount to swap plus a trade fee
value: swapAmount + ton("0.2"),
body: NativeSwap{
amount: swapAmount,
// Address of the swap pool, which is the TON/USDT pair in this case
poolAddress: address("EQA-X_yo3fzzbDbJ_0bzFWKqtRuZFIRa1sJsveZJ1YpViO3r"),
// Set of parameters relevant for the whole swap
swapParams: SwapParams{}, // use defaults
}.toCell(),
});
}
//
// Helper structs described earlier on this page
//
struct SwapStep {
poolAddress: Address;
kind: Int as uint1 = 0;
limit: Int as coins = 0;
nextStep: Cell?;
}
struct SwapParams {
deadline: Int as uint32 = 0;
recipientAddress: Address? = null;
referralAddress: Address? = null;
fulfillPayload: Cell? = null;
rejectPayload: Cell? = null;
}
▶️ Open in Web IDE

Swap a Jetton for another Jetton or Toncoin

/// https://docs.dedust.io/reference/tlb-schemes#message-swap-1
message(0xe3a0d482) JettonSwapPayload {
// Inlined fields of SwapStep struct
poolAddress: Address;
kind: Int as uint1 = 0;
limit: Int as coins = 0;
nextStep: SwapStep? = null;
// Set of parameters relevant for the entire swap
swapParams: SwapParams;
}
/// NOTE: To calculate and provide the Jetton wallet address for the target user,
/// make sure to check the links after this code snippet.
fun swapJetton(targetJettonWalletAddress: Address) {
send(SendParameters{
to: targetJettonWalletAddress,
value: ton("0.3"),
body: JettonTransfer{
// Unique identifier used to trace transactions across multiple contracts.
// Set to 0, which means we don't mark messages to trace their chains.
queryId: 0,
// Jetton amount for the swap.
amount: 10, // NOTE: change to your amount
// Address of the Jetton vault to send the message to.
destination: address("EQAYqo4u7VF0fa4DPAebk4g9lBytj2VFny7pzXR0trjtXQaO"),
// Address to return any exceeding funds.
responseDestination: myAddress(),
forwardTonAmount: ton("0.25"),
forwardPayload: JettonSwapPayload{
// Address of the swap pool, which is the TON/USDT pair in this case.
poolAddress: address("EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs"),
// Set of parameters relevant for the entire swap.
swapParams: SwapParams{}, // use defaults
}.toCell(),
}.toCell(),
});
}
//
// Helper structs described earlier on this page
//
struct SwapStep {
poolAddress: Address;
kind: Int as uint1 = 0;
limit: Int as coins = 0;
nextStep: Cell?;
}
struct SwapParams {
deadline: Int as uint32 = 0;
recipientAddress: Address? = null;
referralAddress: Address? = null;
fulfillPayload: Cell? = null;
rejectPayload: Cell? = null;
}
//
// Messages from the Jetton standard
//
message(0xf8a7ea5) JettonTransfer {
queryId: Int as uint64;
amount: Int as coins;
destination: Address;
responseDestination: Address;
customPayload: Cell? = null;
forwardTonAmount: Int as coins;
forwardPayload: Cell?; // slightly adjusted
}
▶️ Open in Web IDE

Liquidity Provisioning

To provide liquidity to a particular DeDust pool, you must provide both assets. The pool will then issue special LP tokens to the depositor’s address.

Read more about liquidity provisioning in the DeDust documentation.

import "@stdlib/deploy";
/// https://docs.dedust.io/reference/tlb-schemes#message-deposit_liquidity-1
message(0x40e108d6) JettonDepositLiquidity {
// Pool type: 0 for volatile, 1 for stable
// A volatile pool is based on the "Constant Product" formula
// A stable-swap pool is optimized for assets of near-equal value,
// e.g., USDT/USDC, TON/stTON, etc.
poolType: Int as uint1;
// Provided assets
asset0: Asset;
asset1: Asset;
// Minimal amount of LP tokens to be received
// If less liquidity is provided, the provisioning will be rejected
// Defaults to 0, making this value ignored
minimalLpAmount: Int as coins = 0;
// Target amount of the first asset
targetBalances0: Int as coins;
// Target amount of the second asset
targetBalances1: Int as coins;
// Custom payload attached to the transaction if the provisioning is successful
// Defaults to `null`, which means no payload
fulfillPayload: Cell? = null;
// Custom payload attached to the transaction if the provisioning is rejected
// Defaults to `null`, which means no payload
rejectPayload: Cell? = null;
}
/// https://docs.dedust.io/reference/tlb-schemes#message-deposit_liquidity
message(0xd55e4686) NativeDepositLiquidity {
// Unique identifier used to trace transactions across multiple contracts
// Defaults to 0, which means messages are not marked to trace their chains
queryId: Int as uint64 = 0;
// Toncoin amount for the deposit
amount: Int as coins;
// Inlined fields of JettonDepositLiquidity message without the opcode prefix
poolType: Int as uint1;
asset0: Asset;
asset1: Asset;
minimalLpAmount: Int as coins = 0;
targetBalances0: Int as coins;
targetBalances1: Int as coins;
fulfillPayload: Cell? = null;
rejectPayload: Cell? = null;
}
/// https://docs.dedust.io/reference/tlb-schemes#asset
struct Asset {
// Specify 0 for native (TON) and omit all following fields
// Specify 1 for Jetton, then you must set non-null values for the following fields
type: Int as uint4;
workchain: Int as uint8 = 0; // Both these zeros will be removed during the .build() function. Only type will remain.
address: Int as uint256 = 0;
}
const PoolTypeVolatile: Int = 0;
const PoolTypeStable: Int = 1;
const AssetTypeNative: Int = 0b0000;
const AssetTypeJetton: Int = 0b0001;
const JettonProvideLpGas: Int = ton("0.5");
const JettonProvideLpGasFwd: Int = ton("0.4");
const TonProvideLpGas: Int = ton("0.15");
// This example directly uses the provided `myJettonWalletAddress`.
// In real-world scenarios, it's more reliable to calculate this address on-chain or save it during initialization to prevent any issues.
fun provideLiquidity(myJettonWalletAddress: Address) {
let jettonMasterRaw = parseStdAddress(
address("EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs")
.asSlice()
);
// Step 1. Prepare input
let jettonAmount = ton("1");
let tonAmount = ton("1");
let asset0 = Asset{
type: AssetTypeNative,
};
let asset1 = Asset{
type: AssetTypeJetton,
workchain: jettonMasterRaw.workchain,
address: jettonMasterRaw.address,
};
// Step 2. Deposit Jetton to Vault
let jettonDepositBody = JettonDepositLiquidity{
poolType: PoolTypeVolatile,
asset0,
asset1,
targetBalances0: tonAmount,
targetBalances1: jettonAmount,
}.build(); // Notice .build() instead of .toCell(),
// since we want some custom serialization logic.
send(SendParameters{
to: myJettonWalletAddress,
value: JettonProvideLpGas,
body: JettonTransfer{
queryId: 42,
amount: jettonAmount,
// Jetton Vault
destination: address("EQAYqo4u7VF0fa4DPAebk4g9lBytj2VFny7pzXR0trjtXQaO"),
responseDestination: myAddress(),
forwardTonAmount: JettonProvideLpGasFwd,
forwardPayload: jettonDepositBody,
}.toCell()
});
// Step 3. Deposit TON to Vault
let nativeDepositBody = NativeDepositLiquidity{
queryId: 42,
amount: tonAmount,
poolType: PoolTypeVolatile,
asset0,
asset1,
targetBalances0: tonAmount,
targetBalances1: jettonAmount,
}.build(); // Notice .build() instead of .toCell(),
// since we want some custom serialization logic.
send(SendParameters{
to: address("EQDa4VOnTYlLvDJ0gZjNYm5PXfSmmtL6Vs6A_CZEtXCNICq_"),
value: tonAmount + TonProvideLpGas,
body: nativeDepositBody,
});
}
//
// Helper extension functions to build respective structures and messages
//
extends fun build(self: Asset): Cell {
let assetBuilder = beginCell()
.storeUint(self.type, 4);
if (self.type == AssetTypeNative) {
return assetBuilder.endCell();
}
if (self.type == AssetTypeJetton) {
return assetBuilder
.storeUint(self.workchain, 8)
.storeUint(self.address, 256)
.endCell();
}
// Unknown asset type
return beginCell().endCell();
}
extends fun build(self: JettonDepositLiquidity): Cell {
return beginCell()
.storeUint(0x40e108d6, 32)
.storeUint(self.poolType, 1)
.storeSlice(self.asset0.build().asSlice())
.storeSlice(self.asset1.build().asSlice())
.storeCoins(self.minimalLpAmount)
.storeCoins(self.targetBalances0)
.storeCoins(self.targetBalances1)
.storeMaybeRef(self.fulfillPayload)
.storeMaybeRef(self.rejectPayload)
.endCell();
}
extends fun build(self: NativeDepositLiquidity): Cell {
return beginCell()
.storeUint(0xd55e4686, 32)
.storeUint(self.queryId, 64)
.storeCoins(self.amount)
.storeUint(self.poolType, 1)
.storeSlice(self.asset0.build().asSlice())
.storeSlice(self.asset1.build().asSlice())
.storeRef(
beginCell()
.storeCoins(self.minimalLpAmount)
.storeCoins(self.targetBalances0)
.storeCoins(self.targetBalances1)
.endCell()
)
.storeMaybeRef(self.fulfillPayload)
.storeMaybeRef(self.rejectPayload)
.endCell();
}
//
// Messages from the Jetton standard
//
message(0xf8a7ea5) JettonTransfer {
queryId: Int as uint64;
amount: Int as coins;
destination: Address;
responseDestination: Address?;
customPayload: Cell? = null;
forwardTonAmount: Int as coins;
forwardPayload: Cell?; // Slightly adjusted
}
▶️ Open in Web IDE

Withdraw liquidity

To withdraw liquidity, burning LP tokens is required. You can refer to examples of Jetton burning in the respective section of the Jettons Cookbook page. However, more Toncoin should be added than for a normal burn, since adding too few may result in LP tokens being burned but no liquidity (or only partial liquidity) being sent from the pool. Therefore, consider attaching at least 0.50.5 Toncoin — the excess amount will be returned.