Position Module

Manages liquidity position NFTs and their associated metadata.

Data Structures

Position

The main NFT struct representing position ownership:

struct Position has key, store {
    id: UID,                    // Unique identifier
    pool: ID,                   // Associated pool ID
    index: u64,                 // Position index number
    coin_type_a: TypeName,      // First token type
    coin_type_b: TypeName,      // Second token type
    name: String,               // Display name
    description: String,        // Description
    url: String,                // Icon URL
    tick_lower_index: I32,      // Lower tick bound
    tick_upper_index: I32,      // Upper tick bound
    liquidity: u128,            // Current liquidity amount
    lock_until: u64,            // Lock expiration timestamp
}

PositionInfo

The computational data for position calculations:

struct PositionInfo has copy, drop, store {
    position_id: ID,            // Corresponding Position NFT ID
    liquidity: u128,            // Active liquidity
    tick_lower_index: I32,      // Lower tick
    tick_upper_index: I32,      // Upper tick
    fee_growth_inside_a: u128,  // Fee growth for token A
    fee_growth_inside_b: u128,  // Fee growth for token B
    fee_owned_a: u64,          // Accumulated fees for token A
    fee_owned_b: u64,          // Accumulated fees for token B
    points_owned: u128,         // Accumulated points
    points_growth_inside: u128, // Points growth tracker
    rewards: vector<PositionReward>, // Multi-token rewards
}

PositionManager

Central registry managing all positions:

struct PositionManager has store {
    tick_spacing: u32,              // Tick spacing for the pool
    position_index: u64,            // Next position index
    positions: LinkedTable<ID, PositionInfo>, // Position data storage
}

PositionReward

Individual reward token tracking:

struct PositionReward has copy, drop, store {
    growth_inside: u128,    // Growth rate inside position range
    amount_owned: u64,      // Accumulated reward amount
}

Core Functions

Position Creation

open_position<CoinTypeA, CoinTypeB>

Creates a new liquidity position NFT.

Parameters:

  • manager: Mutable reference to PositionManager

  • pool_id: ID of the target pool

  • pool_index: Pool's index number

  • icon_url: URL for position icon

  • tick_lower_index: Lower bound of liquidity range

  • tick_upper_index: Upper bound of liquidity range

  • lock_until: Timestamp until position is locked

  • ctx: Transaction context

Returns: New Position NFT

Example:

let position = position::open_position<USDC, ETH>(
    &mut manager,
    pool_id,
    1,
    string::utf8(b"https://example.com/icon.png"),
    i32::from(-1000),  // Lower tick
    i32::from(1000),   // Upper tick
    0,                 // No lock
    ctx
);

Liquidity Management

increase_liquidity

Adds liquidity to an existing position.

Parameters:

  • manager: Mutable reference to PositionManager

  • position: Mutable reference to Position NFT

  • delta_liquidity: Amount of liquidity to add

  • fee_growth_inside_a: Current fee growth for token A

  • fee_growth_inside_b: Current fee growth for token B

  • points_growth_inside: Current points growth

  • rewards_growth_inside: Vector of current reward growth rates

Returns: New total liquidity amount

decrease_liquidity

Removes liquidity from a position.

Parameters: Similar to increase_liquidity but removes instead of adds Additional Checks:

  • Validates position is not locked

  • Ensures sufficient liquidity exists

Fee Management

update_fee

Updates accumulated fees for a position.

public(friend) fun update_fee(
    manager: &mut PositionManager,
    position_id: ID,
    fee_growth_a: u128,
    fee_growth_b: u128,
): (u64, u64)

reset_fee

Collects accumulated fees and resets counters.

public(friend) fun reset_fee(
    manager: &mut PositionManager,
    position_id: ID,
): (u64, u64)

Position Lifecycle

1. Creation

// Create new position
let position = open_position<TokenA, TokenB>(
    &mut manager, pool_id, pool_index, icon_url, 
    lower_tick, upper_tick, lock_until, ctx
);

2. Liquidity Addition

// Add liquidity
let new_liquidity = increase_liquidity(
    &mut manager, &mut position, liquidity_amount,
    fee_growth_a, fee_growth_b, points_growth, rewards_growth
);

3. Fee Collection

// Collect fees
let (fee_a, fee_b) = update_and_reset_fee(
    &mut manager, position_id, fee_growth_a, fee_growth_b
);

4. Liquidity Removal

// Remove liquidity
let remaining_liquidity = decrease_liquidity(
    &mut manager, &mut position, liquidity_to_remove,
    fee_growth_a, fee_growth_b, points_growth, rewards_growth, clock
);

5. Position Closure

// Close empty position
if (is_empty(&position_info)) {
    close_position(&mut manager, position);
}

Fee and Reward Management

Fee Calculation

Fees are calculated using the formula:

delta_fee = liquidity × (current_growth - last_growth) / 2^64

The system tracks fee growth both globally and per position to calculate accumulated fees accurately.

Multi-token Rewards

