Post
Share your knowledge.
How can I create a v3,3 DEX model in Sui Move?
I'm particularly interested in how current DEXs like Momentum and Magma implement concentrated liquidity pools with specific price ranges in Sui Move. How can I design a pool where liquidity is concentrated in a particular range, and how would the system handle liquidity adjustments as the price moves within or outside that range?
- Sui
- Architecture
- Move
Answers
5The easiest way to understand how they implement this is by looking directly at their code. Thanks to SuiVision, we can decompile the bytecode back into the original source code.
You can check out the Magma package here — they haven't open-sourced it, but you can still view it: https://suivision.xyz/package/0x4a35d3dfef55ed3631b7158544c6322a23bc434fe4fca1234cb680ce0505f82d?tab=Code
Main Modules You Should Focus On
| Module | What it does |
|---|---|
tick | Controls price range accounting (tick_lower to tick_upper) |
position | Stores each LP’s liquidity & range info |
pool | Handles the logic for swapping, liquidity changes, and rewards |
Code Walkthrough + High-Level Explanation
1. AddLiquidityEvent
struct AddLiquidityEvent has copy, drop, store {
pool: 0x2::object::ID,
position: 0x2::object::ID,
tick_lower: 0x659c0e9c4c8a416f040fa758d4fc4073a5fdd1fed97edadcd5cba5180fb36246::i32::I32,
tick_upper: 0x659c0e9c4c8a416f040fa758d4fc4073a5fdd1fed97edadcd5cba5180fb36246::i32::I32,
liquidity: u128,
after_liquidity: u128,
amount_a: u64,
amount_b: u64,
}
Purpose: Event structure that logs when liquidity is added.
Key fields:
tick_lower,tick_upper: range where liquidity is concentratedliquidity: amount of liquidity addedamount_a,amount_b: token amounts depositedafter_liquidity: result after adjusting internal accounting
Helps trace LP activity and makes it indexable off-chain.
2. add_liquidity_internal
fun add_liquidity_internal<T0, T1>(arg0: &mut Pool<T0, T1>, arg1: &mut 0x4a35d3dfef55ed3631b7158544c6322a23bc434fe4fca1234cb680ce0505f82d::position::Position, arg2: bool, arg3: u128, arg4: u64, arg5: bool, arg6: u64) : AddLiquidityReceipt<T0, T1> {
assert!(!arg0.is_pause, 13);
validate_pool_position<T0, T1>(arg0, arg1);
0x4a35d3dfef55ed3631b7158544c6322a23bc434fe4fca1234cb680ce0505f82d::rewarder::settle(&mut arg0.rewarder_manager, arg0.liquidity, arg6);
let (v0, v1) = 0x4a35d3dfef55ed3631b7158544c6322a23bc434fe4fca1234cb680ce0505f82d::position::tick_range(arg1);
let (v2, v3, v4) = if (arg2) {
let (v5, v6, v7) = 0x4a35d3dfef55ed3631b7158544c6322a23bc434fe4fca1234cb680ce0505f82d::clmm_math::get_liquidity_by_amount(v0, v1, arg0.current_tick_index, arg0.current_sqrt_price, arg4, arg5);
(v5, v6, v7)
} else {
let (v8, v9) = 0x4a35d3dfef55ed3631b7158544c6322a23bc434fe4fca1234cb680ce0505f82d::clmm_math::get_amount_by_liquidity(v0, v1, arg0.current_tick_index, arg0.current_sqrt_price, arg3, true);
(arg3, v8, v9)
};
let (v10, v11, v12, v13, v14) = get_all_growths_in_tick_range<T0, T1>(arg0, v0, v1);
0x4a35d3dfef55ed3631b7158544c6322a23bc434fe4fca1234cb680ce0505f82d::tick::increase_liquidity(&mut arg0.tick_manager, arg0.current_tick_index, v0, v1, v2, arg0.fee_growth_global_a, arg0.fee_growth_global_b, 0x4a35d3dfef55ed3631b7158544c6322a23bc434fe4fca1234cb680ce0505f82d::rewarder::points_growth_global(&arg0.rewarder_manager), 0x4a35d3dfef55ed3631b7158544c6322a23bc434fe4fca1234cb680ce0505f82d::rewarder::rewards_growth_global(&arg0.rewarder_manager), arg0.magma_distribution_growth_global);
if (0x659c0e9c4c8a416f040fa758d4fc4073a5fdd1fed97edadcd5cba5180fb36246::i32::gte(arg0.current_tick_index, v0) && 0x659c0e9c4c8a416f040fa758d4fc4073a5fdd1fed97edadcd5cba5180fb36246::i32::lt(arg0.current_tick_index, v1)) {
assert!(0x659c0e9c4c8a416f040fa758d4fc4073a5fdd1fed97edadcd5cba5180fb36246::math_u128::add_check(arg0.liquidity, v2), 1);
arg0.liquidity = arg0.liquidity + v2;
};
let v15 = AddLiquidityEvent{
pool : 0x2::object::id<Pool<T0, T1>>(arg0),
position : 0x2::object::id<0x4a35d3dfef55ed3631b7158544c6322a23bc434fe4fca1234cb680ce0505f82d::position::Position>(arg1),
tick_lower : v0,
tick_upper : v1,
liquidity : arg3,
after_liquidity : 0x4a35d3dfef55ed3631b7158544c6322a23bc434fe4fca1234cb680ce0505f82d::position::increase_liquidity(&mut arg0.position_manager, arg1, v2, v10, v11, v13, v12, v14),
amount_a : v3,
amount_b : v4,
};
0x2::event::emit<AddLiquidityEvent>(v15);
AddLiquidityReceipt<T0, T1>{
pool_id : 0x2::object::id<Pool<T0, T1>>(arg0),
amount_a : v3,
amount_b : v4,
}
}
Purpose: Core engine that handles how liquidity is added to a pool.
Key operations:
-
validate_pool_position(...)– checks position is valid for pool. -
tick_range(arg1)– gets lower/upper ticks from position object. -
If
arg2 == true, calculateliquidityfrom token amount using:get_liquidity_by_amount(...)Else calculate
token amountsfromliquidity:get_amount_by_liquidity(...) -
Adjust tick ranges:
tick::increase_liquidity(...) // updates tick accounting -
If current tick is within range → add liquidity to pool:
arg0.liquidity = arg0.liquidity + v2; -
Emit event + update PositionInfo:
position::increase_liquidity(...) event::emit(...)
🔥 Core of concentrated liquidity logic. Liquidity is only active inside tick range. Outside that, liquidity does not participate.
3. get_rewards_in_tick_range
public fun get_rewards_in_tick_range<T0, T1>(arg0: &Pool<T0, T1>, arg1: 0x659c0e9c4c8a416f040fa758d4fc4073a5fdd1fed97edadcd5cba5180fb36246::i32::I32, arg2: 0x659c0e9c4c8a416f040fa758d4fc4073a5fdd1fed97edadcd5cba5180fb36246::i32::I32) : vector<u128> {
0x4a35d3dfef55ed3631b7158544c6322a23bc434fe4fca1234cb680ce0505f82d::tick::get_rewards_in_range(arg0.current_tick_index, 0x4a35d3dfef55ed3631b7158544c6322a23bc434fe4fca1234cb680ce0505f82d::rewarder::rewards_growth_global(&arg0.rewarder_manager), 0x4a35d3dfef55ed3631b7158544c6322a23bc434fe4fca1234cb680ce0505f82d::tick::try_borrow_tick(&arg0.tick_manager, arg1), 0x4a35d3dfef55ed3631b7158544c6322a23bc434fe4fca1234cb680ce0505f82d::tick::try_borrow_tick(&arg0.tick_manager, arg2))
}
Purpose: Calculate rewards accumulated within a tick range.
Critical for reward distribution logic in v3,3 where LPs earn yield only in active range.
4. get_rewards_in_range + get_reward_growth_outside
public fun get_reward_growth_outside(arg0: &Tick, arg1: u64) : u128 {
if (0x1::vector::length<u128>(&arg0.rewards_growth_outside) <= arg1) {
0
} else {
*0x1::vector::borrow<u128>(&arg0.rewards_growth_outside, arg1)
}
}
public fun get_rewards_in_range(arg0: 0x659c0e9c4c8a416f040fa758d4fc4073a5fdd1fed97edadcd5cba5180fb36246::i32::I32, arg1: vector<u128>, arg2: 0x1::option::Option<Tick>, arg3: 0x1::option::Option<Tick>) : vector<u128> {
let v0 = 0x1::vector::empty<u128>();
let v1 = 0;
while (v1 < 0x1::vector::length<u128>(&arg1)) {
let v2 = *0x1::vector::borrow<u128>(&arg1, v1);
let v3 = if (0x1::option::is_none<Tick>(&arg2)) {
v2
} else {
let v4 = 0x1::option::borrow<Tick>(&arg2);
let v5 = if (0x659c0e9c4c8a416f040fa758d4fc4073a5fdd1fed97edadcd5cba5180fb36246::i32::lt(arg0, v4.index)) {
0x659c0e9c4c8a416f040fa758d4fc4073a5fdd1fed97edadcd5cba5180fb36246::math_u128::wrapping_sub(v2, get_reward_growth_outside(v4, v1))
} else {
get_reward_growth_outside(v4, v1)
};
v5
};
let v6 = if (0x1::option::is_none<Tick>(&arg3)) {
0
} else {
let v7 = 0x1::option::borrow<Tick>(&arg3);
let v8 = if (0x659c0e9c4c8a416f040fa758d4fc4073a5fdd1fed97edadcd5cba5180fb36246::i32::lt(arg0, v7.index)) {
get_reward_growth_outside(v7, v1)
} else {
let v9 = get_reward_growth_outside(v7, v1);
0x659c0e9c4c8a416f040fa758d4fc4073a5fdd1fed97edadcd5cba5180fb36246::math_u128::wrapping_sub(v2, v9)
};
v8
};
0x1::vector::push_back<u128>(&mut v0, 0x659c0e9c4c8a416f040fa758d4fc4073a5fdd1fed97edadcd5cba5180fb36246::math_u128::wrapping_sub(0x659c0e9c4c8a416f040fa758d4fc4073a5fdd1fed97edadcd5cba5180fb36246::math_u128::wrapping_sub(v2, v3), v6));
v1 = v1 + 1;
};
v0
}
Purpose: Computes net rewards inside tick range, subtracting outside growths.
Implements the same logic as Uniswap v3’s "outside growth" trick:
- Track reward growth outside a tick
- When price crosses tick, delta is used to compute rewards inside
This ensures precision for reward accounting across dynamic ranges.
5. Position and PositionInfo
struct Position has store, key {
id: 0x2::object::UID,
pool: 0x2::object::ID,
index: u64,
coin_type_a: 0x1::type_name::TypeName,
coin_type_b: 0x1::type_name::TypeName,
name: 0x1::string::String,
description: 0x1::string::String,
url: 0x1::string::String,
tick_lower_index: 0x659c0e9c4c8a416f040fa758d4fc4073a5fdd1fed97edadcd5cba5180fb36246::i32::I32,
tick_upper_index: 0x659c0e9c4c8a416f040fa758d4fc4073a5fdd1fed97edadcd5cba5180fb36246::i32::I32,
liquidity: u128,
}
struct PositionInfo has copy, drop, store {
position_id: 0x2::object::ID,
liquidity: u128,
tick_lower_index: 0x659c0e9c4c8a416f040fa758d4fc4073a5fdd1fed97edadcd5cba5180fb36246::i32::I32,
tick_upper_index: 0x659c0e9c4c8a416f040fa758d4fc4073a5fdd1fed97edadcd5cba5180fb36246::i32::I32,
fee_growth_inside_a: u128,
fee_growth_inside_b: u128,
fee_owned_a: u64,
fee_owned_b: u64,
points_owned: u128,
points_growth_inside: u128,
rewards: vector<PositionReward>,
magma_distribution_staked: bool,
magma_distribution_growth_inside: u128,
magma_distribution_owned: u64,
}
Purpose: NFT-style ownership and internal state of LP positions.
Important fields:
tick_lower_indexandtick_upper_index: liquidity boundsliquidity: how much capital is allocatedfee_growth_inside_a/b,points_growth_inside,rewards: reward tracking
6. open_position
public(friend) fun open_position<T0, T1>(arg0: &mut PositionManager, arg1: 0x2::object::ID, arg2: u64, arg3: 0x1::string::String, arg4: 0x659c0e9c4c8a416f040fa758d4fc4073a5fdd1fed97edadcd5cba5180fb36246::i32::I32, arg5: 0x659c0e9c4c8a416f040fa758d4fc4073a5fdd1fed97edadcd5cba5180fb36246::i32::I32, arg6: &mut 0x2::tx_context::TxContext) : Position {
check_position_tick_range(arg4, arg5, arg0.tick_spacing);
let v0 = arg0.position_index + 1;
let v1 = Position{
id : 0x2::object::new(arg6),
pool : arg1,
index : v0,
coin_type_a : 0x1::type_name::get<T0>(),
coin_type_b : 0x1::type_name::get<T1>(),
name : new_position_name(arg2, v0),
description : 0x1::string::utf8(b"Magma Liquidity Position"),
url : arg3,
tick_lower_index : arg4,
tick_upper_index : arg5,
liquidity : 0,
};
let v2 = 0x2::object::id<Position>(&v1);
let v3 = PositionInfo{
position_id : v2,
liquidity : 0,
tick_lower_index : arg4,
tick_upper_index : arg5,
fee_growth_inside_a : 0,
fee_growth_inside_b : 0,
fee_owned_a : 0,
fee_owned_b : 0,
points_owned : 0,
points_growth_inside : 0,
rewards : 0x1::vector::empty<PositionReward>(),
magma_distribution_staked : false,
magma_distribution_growth_inside : 0,
magma_distribution_owned : 0,
};
0x682eaba7450909645bf949db3fc5881432a00b49b4c06d6974ecc4ee684e7992::linked_table::push_back<0x2::object::ID, PositionInfo>(&mut arg0.positions, v2, v3);
arg0.position_index = v0;
v1
}
Purpose: Creates a new position with specific tick range.
📌 Key parts:
- Constructs a new
Positionobject - Initializes a
PositionInfoentry - Stores it in
linked_tableof thePositionManager
This is how LPs pick their range and mint a new NFT for tracking.
For more detail you can go into the source code
Designing concentrated liquidity pools in Sui Move, similar to Momentum or Magma, centers around 'ticks' that define discrete price points. An LP provides liquidity for a specific range, say between tick_A and tick_B. Internally, the pool tracks a global current price (often as sqrt_price_X96) and the total active liquidity. Each tick stores 'net liquidity' changes as the price crosses it. When you add liquidity, you specify a lower and upper tick, and the system calculates how much 'liquidity' (L) that represents. As for adjustments, if the current price is within your chosen range (between your lower and upper ticks), your liquidity is active, earns fees, and participates in swaps. If the price moves outside your defined range, your liquidity becomes inactive; it stops earning fees and isn't used for swaps. Your tokens effectively sit idle in your position until the price re-enters your range. To reactivate out-of-range liquidity, you'd typically need to manually remove it and re-add it to a new, active range.
For Magma, you can read the bytecode from deployed packages:
clmm:0x4a35d3dfef55ed3631b7158544c6322a23bc434fe4fca1234cb680ce0505f82d
magma_integrate: 0x2e704d8afc1d6d7f154dee337cc14c153f6f9ce1708213e5dc04a32afe0e45f1
Do you know the answer?
Please log in and share it.
Sui is a Layer 1 protocol blockchain designed as the first internet-scale programmable blockchain platform.
- How to Maximize Profit Holding SUI: Sui Staking vs Liquid Staking616
- Why does BCS require exact field order for deserialization when Move structs have named fields?65
- Multiple Source Verification Errors" in Sui Move Module Publications - Automated Error Resolution55
- Sui Move Error - Unable to process transaction No valid gas coins found for the transaction419
- Sui Transaction Failing: Objects Reserved for Another Transaction410