Skip to main content

rialo_stake_manager_interface/
lib.rs

1// Copyright (c) Subzero Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! The Stake Manager program interface.
5
6pub mod instruction;
7
8rialo_s_pubkey::declare_id!("StakeManager1111111111111111111111111111111");
9
10/// Check if an activation is still pending (has not taken effect yet).
11///
12/// State transitions occur at epoch boundaries (FreezeStakes). An activation
13/// is considered "pending" or "activating" until at least one FreezeStakes
14/// has occurred since the activation was requested.
15///
16/// # State Transition Model
17///
18/// - **Pending/Activating**: `activation_timestamp >= last_freeze_timestamp`
19///   - No FreezeStakes has occurred since activation was requested
20///   - Stake is in the "activating" state, changes take effect at next epoch boundary
21///
22/// - **Activated**: `activation_timestamp < last_freeze_timestamp`
23///   - At least one FreezeStakes has occurred since activation
24///   - Stake has transitioned to "activated" state, delegation is now effective
25///
26/// # Arguments
27/// * `activation_timestamp` - When ActivateStake was called (in milliseconds)
28/// * `last_freeze_timestamp` - When FreezeStakes was last called (epoch boundary)
29///
30/// # Returns
31/// * `true` - Activation is pending (activating state)
32/// * `false` - Activation has taken effect (activated state)
33#[inline]
34pub fn is_activation_pending(activation_timestamp: u64, last_freeze_timestamp: u64) -> bool {
35    activation_timestamp >= last_freeze_timestamp
36}
37
38/// Check if a deactivation is still pending (has not taken effect yet).
39///
40/// State transitions occur at epoch boundaries (FreezeStakes). A deactivation
41/// is considered "pending" or "deactivating" until at least one FreezeStakes
42/// has occurred since the deactivation was requested.
43///
44/// # State Transition Model
45///
46/// - **Pending/Deactivating**: `deactivation_timestamp >= last_freeze_timestamp`
47///   - No FreezeStakes has occurred since deactivation was requested
48///   - Stake is in the "deactivating" state, still counted for validator selection
49///
50/// - **Deactivated**: `deactivation_timestamp < last_freeze_timestamp`
51///   - At least one FreezeStakes has occurred since deactivation
52///   - Stake has transitioned to "deactivated" state, unbonding period begins
53///
54/// # Arguments
55/// * `deactivation_timestamp` - When DeactivateStake was called (in milliseconds)
56/// * `last_freeze_timestamp` - When FreezeStakes was last called (epoch boundary)
57///
58/// # Returns
59/// * `true` - Deactivation is pending (deactivating state)
60/// * `false` - Deactivation has taken effect (deactivated state, in unbonding)
61#[inline]
62pub fn is_deactivation_pending(deactivation_timestamp: u64, last_freeze_timestamp: u64) -> bool {
63    deactivation_timestamp >= last_freeze_timestamp
64}
65
66/// Check if unbonding is complete using the two-step validation process.
67///
68/// Unbonding completion requires TWO conditions to be met:
69///
70/// 1. **State transition**: Stake must have transitioned to "deactivated" state.
71///    - A stake is "deactivating" while `deactivation_timestamp >= last_freeze_timestamp`
72///    - A stake becomes "deactivated" when `deactivation_timestamp < last_freeze_timestamp`
73///    - This ensures at least one FreezeStakes (epoch boundary) has occurred since deactivation
74///
75/// 2. **Duration enforcement**: The unbonding period must have actually elapsed in real time.
76///    - Uses current block timestamp (Clock sysvar) for real-time guarantees
77///    - This ensures the time-based penalty has been served, even if FreezeStakes occurred
78///
79/// # Arguments
80/// * `deactivation_timestamp` - When deactivation was requested (in milliseconds)
81/// * `last_freeze_timestamp` - When FreezeStakes was last called (epoch boundary)
82/// * `unbonding_end` - When unbonding completes (from `ValidatorInfo::end_of_unbonding`)
83/// * `current_timestamp` - Current block timestamp from Clock sysvar (in milliseconds)
84///
85/// # Returns
86/// * `true` - Unbonding is complete, stake can be reactivated or fully withdrawn
87/// * `false` - Unbonding is not complete, stake is still locked
88///
89/// # Security
90/// This two-step check prevents attackers from bypassing unbonding penalties by
91/// exploiting timestamp manipulation or epoch boundary timing.
92///
93/// # Example
94/// ```
95/// use rialo_stake_manager_interface::is_unbonding_complete;
96///
97/// // Deactivated at 1000ms, last freeze at 2000ms, unbonding_end = 1500ms, current time 2000ms
98/// // State transition: 1000 < 2000 ✓
99/// // Duration: 1500 < 2000 ✓
100/// assert!(is_unbonding_complete(1000, 2000, 1500, 2000));
101///
102/// // Deactivated at 1000ms, last freeze at 2000ms, unbonding_end = 2500ms, current time 2000ms
103/// // State transition: 1000 < 2000 ✓
104/// // Duration: 2500 >= 2000 ✗
105/// assert!(!is_unbonding_complete(1000, 2000, 2500, 2000));
106/// ```
107pub fn is_unbonding_complete(
108    deactivation_timestamp: u64,
109    last_freeze_timestamp: u64,
110    unbonding_end: u64,
111    current_timestamp: u64,
112) -> bool {
113    // Check 1: State transition - must be "deactivated" (not "deactivating")
114    if is_deactivation_pending(deactivation_timestamp, last_freeze_timestamp) {
115        return false; // Still deactivating
116    }
117
118    // Check 2: Duration enforcement - unbonding must have ended
119    unbonding_end < current_timestamp
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn test_is_unbonding_complete_both_conditions_met() {
128        // Deactivated at 1000, freeze at 2000, unbonding_end = 1000+500 = 1500, current 2000
129        // State: 1000 < 2000 ✓, Duration: 1500 < 2000 ✓
130        assert!(is_unbonding_complete(1000, 2000, 1500, 2000));
131    }
132
133    #[test]
134    fn test_is_unbonding_complete_still_deactivating() {
135        // Deactivated at 2000, freeze at 2000, unbonding_end = 2000+500 = 2500, current 3000
136        // State: 2000 >= 2000 ✗ (still deactivating)
137        assert!(!is_unbonding_complete(2000, 2000, 2500, 3000));
138    }
139
140    #[test]
141    fn test_is_unbonding_complete_duration_not_elapsed() {
142        // Deactivated at 1000, freeze at 2000, unbonding_end = 1000+1500 = 2500, current 2000
143        // State: 1000 < 2000 ✓, Duration: 2500 >= 2000 ✗
144        assert!(!is_unbonding_complete(1000, 2000, 2500, 2000));
145    }
146
147    #[test]
148    fn test_is_unbonding_complete_at_boundary() {
149        // Deactivated at 1000, freeze at 2000, unbonding_end = 1000+1000 = 2000, current 2000
150        // State: 1000 < 2000 ✓, Duration: 2000 >= 2000 ✗ (need < not <=)
151        assert!(!is_unbonding_complete(1000, 2000, 2000, 2000));
152    }
153
154    #[test]
155    fn test_is_unbonding_complete_just_past_boundary() {
156        // Deactivated at 1000, freeze at 2000, unbonding_end = 1000+1000 = 2000, current 2001
157        // State: 1000 < 2000 ✓, Duration: 2000 < 2001 ✓
158        assert!(is_unbonding_complete(1000, 2000, 2000, 2001));
159    }
160
161    #[test]
162    fn test_is_unbonding_complete_zero_unbonding_period() {
163        // Deactivated at 1000, freeze at 2000, unbonding_end = 1000+0 = 1000, current 1500
164        // State: 1000 < 2000 ✓, Duration: 1000 < 1500 ✓
165        assert!(is_unbonding_complete(1000, 2000, 1000, 1500));
166    }
167}