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}