The rewards system supports multiple reward tokens simultaneously:

// Each position can have multiple rewards
struct PositionReward {
    growth_inside: u128,    // Growth rate inside position's range
    amount_owned: u64,      // Accumulated reward amount
}

Reward Distribution

Rewards are distributed proportionally based on:

  • Position liquidity amount

  • Time in range

  • Growth rates specific to the position's tick range

Position Locking

Lock Mechanism

Positions can be locked until a specific timestamp to prevent premature liquidity removal:

// Lock position until timestamp
lock_position(&mut position, lock_until_timestamp);

// Check lock status
let lock_time = get_lock_until(&position);

Lock Validation

When decreasing liquidity, the system validates:

let current_time = clock::timestamp_ms(clock);
assert!(position.lock_until <= current_time, 11);

API Reference

Query Functions

Position Information

  • liquidity(position: &Position): u128 - Get position liquidity

  • tick_range(position: &Position): (I32, I32) - Get tick range

  • pool_id(position: &Position): ID - Get associated pool ID

  • index(position: &Position): u64 - Get position index

Position Info Queries

  • info_liquidity(info: &PositionInfo): u128 - Get liquidity from info

  • info_fee_owned(info: &PositionInfo): (u64, u64) - Get accumulated fees

  • info_points_owned(info: &PositionInfo): u128 - Get accumulated points

  • info_rewards(info: &PositionInfo): &vector<PositionReward> - Get rewards

Manager Queries

  • is_position_exist(manager: &PositionManager, id: ID): bool - Check if position exists

  • fetch_positions(manager: &PositionManager, start: vector<ID>, limit: u64): vector<PositionInfo> - Get multiple positions

Validation Functions

check_position_tick_range

Validates tick range parameters:

  • tick_lower < tick_upper

  • Ticks within valid bounds

  • Ticks align with tick spacing

is_empty

Checks if position can be closed:

  • Zero liquidity

  • No accumulated fees

  • No pending rewards

Examples

Complete Position Lifecycle Example

// 1. Create position manager
let manager = position::new(10, ctx); // 10 tick spacing

// 2. Open new position
let position = position::open_position<USDC, ETH>(
    &mut manager,
    pool_id,
    1,
    string::utf8(b"https://example.com/icon.png"),
    i32::from(-100),   // Lower tick
    i32::from(100),    // Upper tick
    0,                 // No lock
    ctx
);

// 3. Add liquidity
let liquidity_added = position::increase_liquidity(
    &mut manager,
    &mut position,
    1000000, // 1M liquidity units
    fee_growth_a,
    fee_growth_b,
    points_growth,
    rewards_growth
);

// 4. Later: collect fees
let (fee_a, fee_b) = position::update_and_reset_fee(
    &mut manager,
    object::id(&position),
    new_fee_growth_a,
    new_fee_growth_b
);

// 5. Remove some liquidity
let remaining = position::decrease_liquidity(
    &mut manager,
    &mut position,
    500000, // Remove 500K liquidity
    fee_growth_a,
    fee_growth_b,
    points_growth,
    rewards_growth,
    clock
);

// 6. Check if position can be closed
let position_info = position::borrow_position_info(
    &manager, 
    object::id(&position)
);
if (position::is_empty(position_info)) {
    position::close_position(&mut manager, position);
}

Batch Position Query Example

// Query multiple positions
let positions = position::fetch_positions(
    &manager,
    vector::empty<ID>(), // Start from beginning
    10                   // Limit to 10 positions
);

// Process each position
let i = 0;
while (i < vector::length(&positions)) {
    let pos_info = vector::borrow(&positions, i);
    let liquidity = position::info_liquidity(pos_info);
    let (fee_a, fee_b) = position::info_fee_owned(pos_info);
    // Process position data...
    i = i + 1;
}

Best Practices

Position Management

  1. Always validate tick ranges before creating positions

  2. Check lock status before attempting to decrease liquidity

  3. Update fees and rewards before liquidity changes

  4. Use batch queries for efficient data retrieval

Error Handling

The module uses various assertion codes:

  • 1: Fee overflow in token A/B

  • 2: Reward overflow

  • 3: Points overflow

  • 5: Invalid tick range

  • 6: Position not found

  • 7: Cannot close non-empty position

  • 8: Liquidity addition overflow

  • 9: Insufficient liquidity to remove

  • 10: Invalid reward index

  • 11: Position is locked

Gas Optimization

  • Use fetch_positions for batch operations

  • Combine fee updates with liquidity changes

  • Close empty positions to free storage

Security Considerations

  1. Position Ownership: Only the NFT holder can modify the position

  2. Tick Validation: All tick ranges are validated for correctness

  3. Overflow Protection: All arithmetic operations include overflow checks

  4. Lock Enforcement: Locked positions cannot have liquidity decreased

  5. Access Control: Critical functions are friend-only

This documentation provides a comprehensive guide to the Ferra CLMM Position Management system. For additional technical details, refer to the source code and inline comments.

Last updated