Sui.

Post

Share your knowledge.

fomo on Sui.
Jul 08, 2025
Expert Q&A

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
1
5
Share
Comments
.

Answers

5
Jojo.
Jojo821
Jul 10 2025, 05:15

The 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

ModuleWhat it does
tickControls price range accounting (tick_lower to tick_upper)
positionStores each LP’s liquidity & range info
poolHandles 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 concentrated
  • liquidity: amount of liquidity added
  • amount_a, amount_b: token amounts deposited
  • after_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:

  1. validate_pool_position(...) – checks position is valid for pool.

  2. tick_range(arg1) – gets lower/upper ticks from position object.

  3. If arg2 == true, calculate liquidity from token amount using:

    get_liquidity_by_amount(...)
    
    

    Else calculate token amounts from liquidity:

    get_amount_by_liquidity(...)
    
    
  4. Adjust tick ranges:

    tick::increase_liquidity(...)  // updates tick accounting
    
    
  5. If current tick is within range → add liquidity to pool:

    arg0.liquidity = arg0.liquidity + v2;
    
    
  6. 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_index and tick_upper_index: liquidity bounds
  • liquidity: how much capital is allocated
  • fee_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 Position object
  • Initializes a PositionInfo entry
  • Stores it in linked_table of the PositionManager

This is how LPs pick their range and mint a new NFT for tracking.

For more detail you can go into the source code

1
Best Answer
Comments
.
robber.sui.
Sep 5 2025, 14:22

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.

5
Comments
.
0xduckmove.
Jul 9 2025, 04:09

For Magma, you can read the bytecode from deployed packages:

clmm:0x4a35d3dfef55ed3631b7158544c6322a23bc434fe4fca1234cb680ce0505f82d

magma_integrate: 0x2e704d8afc1d6d7f154dee337cc14c153f6f9ce1708213e5dc04a32afe0e45f1

1
Comments
.

Do you know the answer?

Please log in and share it.