1+ use jito_steward:: constants:: BASIS_POINTS_MAX ;
2+ use kobe_core:: db_models:: bam_epoch_metric:: BamEpochMetric ;
3+
14/// Criteria for BAM delegation amount based on JIP-28 specification
25pub ( crate ) struct BamDelegationCriteria {
36 /// JIP-28 tier breakpoints
47 ///
5- /// 0: stakeweight_threshold
6- /// 1: allocation_pct
7- tiers : Vec < ( f64 , f64 ) > ,
8+ /// 0: stakeweight_threshold (in BPS)
9+ /// 1: allocation_pct (in BPS)
10+ tiers : Vec < ( u64 , u64 ) > ,
811}
912
1013impl BamDelegationCriteria {
1114 /// Create a new calculator with JIP-28 default tiers
1215 pub fn new ( ) -> Self {
1316 Self {
1417 tiers : vec ! [
15- ( 0.00 , 0.20 ) , // Initial -> 20%
16- ( 0.20 , 0.30 ) , // 20% -> 30%
17- ( 0.25 , 0.40 ) , // 25% -> 40%
18- ( 0.30 , 0.50 ) , // 30% -> 50%
19- ( 0.35 , 0.70 ) , // 35% -> 70%
20- ( 0.40 , 1.00 ) , // 40% -> 100%
18+ ( 0 , 2_000 ) , // 0% -> 20%
19+ ( 2_000 , 3_000 ) , // 20% -> 30%
20+ ( 2_500 , 4_000 ) , // 25% -> 40%
21+ ( 3_000 , 5_000 ) , // 30% -> 50%
22+ ( 3_500 , 7_000 ) , // 35% -> 70%
23+ ( 4_000 , 10_000 ) , // 40% -> 100%
2124 ] ,
2225 }
2326 }
2427
2528 /// Calculate BAM stakeweight (percentage of network stake running BAM)
26- fn calculate_bam_stakeweight ( & self , bam_sol_stake : u64 , total_sol_stake : u64 ) -> Option < f64 > {
29+ fn calculate_bam_stakeweight ( & self , bam_sol_stake : u64 , total_sol_stake : u64 ) -> Option < u64 > {
2730 if total_sol_stake == 0 {
2831 return None ;
2932 }
30- Some ( bam_sol_stake as f64 / total_sol_stake as f64 )
33+
34+ let stakeweight_bps = ( bam_sol_stake as u128 )
35+ . checked_mul ( BASIS_POINTS_MAX as u128 ) ?
36+ . checked_div ( total_sol_stake as u128 ) ?;
37+
38+ Some ( stakeweight_bps as u64 )
3139 }
3240
33- /// Get JitoSOL allocation percentage for a given BAM stakeweight
34- fn get_allocation_percentage ( & self , bam_stakeweight : f64 ) -> f64 {
41+ /// Calculate current tier level with two-epoch validation
42+ pub fn calculate_current_allocation (
43+ & self ,
44+ current_epoch_metric : & BamEpochMetric ,
45+ previous_epoch_metric : Option < & BamEpochMetric > ,
46+ ) -> u64 {
47+ let current_stakeweight_bps = self
48+ . calculate_bam_stakeweight (
49+ current_epoch_metric. get_bam_stake ( ) ,
50+ current_epoch_metric. get_total_stake ( ) ,
51+ )
52+ . unwrap_or ( 0 ) ;
53+
54+ // If no previous epoch, return initial 20% (2000 BPS)
55+ let Some ( prev_metric) = previous_epoch_metric else {
56+ return 2_000 ;
57+ } ;
58+
59+ let previous_stakeweight_bps = self
60+ . calculate_bam_stakeweight ( prev_metric. get_bam_stake ( ) , prev_metric. get_total_stake ( ) )
61+ . unwrap_or ( 0 ) ;
62+
63+ // Find highest tier where BOTH epochs meet threshold
3564 self . tiers
3665 . iter ( )
3766 . rev ( )
38- . find ( |( threshold, _) | bam_stakeweight >= * threshold)
67+ . find ( |( threshold, _) | {
68+ current_stakeweight_bps >= * threshold && previous_stakeweight_bps >= * threshold
69+ } )
3970 . map ( |( _, allocation) | * allocation)
40- . unwrap_or ( 0.20 ) // Default to initial 20%
71+ . unwrap_or ( 2_000 )
4172 }
4273
43- /// Calculate total available BAM delegation stake
44- ///
45- /// # Arguments
46- /// * `bam_stake` - Total stake of all BAM validators
47- /// * `total_stake` - Total stake across entire Solana network
48- /// * `total_jitosol_tvl` - Total value locked in JitoSOL stake pool
49- ///
50- /// # Returns
51- /// Total JitoSOL amount available for delegation to all BAM validators
74+ /// Calculate available delegation amount in lamports
5275 pub fn calculate_available_delegation (
5376 & self ,
54- bam_stake : u64 ,
55- total_stake : u64 ,
77+ allocation_bps : u64 ,
5678 total_jitosol_tvl : u64 ,
5779 ) -> u64 {
58- let stakeweight = match self . calculate_bam_stakeweight ( bam_stake, total_stake) {
59- Some ( sw) => sw,
60- None => return 0 ,
61- } ;
62-
63- let allocation_pct = self . get_allocation_percentage ( stakeweight) ;
64- ( total_jitosol_tvl as f64 * allocation_pct) as u64
80+ ( total_jitosol_tvl as u128 )
81+ . saturating_mul ( allocation_bps as u128 )
82+ . saturating_div ( BASIS_POINTS_MAX as u128 ) as u64
6583 }
6684}
6785
@@ -73,61 +91,223 @@ mod tests {
7391 fn test_stakeweight_calculation ( ) {
7492 let criteria = BamDelegationCriteria :: new ( ) ;
7593
94+ // 25% = 2500 BPS
7695 assert_eq ! (
7796 criteria. calculate_bam_stakeweight( 100_000_000 , 400_000_000 ) ,
78- Some ( 0.25 )
97+ Some ( 2_500 )
98+ ) ;
99+
100+ // 0% = 0 BPS
101+ assert_eq ! ( criteria. calculate_bam_stakeweight( 0 , 400_000_000 ) , Some ( 0 ) ) ;
102+
103+ // Division by zero
104+ assert_eq ! ( criteria. calculate_bam_stakeweight( 100_000_000 , 0 ) , None ) ;
105+ }
106+
107+ #[ test]
108+ fn test_two_epoch_validation_initial_epoch ( ) {
109+ let criteria = BamDelegationCriteria :: new ( ) ;
110+
111+ // First epoch ever - no previous data
112+ let current = BamEpochMetric :: new (
113+ 100 ,
114+ 100_000_000 , // 25% stakeweight
115+ 400_000_000 ,
116+ 10 ,
117+ ) ;
118+
119+ // Should only get initial 20% regardless of current stakeweight
120+ assert_eq ! ( criteria. calculate_current_allocation( & current, None ) , 2000 ) ;
121+ }
122+
123+ #[ test]
124+ fn test_two_epoch_validation_tier_advancement ( ) {
125+ let criteria = BamDelegationCriteria :: new ( ) ;
126+
127+ let previous = BamEpochMetric :: new (
128+ 99 ,
129+ 100_000_000 , // 25% stakeweight
130+ 400_000_000 ,
131+ 10 ,
79132 ) ;
133+
134+ let current = BamEpochMetric :: new (
135+ 100 ,
136+ 100_000_000 , // 25% stakeweight (same as previous)
137+ 400_000_000 ,
138+ 10 ,
139+ ) ;
140+
141+ // Both epochs at 25% -> should advance to 40% allocation
80142 assert_eq ! (
81- criteria. calculate_bam_stakeweight ( 0 , 400_000_000 ) ,
82- Some ( 0.0 )
143+ criteria. calculate_current_allocation ( & current , Some ( & previous ) ) ,
144+ 4000
83145 ) ;
84- assert_eq ! ( criteria. calculate_bam_stakeweight( 100_000_000 , 0 ) , None ) ;
85146 }
86147
87148 #[ test]
88- fn test_allocation_tiers ( ) {
149+ fn test_two_epoch_validation_insufficient_previous_epoch ( ) {
89150 let criteria = BamDelegationCriteria :: new ( ) ;
90151
91- // Test each tier boundary
92- assert_eq ! ( criteria. get_allocation_percentage( 0.00 ) , 0.20 ) ;
93- assert_eq ! ( criteria. get_allocation_percentage( 0.19 ) , 0.20 ) ;
94- assert_eq ! ( criteria. get_allocation_percentage( 0.20 ) , 0.30 ) ;
95- assert_eq ! ( criteria. get_allocation_percentage( 0.24 ) , 0.30 ) ;
96- assert_eq ! ( criteria. get_allocation_percentage( 0.25 ) , 0.40 ) ;
97- assert_eq ! ( criteria. get_allocation_percentage( 0.30 ) , 0.50 ) ;
98- assert_eq ! ( criteria. get_allocation_percentage( 0.35 ) , 0.70 ) ;
99- assert_eq ! ( criteria. get_allocation_percentage( 0.40 ) , 1.00 ) ;
100- assert_eq ! ( criteria. get_allocation_percentage( 0.50 ) , 1.00 ) ;
152+ let previous = BamEpochMetric :: new (
153+ 99 ,
154+ 80_000_000 , // 20% stakeweight (below 25% threshold)
155+ 400_000_000 ,
156+ 10 ,
157+ ) ;
158+
159+ let current = BamEpochMetric :: new (
160+ 100 ,
161+ 100_000_000 , // 25% stakeweight
162+ 400_000_000 ,
163+ 10 ,
164+ ) ;
165+
166+ // Current is 25% but previous was only 20%
167+ // Should stay at 30% tier (20% threshold met in both)
168+ assert_eq ! (
169+ criteria. calculate_current_allocation( & current, Some ( & previous) ) ,
170+ 3000
171+ ) ;
101172 }
102173
103174 #[ test]
104- fn test_available_delegation ( ) {
175+ fn test_two_epoch_validation_insufficient_current_epoch ( ) {
105176 let criteria = BamDelegationCriteria :: new ( ) ;
106177
107- // 25% BAM stakeweight -> 40% JitoSOL allocation
108- let result = criteria. calculate_available_delegation (
109- 100_000_000 , // 100M SOL in BAM
110- 400_000_000 , // 400M SOL total (25%)
111- 10_000_000 , // 10M JitoSOL TVL
178+ let previous = BamEpochMetric :: new (
179+ 99 ,
180+ 100_000_000 , // 25% stakeweight
181+ 400_000_000 ,
182+ 10 ,
183+ ) ;
184+
185+ let current = BamEpochMetric :: new (
186+ 100 ,
187+ 80_000_000 , // 20% stakeweight (dropped below threshold)
188+ 400_000_000 ,
189+ 10 ,
112190 ) ;
113- assert_eq ! ( result, 4_000_000 ) ; // 40% of 10M = 4M
114191
115- // Edge case: 0 total stake
116- let result = criteria. calculate_available_delegation ( 100_000_000 , 0 , 10_000_000 ) ;
117- assert_eq ! ( result, 0 ) ;
192+ // Previous was 25% but current dropped to 20%
193+ // Should fall back to 30% tier (20% threshold met in both)
194+ assert_eq ! (
195+ criteria. calculate_current_allocation( & current, Some ( & previous) ) ,
196+ 3000
197+ ) ;
118198 }
119199
120200 #[ test]
121- fn test_jip28_spec_example ( ) {
201+ fn test_two_epoch_validation_volatility_protection ( ) {
122202 let criteria = BamDelegationCriteria :: new ( ) ;
123203
124- // Scenario from JIP-28:
125- // If BAM has 35% of network stake, they get 70% of JitoSOL TVL
126- let result = criteria. calculate_available_delegation (
127- 140_000_000 , // 140M SOL in BAM (35%)
128- 400_000_000 , // 400M total network stake
129- 10_000_000 , // 10M JitoSOL TVL
204+ // Epoch N-1: 24% (just below 25% threshold)
205+ let previous = BamEpochMetric :: new ( 99 , 96_000_000 , 400_000_000 , 10 ) ;
206+
207+ // Epoch N: 26% (just above 25% threshold)
208+ let current = BamEpochMetric :: new ( 100 , 104_000_000 , 400_000_000 , 10 ) ;
209+
210+ // Even though current is above 25%, previous wasn't
211+ // Should stay at 30% (20% tier) not jump to 40% (25% tier)
212+ assert_eq ! (
213+ criteria. calculate_current_allocation( & current, Some ( & previous) ) ,
214+ 3000
215+ ) ;
216+ }
217+
218+ #[ test]
219+ fn test_two_epoch_validation_highest_tier ( ) {
220+ let criteria = BamDelegationCriteria :: new ( ) ;
221+
222+ let previous = BamEpochMetric :: new (
223+ 99 ,
224+ 160_000_000 , // 40% stakeweight
225+ 400_000_000 ,
226+ 10 ,
227+ ) ;
228+
229+ let current = BamEpochMetric :: new (
230+ 100 ,
231+ 180_000_000 , // 45% stakeweight
232+ 400_000_000 ,
233+ 10 ,
234+ ) ;
235+
236+ // Both epochs above 40% threshold -> 100% allocation
237+ assert_eq ! (
238+ criteria. calculate_current_allocation( & current, Some ( & previous) ) ,
239+ 10_000
240+ ) ;
241+ }
242+
243+ #[ test]
244+ fn test_two_epoch_validation_multiple_tier_jump_prevented ( ) {
245+ let criteria = BamDelegationCriteria :: new ( ) ;
246+
247+ // Previous: 15% stakeweight
248+ let previous = BamEpochMetric :: new ( 99 , 60_000_000 , 400_000_000 , 10 ) ;
249+
250+ // Current: 35% stakeweight (jumped 20 percentage points!)
251+ let current = BamEpochMetric :: new ( 100 , 140_000_000 , 400_000_000 , 10 ) ;
252+
253+ // Can't skip tiers - previous only qualified for initial 20%
254+ // Should get 20% allocation, not 70%
255+ assert_eq ! (
256+ criteria. calculate_current_allocation( & current, Some ( & previous) ) ,
257+ 2000
258+ ) ;
259+ }
260+
261+ #[ test]
262+ fn test_two_epoch_validation_gradual_progression ( ) {
263+ let criteria = BamDelegationCriteria :: new ( ) ;
264+
265+ // Simulate progression through tiers
266+
267+ // Epoch 1: Both at 20% -> 30% allocation
268+ let epoch_1 = BamEpochMetric :: new ( 100 , 80_000_000 , 400_000_000 , 10 ) ;
269+ let epoch_2 = BamEpochMetric :: new ( 101 , 80_000_000 , 400_000_000 , 10 ) ;
270+ assert_eq ! (
271+ criteria. calculate_current_allocation( & epoch_2, Some ( & epoch_1) ) ,
272+ 3000
273+ ) ;
274+
275+ // Epoch 3: Both at 25% -> 40% allocation
276+ let epoch_3 = BamEpochMetric :: new ( 102 , 100_000_000 , 400_000_000 , 10 ) ;
277+ assert_eq ! (
278+ criteria. calculate_current_allocation( & epoch_3, Some ( & epoch_2) ) ,
279+ 3000
280+ ) ;
281+
282+ let epoch_4 = BamEpochMetric :: new ( 103 , 100_000_000 , 400_000_000 , 10 ) ;
283+ assert_eq ! (
284+ criteria. calculate_current_allocation( & epoch_4, Some ( & epoch_3) ) ,
285+ 4000
286+ ) ;
287+ }
288+
289+ #[ test]
290+ fn test_two_epoch_validation_edge_case_zero_stake ( ) {
291+ let criteria = BamDelegationCriteria :: new ( ) ;
292+
293+ let previous = BamEpochMetric :: new (
294+ 99 ,
295+ 0 , // No BAM stake
296+ 400_000_000 ,
297+ 0 ,
298+ ) ;
299+
300+ let current = BamEpochMetric :: new (
301+ 100 ,
302+ 100_000_000 , // Suddenly 25% stake
303+ 400_000_000 ,
304+ 10 ,
305+ ) ;
306+
307+ // Previous had 0%, so can only get initial 20%
308+ assert_eq ! (
309+ criteria. calculate_current_allocation( & current, Some ( & previous) ) ,
310+ 2000
130311 ) ;
131- assert_eq ! ( result, 7_000_000 ) ; // 70% of 10M = 7M
132312 }
133313}
0 commit comments