Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

Commit da37530

Browse files
authored
stake-pool: Add tolerance for stake accounts at minimum (#3839)
* stake-pool: Add tolerance for stake accounts at minimum * Use test-case to check more cases * Add more tolerance on withdrawal * Potentially fix test per #3854 * Keep throwing solutions until CI passes * Fix repeated transaction issue * Fix preferred withdrawal tolerance too * Remove doubled tolerance
1 parent c7fbd4b commit da37530

File tree

6 files changed

+301
-97
lines changed

6 files changed

+301
-97
lines changed

stake-pool/program/src/big_vec.rs

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -148,14 +148,14 @@ impl<'data> BigVec<'data> {
148148
}
149149

150150
/// Find matching data in the array
151-
pub fn find<T: Pack>(&self, data: &[u8], predicate: fn(&[u8], &[u8]) -> bool) -> Option<&T> {
151+
pub fn find<T: Pack, F: Fn(&[u8]) -> bool>(&self, predicate: F) -> Option<&T> {
152152
let len = self.len() as usize;
153153
let mut current = 0;
154154
let mut current_index = VEC_SIZE_BYTES;
155155
while current != len {
156156
let end_index = current_index + T::LEN;
157157
let current_slice = &self.data[current_index..end_index];
158-
if predicate(current_slice, data) {
158+
if predicate(current_slice) {
159159
return Some(unsafe { &*(current_slice.as_ptr() as *const T) });
160160
}
161161
current_index = end_index;
@@ -165,18 +165,14 @@ impl<'data> BigVec<'data> {
165165
}
166166

167167
/// Find matching data in the array
168-
pub fn find_mut<T: Pack>(
169-
&mut self,
170-
data: &[u8],
171-
predicate: fn(&[u8], &[u8]) -> bool,
172-
) -> Option<&mut T> {
168+
pub fn find_mut<T: Pack, F: Fn(&[u8]) -> bool>(&mut self, predicate: F) -> Option<&mut T> {
173169
let len = self.len() as usize;
174170
let mut current = 0;
175171
let mut current_index = VEC_SIZE_BYTES;
176172
while current != len {
177173
let end_index = current_index + T::LEN;
178174
let current_slice = &self.data[current_index..end_index];
179-
if predicate(current_slice, data) {
175+
if predicate(current_slice) {
180176
return Some(unsafe { &mut *(current_slice.as_ptr() as *mut T) });
181177
}
182178
current_index = end_index;
@@ -242,10 +238,7 @@ impl<'data, 'vec, T: Pack + 'data> Iterator for IterMut<'data, 'vec, T> {
242238

243239
#[cfg(test)]
244240
mod tests {
245-
use {
246-
super::*,
247-
solana_program::{program_memory::sol_memcmp, program_pack::Sealed},
248-
};
241+
use {super::*, solana_program::program_pack::Sealed};
249242

250243
#[derive(Debug, PartialEq)]
251244
struct TestStruct {
@@ -317,11 +310,11 @@ mod tests {
317310
check_big_vec_eq(&v, &[2, 4]);
318311
}
319312

320-
fn find_predicate(a: &[u8], b: &[u8]) -> bool {
321-
if a.len() != b.len() {
313+
fn find_predicate(a: &[u8], b: u64) -> bool {
314+
if a.len() != 8 {
322315
false
323316
} else {
324-
sol_memcmp(a, b, a.len()) == 0
317+
u64::try_from_slice(&a[0..8]).unwrap() == b
325318
}
326319
}
327320

@@ -330,32 +323,26 @@ mod tests {
330323
let mut data = [0u8; 4 + 8 * 4];
331324
let v = from_slice(&mut data, &[1, 2, 3, 4]);
332325
assert_eq!(
333-
v.find::<TestStruct>(&1u64.to_le_bytes(), find_predicate),
326+
v.find::<TestStruct, _>(|x| find_predicate(x, 1)),
334327
Some(&TestStruct::new(1))
335328
);
336329
assert_eq!(
337-
v.find::<TestStruct>(&4u64.to_le_bytes(), find_predicate),
330+
v.find::<TestStruct, _>(|x| find_predicate(x, 4)),
338331
Some(&TestStruct::new(4))
339332
);
340-
assert_eq!(
341-
v.find::<TestStruct>(&5u64.to_le_bytes(), find_predicate),
342-
None
343-
);
333+
assert_eq!(v.find::<TestStruct, _>(|x| find_predicate(x, 5)), None);
344334
}
345335

346336
#[test]
347337
fn find_mut() {
348338
let mut data = [0u8; 4 + 8 * 4];
349339
let mut v = from_slice(&mut data, &[1, 2, 3, 4]);
350340
let mut test_struct = v
351-
.find_mut::<TestStruct>(&1u64.to_le_bytes(), find_predicate)
341+
.find_mut::<TestStruct, _>(|x| find_predicate(x, 1))
352342
.unwrap();
353343
test_struct.value = 0;
354344
check_big_vec_eq(&v, &[0, 2, 3, 4]);
355-
assert_eq!(
356-
v.find_mut::<TestStruct>(&5u64.to_le_bytes(), find_predicate),
357-
None
358-
);
345+
assert_eq!(v.find_mut::<TestStruct, _>(|x| find_predicate(x, 5)), None);
359346
}
360347

361348
#[test]

stake-pool/program/src/processor.rs

Lines changed: 57 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -844,10 +844,9 @@ impl Processor {
844844
if header.max_validators == validator_list.len() {
845845
return Err(ProgramError::AccountDataTooSmall);
846846
}
847-
let maybe_validator_stake_info = validator_list.find::<ValidatorStakeInfo>(
848-
validator_vote_info.key.as_ref(),
849-
ValidatorStakeInfo::memcmp_pubkey,
850-
);
847+
let maybe_validator_stake_info = validator_list.find::<ValidatorStakeInfo, _>(|x| {
848+
ValidatorStakeInfo::memcmp_pubkey(x, validator_vote_info.key)
849+
});
851850
if maybe_validator_stake_info.is_some() {
852851
return Err(StakePoolError::ValidatorAlreadyAdded.into());
853852
}
@@ -994,10 +993,9 @@ impl Processor {
994993

995994
let (meta, stake) = get_stake_state(stake_account_info)?;
996995
let vote_account_address = stake.delegation.voter_pubkey;
997-
let maybe_validator_stake_info = validator_list.find_mut::<ValidatorStakeInfo>(
998-
vote_account_address.as_ref(),
999-
ValidatorStakeInfo::memcmp_pubkey,
1000-
);
996+
let maybe_validator_stake_info = validator_list.find_mut::<ValidatorStakeInfo, _>(|x| {
997+
ValidatorStakeInfo::memcmp_pubkey(x, &vote_account_address)
998+
});
1001999
if maybe_validator_stake_info.is_none() {
10021000
msg!(
10031001
"Vote account {} not found in stake pool",
@@ -1154,10 +1152,9 @@ impl Processor {
11541152
let (meta, stake) = get_stake_state(validator_stake_account_info)?;
11551153
let vote_account_address = stake.delegation.voter_pubkey;
11561154

1157-
let maybe_validator_stake_info = validator_list.find_mut::<ValidatorStakeInfo>(
1158-
vote_account_address.as_ref(),
1159-
ValidatorStakeInfo::memcmp_pubkey,
1160-
);
1155+
let maybe_validator_stake_info = validator_list.find_mut::<ValidatorStakeInfo, _>(|x| {
1156+
ValidatorStakeInfo::memcmp_pubkey(x, &vote_account_address)
1157+
});
11611158
if maybe_validator_stake_info.is_none() {
11621159
msg!(
11631160
"Vote account {} not found in stake pool",
@@ -1316,10 +1313,9 @@ impl Processor {
13161313

13171314
let vote_account_address = validator_vote_account_info.key;
13181315

1319-
let maybe_validator_stake_info = validator_list.find_mut::<ValidatorStakeInfo>(
1320-
vote_account_address.as_ref(),
1321-
ValidatorStakeInfo::memcmp_pubkey,
1322-
);
1316+
let maybe_validator_stake_info = validator_list.find_mut::<ValidatorStakeInfo, _>(|x| {
1317+
ValidatorStakeInfo::memcmp_pubkey(x, vote_account_address)
1318+
});
13231319
if maybe_validator_stake_info.is_none() {
13241320
msg!(
13251321
"Vote account {} not found in stake pool",
@@ -1481,10 +1477,9 @@ impl Processor {
14811477
}
14821478

14831479
if let Some(vote_account_address) = vote_account_address {
1484-
let maybe_validator_stake_info = validator_list.find::<ValidatorStakeInfo>(
1485-
vote_account_address.as_ref(),
1486-
ValidatorStakeInfo::memcmp_pubkey,
1487-
);
1480+
let maybe_validator_stake_info = validator_list.find::<ValidatorStakeInfo, _>(|x| {
1481+
ValidatorStakeInfo::memcmp_pubkey(x, &vote_account_address)
1482+
});
14881483
match maybe_validator_stake_info {
14891484
Some(vsi) => {
14901485
if vsi.status != StakeStatus::Active {
@@ -2031,10 +2026,9 @@ impl Processor {
20312026
}
20322027

20332028
let mut validator_stake_info = validator_list
2034-
.find_mut::<ValidatorStakeInfo>(
2035-
vote_account_address.as_ref(),
2036-
ValidatorStakeInfo::memcmp_pubkey,
2037-
)
2029+
.find_mut::<ValidatorStakeInfo, _>(|x| {
2030+
ValidatorStakeInfo::memcmp_pubkey(x, &vote_account_address)
2031+
})
20382032
.ok_or(StakePoolError::ValidatorNotFound)?;
20392033
check_validator_stake_address(
20402034
program_id,
@@ -2428,7 +2422,7 @@ impl Processor {
24282422
.checked_sub(pool_tokens_fee)
24292423
.ok_or(StakePoolError::CalculationFailure)?;
24302424

2431-
let withdraw_lamports = stake_pool
2425+
let mut withdraw_lamports = stake_pool
24322426
.calc_lamports_withdraw_amount(pool_tokens_burnt)
24332427
.ok_or(StakePoolError::CalculationFailure)?;
24342428

@@ -2442,17 +2436,27 @@ impl Processor {
24422436
let meta = stake_state.meta().ok_or(StakePoolError::WrongStakeState)?;
24432437
let required_lamports = minimum_stake_lamports(&meta, stake_minimum_delegation);
24442438

2439+
let lamports_per_pool_token = stake_pool
2440+
.get_lamports_per_pool_token()
2441+
.ok_or(StakePoolError::CalculationFailure)?;
2442+
let minimum_lamports_with_tolerance =
2443+
required_lamports.saturating_add(lamports_per_pool_token);
2444+
24452445
let has_active_stake = validator_list
2446-
.find::<ValidatorStakeInfo>(
2447-
&required_lamports.to_le_bytes(),
2448-
ValidatorStakeInfo::active_lamports_not_equal,
2449-
)
2446+
.find::<ValidatorStakeInfo, _>(|x| {
2447+
ValidatorStakeInfo::active_lamports_greater_than(
2448+
x,
2449+
&minimum_lamports_with_tolerance,
2450+
)
2451+
})
24502452
.is_some();
24512453
let has_transient_stake = validator_list
2452-
.find::<ValidatorStakeInfo>(
2453-
&0u64.to_le_bytes(),
2454-
ValidatorStakeInfo::transient_lamports_not_equal,
2455-
)
2454+
.find::<ValidatorStakeInfo, _>(|x| {
2455+
ValidatorStakeInfo::transient_lamports_greater_than(
2456+
x,
2457+
&minimum_lamports_with_tolerance,
2458+
)
2459+
})
24562460
.is_some();
24572461

24582462
let validator_list_item_info = if *stake_split_from.key == stake_pool.reserve_stake {
@@ -2478,25 +2482,23 @@ impl Processor {
24782482
stake_pool.preferred_withdraw_validator_vote_address
24792483
{
24802484
let preferred_validator_info = validator_list
2481-
.find::<ValidatorStakeInfo>(
2482-
preferred_withdraw_validator.as_ref(),
2483-
ValidatorStakeInfo::memcmp_pubkey,
2484-
)
2485+
.find::<ValidatorStakeInfo, _>(|x| {
2486+
ValidatorStakeInfo::memcmp_pubkey(x, &preferred_withdraw_validator)
2487+
})
24852488
.ok_or(StakePoolError::ValidatorNotFound)?;
24862489
let available_lamports = preferred_validator_info
24872490
.active_stake_lamports
2488-
.saturating_sub(required_lamports);
2491+
.saturating_sub(minimum_lamports_with_tolerance);
24892492
if preferred_withdraw_validator != vote_account_address && available_lamports > 0 {
24902493
msg!("Validator vote address {} is preferred for withdrawals, it currently has {} lamports available. Please withdraw those before using other validator stake accounts.", preferred_withdraw_validator, preferred_validator_info.active_stake_lamports);
24912494
return Err(StakePoolError::IncorrectWithdrawVoteAddress.into());
24922495
}
24932496
}
24942497

24952498
let validator_stake_info = validator_list
2496-
.find_mut::<ValidatorStakeInfo>(
2497-
vote_account_address.as_ref(),
2498-
ValidatorStakeInfo::memcmp_pubkey,
2499-
)
2499+
.find_mut::<ValidatorStakeInfo, _>(|x| {
2500+
ValidatorStakeInfo::memcmp_pubkey(x, &vote_account_address)
2501+
})
25002502
.ok_or(StakePoolError::ValidatorNotFound)?;
25012503

25022504
let withdraw_source = if has_active_stake {
@@ -2548,11 +2550,21 @@ impl Processor {
25482550
}
25492551
}
25502552
StakeWithdrawSource::ValidatorRemoval => {
2551-
if withdraw_lamports != stake_split_from.lamports() {
2552-
msg!("Cannot withdraw a whole account worth {} lamports, must withdraw exactly {} lamports worth of pool tokens",
2553-
withdraw_lamports, stake_split_from.lamports());
2553+
let split_from_lamports = stake_split_from.lamports();
2554+
let upper_bound = split_from_lamports.saturating_add(lamports_per_pool_token);
2555+
if withdraw_lamports < split_from_lamports || withdraw_lamports > upper_bound {
2556+
msg!(
2557+
"Cannot withdraw a whole account worth {} lamports, \
2558+
must withdraw at least {} lamports worth of pool tokens \
2559+
with a margin of {} lamports",
2560+
withdraw_lamports,
2561+
split_from_lamports,
2562+
lamports_per_pool_token
2563+
);
25542564
return Err(StakePoolError::StakeLamportsNotEqualToMinimum.into());
25552565
}
2566+
// truncate the lamports down to the amount in the account
2567+
withdraw_lamports = split_from_lamports;
25562568
}
25572569
}
25582570
Some((validator_stake_info, withdraw_source))

stake-pool/program/src/state.rs

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,15 @@ impl StakePool {
255255
}
256256
}
257257

258+
/// Get the current value of pool tokens, rounded up
259+
#[inline]
260+
pub fn get_lamports_per_pool_token(&self) -> Option<u64> {
261+
self.total_lamports
262+
.checked_add(self.pool_token_supply)?
263+
.checked_sub(1)?
264+
.checked_div(self.pool_token_supply)
265+
}
266+
258267
/// Checks that the withdraw or deposit authority is valid
259268
fn check_program_derived_authority(
260269
authority_address: &Pubkey,
@@ -660,24 +669,24 @@ impl ValidatorStakeInfo {
660669

661670
/// Performs a very cheap comparison, for checking if this validator stake
662671
/// info matches the vote account address
663-
pub fn memcmp_pubkey(data: &[u8], vote_address_bytes: &[u8]) -> bool {
672+
pub fn memcmp_pubkey(data: &[u8], vote_address: &Pubkey) -> bool {
664673
sol_memcmp(
665674
&data[41..41 + PUBKEY_BYTES],
666-
vote_address_bytes,
675+
vote_address.as_ref(),
667676
PUBKEY_BYTES,
668677
) == 0
669678
}
670679

671-
/// Performs a very cheap comparison, for checking if this validator stake
672-
/// info does not have active lamports equal to the given bytes
673-
pub fn active_lamports_not_equal(data: &[u8], lamports_le_bytes: &[u8]) -> bool {
674-
sol_memcmp(&data[0..8], lamports_le_bytes, 8) != 0
680+
/// Performs a comparison, used to check if this validator stake
681+
/// info has more active lamports than some limit
682+
pub fn active_lamports_greater_than(data: &[u8], lamports: &u64) -> bool {
683+
u64::try_from_slice(&data[0..8]).unwrap() > *lamports
675684
}
676685

677-
/// Performs a very cheap comparison, for checking if this validator stake
678-
/// info does not have lamports equal to the given bytes
679-
pub fn transient_lamports_not_equal(data: &[u8], lamports_le_bytes: &[u8]) -> bool {
680-
sol_memcmp(&data[8..16], lamports_le_bytes, 8) != 0
686+
/// Performs a comparison, used to check if this validator stake
687+
/// info has more transient lamports than some limit
688+
pub fn transient_lamports_greater_than(data: &[u8], lamports: &u64) -> bool {
689+
u64::try_from_slice(&data[8..16]).unwrap() > *lamports
681690
}
682691

683692
/// Check that the validator stake info is valid

stake-pool/program/tests/huge_pool.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use {
2020
},
2121
};
2222

23-
const HUGE_POOL_SIZE: u32 = 2_000;
23+
const HUGE_POOL_SIZE: u32 = 3_300;
2424
const STAKE_AMOUNT: u64 = 200_000_000_000;
2525

2626
async fn setup(

stake-pool/program/tests/update_validator_list_balance.rs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,11 @@ async fn setup(
9696
// Warp forward so the stakes properly activate, and deposit
9797
slot += slots_per_epoch;
9898
context.warp_to_slot(slot).unwrap();
99+
let last_blockhash = context
100+
.banks_client
101+
.get_new_latest_blockhash(&context.last_blockhash)
102+
.await
103+
.unwrap();
99104

100105
stake_pool_accounts
101106
.update_all(
@@ -111,12 +116,6 @@ async fn setup(
111116
)
112117
.await;
113118

114-
let last_blockhash = context
115-
.banks_client
116-
.get_new_latest_blockhash(&context.last_blockhash)
117-
.await
118-
.unwrap();
119-
120119
for deposit_account in &mut deposit_accounts {
121120
deposit_account
122121
.deposit_stake(
@@ -130,6 +129,11 @@ async fn setup(
130129

131130
slot += slots_per_epoch;
132131
context.warp_to_slot(slot).unwrap();
132+
let last_blockhash = context
133+
.banks_client
134+
.get_new_latest_blockhash(&context.last_blockhash)
135+
.await
136+
.unwrap();
133137

134138
stake_pool_accounts
135139
.update_all(
@@ -418,6 +422,11 @@ async fn merge_into_validator_stake() {
418422

419423
// Warp just a little bit to get a new blockhash and update again
420424
context.warp_to_slot(slot + 10).unwrap();
425+
let last_blockhash = context
426+
.banks_client
427+
.get_new_latest_blockhash(&last_blockhash)
428+
.await
429+
.unwrap();
421430

422431
// Update, should not change, no merges yet
423432
let error = stake_pool_accounts

0 commit comments

Comments
 (0)