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#swapstepstruct 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#swapparamsstruct 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;}
Swap Toncoin for any Jetton
/// https://docs.dedust.io/reference/tlb-schemes#message-swapmessage(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 nanoToncoinsfun 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;}
Swap a Jetton for another Jetton or Toncoin
/// https://docs.dedust.io/reference/tlb-schemes#message-swap-1message(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}
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-1message(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_liquiditymessage(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#assetstruct 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}
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 Toncoin — the excess amount will be returned.