rialo-stake-manager-interface 0.11.0

Instructions and constructors for stake management
Documentation
// Copyright (c) Subzero Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

//! The Stake Manager program interface.

pub mod instruction;

rialo_s_pubkey::declare_id!("StakeManager1111111111111111111111111111111");

/// Check if an activation is still pending (has not taken effect yet).
///
/// State transitions occur at epoch boundaries (FreezeStakes). An activation
/// is considered "pending" or "activating" until at least one FreezeStakes
/// has occurred since the activation was requested.
///
/// # State Transition Model
///
/// - **Pending/Activating**: `activation_timestamp >= last_freeze_timestamp`
///   - No FreezeStakes has occurred since activation was requested
///   - Stake is in the "activating" state, changes take effect at next epoch boundary
///
/// - **Activated**: `activation_timestamp < last_freeze_timestamp`
///   - At least one FreezeStakes has occurred since activation
///   - Stake has transitioned to "activated" state, delegation is now effective
///
/// # Arguments
/// * `activation_timestamp` - When ActivateStake was called (in milliseconds)
/// * `last_freeze_timestamp` - When FreezeStakes was last called (epoch boundary)
///
/// # Returns
/// * `true` - Activation is pending (activating state)
/// * `false` - Activation has taken effect (activated state)
#[inline]
pub fn is_activation_pending(activation_timestamp: u64, last_freeze_timestamp: u64) -> bool {
    activation_timestamp >= last_freeze_timestamp
}

/// Check if a deactivation is still pending (has not taken effect yet).
///
/// State transitions occur at epoch boundaries (FreezeStakes). A deactivation
/// is considered "pending" or "deactivating" until at least one FreezeStakes
/// has occurred since the deactivation was requested.
///
/// # State Transition Model
///
/// - **Pending/Deactivating**: `deactivation_timestamp >= last_freeze_timestamp`
///   - No FreezeStakes has occurred since deactivation was requested
///   - Stake is in the "deactivating" state, still counted for validator selection
///
/// - **Deactivated**: `deactivation_timestamp < last_freeze_timestamp`
///   - At least one FreezeStakes has occurred since deactivation
///   - Stake has transitioned to "deactivated" state, unbonding period begins
///
/// # Arguments
/// * `deactivation_timestamp` - When DeactivateStake was called (in milliseconds)
/// * `last_freeze_timestamp` - When FreezeStakes was last called (epoch boundary)
///
/// # Returns
/// * `true` - Deactivation is pending (deactivating state)
/// * `false` - Deactivation has taken effect (deactivated state, in unbonding)
#[inline]
pub fn is_deactivation_pending(deactivation_timestamp: u64, last_freeze_timestamp: u64) -> bool {
    deactivation_timestamp >= last_freeze_timestamp
}

/// Check if unbonding is complete using the two-step validation process.
///
/// Unbonding completion requires TWO conditions to be met:
///
/// 1. **State transition**: Stake must have transitioned to "deactivated" state.
///    - A stake is "deactivating" while `deactivation_timestamp >= last_freeze_timestamp`
///    - A stake becomes "deactivated" when `deactivation_timestamp < last_freeze_timestamp`
///    - This ensures at least one FreezeStakes (epoch boundary) has occurred since deactivation
///
/// 2. **Duration enforcement**: The unbonding period must have actually elapsed in real time.
///    - Uses current block timestamp (Clock sysvar) for real-time guarantees
///    - This ensures the time-based penalty has been served, even if FreezeStakes occurred
///
/// # Arguments
/// * `deactivation_timestamp` - When deactivation was requested (in milliseconds)
/// * `last_freeze_timestamp` - When FreezeStakes was last called (epoch boundary)
/// * `unbonding_end` - When unbonding completes (from `ValidatorInfo::end_of_unbonding`)
/// * `current_timestamp` - Current block timestamp from Clock sysvar (in milliseconds)
///
/// # Returns
/// * `true` - Unbonding is complete, stake can be reactivated or fully withdrawn
/// * `false` - Unbonding is not complete, stake is still locked
///
/// # Security
/// This two-step check prevents attackers from bypassing unbonding penalties by
/// exploiting timestamp manipulation or epoch boundary timing.
///
/// # Example
/// ```
/// use rialo_stake_manager_interface::is_unbonding_complete;
///
/// // Deactivated at 1000ms, last freeze at 2000ms, unbonding_end = 1500ms, current time 2000ms
/// // State transition: 1000 < 2000 ✓
/// // Duration: 1500 < 2000 ✓
/// assert!(is_unbonding_complete(1000, 2000, 1500, 2000));
///
/// // Deactivated at 1000ms, last freeze at 2000ms, unbonding_end = 2500ms, current time 2000ms
/// // State transition: 1000 < 2000 ✓
/// // Duration: 2500 >= 2000 ✗
/// assert!(!is_unbonding_complete(1000, 2000, 2500, 2000));
/// ```
pub fn is_unbonding_complete(
    deactivation_timestamp: u64,
    last_freeze_timestamp: u64,
    unbonding_end: u64,
    current_timestamp: u64,
) -> bool {
    // Check 1: State transition - must be "deactivated" (not "deactivating")
    if is_deactivation_pending(deactivation_timestamp, last_freeze_timestamp) {
        return false; // Still deactivating
    }

    // Check 2: Duration enforcement - unbonding must have ended
    unbonding_end < current_timestamp
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_is_unbonding_complete_both_conditions_met() {
        // Deactivated at 1000, freeze at 2000, unbonding_end = 1000+500 = 1500, current 2000
        // State: 1000 < 2000 ✓, Duration: 1500 < 2000 ✓
        assert!(is_unbonding_complete(1000, 2000, 1500, 2000));
    }

    #[test]
    fn test_is_unbonding_complete_still_deactivating() {
        // Deactivated at 2000, freeze at 2000, unbonding_end = 2000+500 = 2500, current 3000
        // State: 2000 >= 2000 ✗ (still deactivating)
        assert!(!is_unbonding_complete(2000, 2000, 2500, 3000));
    }

    #[test]
    fn test_is_unbonding_complete_duration_not_elapsed() {
        // Deactivated at 1000, freeze at 2000, unbonding_end = 1000+1500 = 2500, current 2000
        // State: 1000 < 2000 ✓, Duration: 2500 >= 2000 ✗
        assert!(!is_unbonding_complete(1000, 2000, 2500, 2000));
    }

    #[test]
    fn test_is_unbonding_complete_at_boundary() {
        // Deactivated at 1000, freeze at 2000, unbonding_end = 1000+1000 = 2000, current 2000
        // State: 1000 < 2000 ✓, Duration: 2000 >= 2000 ✗ (need < not <=)
        assert!(!is_unbonding_complete(1000, 2000, 2000, 2000));
    }

    #[test]
    fn test_is_unbonding_complete_just_past_boundary() {
        // Deactivated at 1000, freeze at 2000, unbonding_end = 1000+1000 = 2000, current 2001
        // State: 1000 < 2000 ✓, Duration: 2000 < 2001 ✓
        assert!(is_unbonding_complete(1000, 2000, 2000, 2001));
    }

    #[test]
    fn test_is_unbonding_complete_zero_unbonding_period() {
        // Deactivated at 1000, freeze at 2000, unbonding_end = 1000+0 = 1000, current 1500
        // State: 1000 < 2000 ✓, Duration: 1000 < 1500 ✓
        assert!(is_unbonding_complete(1000, 2000, 1000, 1500));
    }
}