Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 20 additions & 6 deletions clients/cli/src/quarantine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ pub async fn get_available_balances(
stake_account_addresses: &[Pubkey],
minimum_pool_balance: u64,
) -> Result<Vec<(u64, u64)>, Error> {
let rent_exempt_reserve = config
.program_client
.get_minimum_balance_for_rent_exemption(StakeStateV2::size_of())
.await?;

let stake_accounts = config
.rpc_client
.get_multiple_accounts(stake_account_addresses)
Expand All @@ -71,16 +76,25 @@ pub async fn get_available_balances(
let mut delegations = vec![];
for stake_account in &stake_accounts {
let delegation = if let Some(account) = stake_account {
// if this assert ever triggers, multistake or another account change has landed
// this function should be updated to use real stake account sizes
// we may be fetching hundreds of accounts here so memoize the rents
assert_eq!(
account.data.len(),
StakeStateV2::size_of(),
"StakeStateV2 is no longer canonical, or StakeStateV2::size_of() is no longer 200."
);

match bincode::deserialize::<StakeStateV2>(&account.data) {
Ok(StakeStateV2::Stake(meta, stake, _)) => (
Ok(StakeStateV2::Stake(_, stake, _)) => (
stake.delegation.stake.saturating_sub(minimum_pool_balance),
account
.lamports
.saturating_sub(stake.delegation.stake)
.saturating_sub(meta.rent_exempt_reserve),
.saturating_sub(rent_exempt_reserve),
),
Ok(StakeStateV2::Initialized(meta)) => {
(0, account.lamports.saturating_sub(meta.rent_exempt_reserve))
Ok(StakeStateV2::Initialized(_)) => {
(0, account.lamports.saturating_sub(rent_exempt_reserve))
}
_ => unreachable!(),
}
Expand Down Expand Up @@ -125,14 +139,14 @@ pub async fn create_uninitialized_stake_account_instruction(
) -> Result<Instruction, Error> {
let rent_amount = config
.program_client
.get_minimum_balance_for_rent_exemption(std::mem::size_of::<StakeStateV2>())
.get_minimum_balance_for_rent_exemption(StakeStateV2::size_of())
.await?;

Ok(system_instruction::create_account(
payer,
stake_account,
rent_amount,
std::mem::size_of::<StakeStateV2>() as u64,
StakeStateV2::size_of() as u64,
&stake::program::id(),
))
}
29 changes: 18 additions & 11 deletions program/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ use {
sysvar::stake_history::StakeHistorySysvar,
},
solana_system_interface::{instruction as system_instruction, program as system_program},
solana_sysvar::SysvarSerialize,
solana_sysvar::{Sysvar, SysvarSerialize},
solana_vote_interface::program as vote_program,
spl_token_interface::{self as spl_token, state::Mint},
};
Expand Down Expand Up @@ -766,8 +766,13 @@ impl Processor {
)?;
check_stake_program(stake_program_info.key)?;

// we expect these numbers to be equal but get them separately in case of future changes
let rent = Rent::get()?;
let pool_rent_exempt_reserve = rent.minimum_balance(pool_stake_info.data_len());
let onramp_rent_exempt_reserve = rent.minimum_balance(pool_onramp_info.data_len());

// get main pool account, we require it to be fully active for most operations
let (pool_stake_meta, pool_stake_state) = get_stake_state(pool_stake_info)?;
let (_, pool_stake_state) = get_stake_state(pool_stake_info)?;
let pool_stake_status = pool_stake_state
.delegation
.stake_activating_and_deactivating(
Expand All @@ -779,17 +784,16 @@ impl Processor {

// get on-ramp and its status. we have to match because unlike the main account it could be Initialized
// if it doesnt exist, it must first be created with InitializePoolOnRamp
let (option_onramp_status, onramp_deactivation_epoch, onramp_rent_exempt_reserve) =
let (option_onramp_status, onramp_deactivation_epoch) =
match try_from_slice_unchecked::<StakeStateV2>(&pool_onramp_info.data.borrow()) {
Ok(StakeStateV2::Initialized(meta)) => (None, u64::MAX, meta.rent_exempt_reserve),
Ok(StakeStateV2::Stake(meta, stake, _)) => (
Ok(StakeStateV2::Initialized(_)) => (None, u64::MAX),
Ok(StakeStateV2::Stake(_, stake, _)) => (
Some(stake.delegation.stake_activating_and_deactivating(
clock.epoch,
stake_history,
PERPETUAL_NEW_WARMUP_COOLDOWN_RATE_EPOCH,
)),
stake.delegation.deactivation_epoch,
meta.rent_exempt_reserve,
),
_ => return Err(SinglePoolError::OnRampDoesntExist.into()),
};
Expand Down Expand Up @@ -831,7 +835,7 @@ impl Processor {
let pool_excess_lamports = pool_stake_info
.lamports()
.saturating_sub(pool_stake_state.delegation.stake)
.saturating_sub(pool_stake_meta.rent_exempt_reserve);
.saturating_sub(pool_rent_exempt_reserve);

// if the on-ramp is fully active, move its stake to the main pool account
if let Some(ref onramp_status) = option_onramp_status {
Expand Down Expand Up @@ -975,17 +979,20 @@ impl Processor {
return Err(SinglePoolError::InvalidPoolStakeAccountUsage.into());
}

let rent = Rent::get()?;
let pool_rent_exempt_reserve = rent.minimum_balance(pool_stake_info.data_len());

let minimum_pool_balance = minimum_pool_balance()?;

let (pool_stake_meta, pool_stake_state) = get_stake_state(pool_stake_info)?;
let (_, pool_stake_state) = get_stake_state(pool_stake_info)?;
let pre_pool_stake = pool_stake_state
.delegation
.stake
.saturating_sub(minimum_pool_balance);
let pre_pool_excess_lamports = pool_stake_info
.lamports()
.checked_sub(pool_stake_state.delegation.stake)
.and_then(|amount| amount.checked_sub(pool_stake_meta.rent_exempt_reserve))
.and_then(|amount| amount.checked_sub(pool_rent_exempt_reserve))
.ok_or(SinglePoolError::ArithmeticOverflow)?;
msg!("Available stake pre merge {}", pre_pool_stake);

Expand Down Expand Up @@ -1013,7 +1020,7 @@ impl Processor {
stake_history_info.clone(),
)?;

let (pool_stake_meta, pool_stake_state) = get_stake_state(pool_stake_info)?;
let (_, pool_stake_state) = get_stake_state(pool_stake_info)?;
let post_pool_stake = pool_stake_state
.delegation
.stake
Expand All @@ -1030,7 +1037,7 @@ impl Processor {
// this includes their rent-exempt reserve if the pool is fully active
let user_excess_lamports = post_pool_lamports
.checked_sub(pool_stake_state.delegation.stake)
.and_then(|amount| amount.checked_sub(pool_stake_meta.rent_exempt_reserve))
.and_then(|amount| amount.checked_sub(pool_rent_exempt_reserve))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noting that this is assuming a stake account will be the same size post-merge, which makes sense to me

Copy link
Member Author

@2501babe 2501babe Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, it isnt articulated by the stake program but this would be in line with the general principle of current behavior. if you Split, the new account has the currently enforced size asserted and both accounts must meet minimum delegation. if you Merge, we dont enforce size and we dont enforce minimum delegation

the principle i read into this is that an old Merge destination is allowed to continue as it was before, since closing the source is good, and the destination would have kept on existing as it was if there was no merge. so i dont think we would ever change this if we made larger stake accounts

.and_then(|amount| amount.checked_sub(pre_pool_excess_lamports))
.ok_or(SinglePoolError::ArithmeticOverflow)?;

Expand Down
16 changes: 8 additions & 8 deletions program/tests/deposit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use {
helpers::*,
solana_program_test::*,
solana_sdk::{signature::Signer, signer::keypair::Keypair, transaction::Transaction},
solana_stake_interface::state::{Authorized, Lockup},
solana_stake_interface::state::{Authorized, Lockup, StakeStateV2},
solana_system_interface::instruction as system_instruction,
spl_associated_token_account_interface::address::get_associated_token_address,
spl_single_pool::{error::SinglePoolError, id, instruction},
Expand Down Expand Up @@ -38,6 +38,9 @@ async fn success(
};
let mut context = program_test.start_with_context().await;

let rent = context.banks_client.get_rent().await.unwrap();
let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of());

let accounts = SinglePoolAccounts::default();
accounts
.initialize_for_deposit(
Expand Down Expand Up @@ -107,7 +110,7 @@ async fn success(
.unwrap();
}

let (alice_meta_before_deposit, alice_stake_before_deposit, _) =
let (_, alice_stake_before_deposit, _) =
get_stake_account(&mut context.banks_client, &accounts.alice_stake.pubkey()).await;
let alice_stake_before_deposit = alice_stake_before_deposit.unwrap().delegation.stake;

Expand Down Expand Up @@ -141,7 +144,7 @@ async fn success(
.await
.lamports;

let (pool_meta_after, pool_stake_after, pool_lamports_after) =
let (_, pool_stake_after, pool_lamports_after) =
get_stake_account(&mut context.banks_client, &accounts.stake_account).await;
let pool_stake_after = pool_stake_after.unwrap().delegation.stake;

Expand All @@ -150,7 +153,7 @@ async fn success(
let expected_deposit = if activate {
alice_stake_before_deposit
} else {
alice_stake_before_deposit + alice_meta_before_deposit.rent_exempt_reserve
alice_stake_before_deposit + rent_exempt_reserve
};

// deposit stake account is closed
Expand All @@ -168,10 +171,7 @@ async fn success(
assert_eq!(pool_lamports_after, pool_lamports_before + expected_deposit);
assert_eq!(
pool_lamports_after,
pool_stake_before
+ expected_deposit
+ pool_meta_after.rent_exempt_reserve
+ pool_extra_lamports,
pool_stake_before + expected_deposit + rent_exempt_reserve + pool_extra_lamports,
);

// alice got her rent and extra back if active, or just extra back otherwise
Expand Down
10 changes: 6 additions & 4 deletions program/tests/replenish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,10 @@ async fn move_value_success(
};
let mut context = program_test.start_with_context().await;

let rent = context.banks_client.get_rent().await.unwrap();
let pool_rent = rent.minimum_balance(StakeStateV2::size_of());
let onramp_rent = pool_rent;

let accounts = SinglePoolAccounts::default();
accounts
.initialize_for_deposit(&mut context, TEST_STAKE_AMOUNT, None)
Expand Down Expand Up @@ -297,15 +301,14 @@ async fn move_value_success(
.await
.unwrap();

let (pool_meta, pool_stake, pool_lamports) =
let (_, pool_stake, pool_lamports) =
get_stake_account(&mut context.banks_client, &accounts.stake_account).await;
let pool_status = pool_stake
.unwrap()
.delegation
.stake_activating_and_deactivating(clock.epoch, &stake_history, Some(0));
let pool_rent = pool_meta.rent_exempt_reserve;

let (onramp_meta, onramp_stake, onramp_lamports) =
let (_, onramp_stake, onramp_lamports) =
get_stake_account(&mut context.banks_client, &accounts.onramp_account).await;
let onramp_status = onramp_stake
.map(|stake| {
Expand All @@ -314,7 +317,6 @@ async fn move_value_success(
.stake_activating_and_deactivating(clock.epoch, &stake_history, Some(0))
})
.unwrap_or_default();
let onramp_rent = onramp_meta.rent_exempt_reserve;

match (onramp_state, move_lamports_to_onramp) {
// stake moved already before test or because of test, new lamports were added to onramp
Expand Down