Skip to content

Commit 47b067d

Browse files
committed
fix: update
1 parent 2e0766e commit 47b067d

File tree

7 files changed

+707
-128
lines changed

7 files changed

+707
-128
lines changed

api/src/resolvers/query_resolver.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -988,7 +988,7 @@ impl QueryResolver {
988988
///
989989
/// # Example
990990
///
991-
/// This endpoint can be used to fetch the bam metric for a specific epoch:
991+
/// This endpoint can be used to fetch the bam validators for a specific epoch:
992992
///
993993
/// ```ignore
994994
/// GET /bam_validators?epoch=800
Lines changed: 246 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,85 @@
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
25
pub(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

1013
impl 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

Comments
 (0)