Sui Move Validator Staking Pool: A Comprehensive Analysis

·

Introduction

Sui utilizes Delegated Proof-of-Stake (DPoS) as the core of its consensus mechanism. Users can stake their SUI tokens with validator nodes, sharing transaction fees, storage costs, and staking rewards during each epoch cycle. Each validator maintains its own staking pool, calculating user rewards through an exchange rate algorithm.

All core logic is implemented via Move on-chain (staking_pool.move). This article provides a detailed analysis of this contract.

Key Features

Data Structures

Each validator maintains a StakingPool with these critical components:

public struct StakingPool has key, store {
    id: UID,
    activation_epoch: Option<u64>, // Epoch when pool becomes active
    deactivation_epoch: Option<u64>, // Epoch when pool becomes inactive
    sui_balance: u64, // Total SUI including staked tokens and rewards
    rewards_pool: Balance<SUI>, // Validator rewards received each epoch
    pool_token_balance: u64, // Total issued pool tokens
    exchange_rates: Table<u64, PoolTokenExchangeRate>, // SUI/token rates per epoch
    pending_stake: u64, // SUI staking to activate next epoch
    pending_total_sui_withdraw: u64, // SUI withdrawals pending
    pending_pool_token_withdraw: u64, // Token withdrawals pending
    extra_fields: Bag,
}

The exchange rate represents the SUI/token ratio:

public struct PoolTokenExchangeRate has store, copy, drop {
    sui_amount: u64,
    pool_token_amount: u64,
}

StakedSui serves as the core staking receipt, with SIP-6 enabled store capability for compatibility with third-party liquid staking protocols:

public struct StakedSui has key, store {
    id: UID,
    pool_id: ID,
    stake_activation_epoch: u64, // Epoch when stake becomes active
    principal: Balance<SUI>, // Staked SUI amount
}

Core Methods

Staking: request_add_stake

This method initiates staking by:

  1. Adding SUI to pending stake
  2. Creating a StakedSui receipt
  3. Making changes effective next epoch
public(package) fun request_add_stake(
    pool: &mut StakingPool,
    stake: Balance<SUI>,
    stake_activation_epoch: u64,
    ctx: &mut TxContext
) : StakedSui {
    let sui_amount = stake.value();
    assert!(!is_inactive(pool), EDelegationToInactivePool);
    assert!(sui_amount > 0, EDelegationOfZeroSui);
    
    let staked_sui = StakedSui {
        id: object::new(ctx),
        pool_id: object::id(pool),
        stake_activation_epoch,
        principal: stake,
    };
    
    pool.pending_stake = pool.pending_stake + sui_amount;
    staked_sui
}

Withdrawal: request_withdraw_stake

This method processes withdrawals by:

  1. Calculating principal and rewards
  2. Adding to pending withdrawals
  3. Processing immediately for inactive pools
public(package) fun request_withdraw_stake(
    pool: &mut StakingPool,
    staked_sui: StakedSui,
    ctx: &TxContext
) : Balance<SUI> {
    if (staked_sui.stake_activation_epoch > ctx.epoch()) {
        let principal = unwrap_staked_sui(staked_sui);
        pool.pending_stake = pool.pending_stake - principal.value();
        return principal
    };
    
    let (pool_token_withdraw_amount, mut principal_withdraw) =
        withdraw_from_principal(pool, staked_sui);
    
    let rewards_withdraw = withdraw_rewards(
        pool, principal_withdraw.value(), pool_token_withdraw_amount, ctx.epoch()
    );
    
    let total_sui_withdraw_amount = principal_withdraw.value() + rewards_withdraw.value();
    
    pool.pending_total_sui_withdraw = pool.pending_total_sui_withdraw + total_sui_withdraw_amount;
    pool.pending_pool_token_withdraw = pool.pending_pool_token_withdraw + pool_token_withdraw_amount;
    
    if (is_inactive(pool)) process_pending_stake_withdraw(pool);
    
    principal_withdraw.join(rewards_withdraw);
    principal_withdraw
}

Epoch Processing: process_pending_stakes_and_withdraws

This method handles:

  1. Processing pending withdrawals
  2. Activating new stakes
  3. Updating exchange rates for the new epoch
public(package) fun process_pending_stakes_and_withdraws(pool: &mut StakingPool, ctx: &TxContext) {
    let new_epoch = ctx.epoch() + 1;
    
    process_pending_stake_withdraw(pool);
    process_pending_stake(pool);
    
    pool.exchange_rates.add(
        new_epoch,
        PoolTokenExchangeRate { 
            sui_amount: pool.sui_balance, 
            pool_token_amount: pool.pool_token_balance 
        },
    );
    
    check_balance_invariants(pool, new_epoch);
}

Key Considerations

  1. Epoch Timing: All staking changes take effect in the next epoch
  2. Reward Calculation: Uses precise exchange rate tracking
  3. Pool Status: Different rules apply for active vs. inactive pools
  4. Security Checks: Strict validation of all operations

👉 Explore more about Sui staking mechanics

FAQ

When do staking rewards become available?

Rewards are distributed at the end of each epoch and can be withdrawn in subsequent epochs.

How are exchange rates calculated?

Rates reflect the ratio between total SUI in the pool (stakes + rewards) and issued pool tokens.

Can I withdraw my stake immediately?

Withdrawals enter pending status and process in the next epoch, except for inactive pools.

What happens if a validator becomes inactive?

The pool stops accepting new stakes but processes all pending withdrawals immediately.

How are rewards calculated?

Rewards come from network fees and are proportional to your stake amount and duration.

👉 Learn about advanced staking strategies

Conclusion

The Sui staking pool contract demonstrates an elegant implementation of validator delegation with:

This system ensures fair and transparent reward distribution while maintaining network security through validator staking.