Skip to content

program: use stake history skillfully#586

Merged
2501babe merged 2 commits intosolana-program:mainfrom
2501babe:20260401_stakehistory
Apr 3, 2026
Merged

program: use stake history skillfully#586
2501babe merged 2 commits intosolana-program:mainfrom
2501babe:20260401_stakehistory

Conversation

@2501babe
Copy link
Copy Markdown
Member

@2501babe 2501babe commented Apr 1, 2026

currently DepositStake eyeballs whether the pool is active based on Clock and relies on Merge to gatekeep bad state transitions. this pr changes it to fully rely on StakeHistory to make its own correct decisions

this also fixes a couple harmless but strange bugs:

  • a user could deposit a deactivated, but not an initialized, stake into an activating pool because we deserialized expecting StakeStateV2::Stake. this now succeeds
  • depositing a deactivated stake into a deactivated pool failed, but only due to an interaction between an esoteric quirk of the stake program and the particulars of our token accounting. this still fails, but much more cleanly

importantly, without this pr, it is likely that any reasonable design for #581 lamport-based accounting would allow deposits to an inactive pool, so we desire to strictly prevent this

we also test DepositStake much more thoroughly. as noted below we have to disable some of our js tests but they will be reenabled with #587

closes #103

@2501babe 2501babe force-pushed the 20260401_stakehistory branch from 9bc3d12 to edbd28c Compare April 1, 2026 12:58
@2501babe 2501babe self-assigned this Apr 1, 2026
@2501babe 2501babe force-pushed the 20260401_stakehistory branch from 21ca8af to d7223a7 Compare April 1, 2026 16:53
Comment on lines +258 to +260
*/

t.true(true);
Copy link
Copy Markdown
Member Author

@2501babe 2501babe Apr 1, 2026

Choose a reason for hiding this comment

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

i disabled the deposit, deposit default, and withdraw js tests for now. i think we should land this pr as-is and fix js after, since this pr is important, the js is less important, and neither the js, nor the program interface these test the js adheres to, are changing

bankrun is deprecated and still on solana 1.x, so i will have to port everything to litesvm. deposit fails now because i use sol_get_sysvar for stake history, which was added in solana 2.x. the withdraw test fails because it has to deposit, withdraw itself is unchanged by this pr

@2501babe 2501babe force-pushed the 20260401_stakehistory branch 3 times, most recently from b8a1fc3 to e275f03 Compare April 1, 2026 17:30
Comment on lines +133 to +154
// inactive deposit into deactivated pool fails
let instructions = instruction::deposit(
&id(),
&accounts.pool,
&accounts.bob_stake.pubkey(),
&accounts.bob_token,
&accounts.bob.pubkey(),
&accounts.bob.pubkey(),
);
let transaction = Transaction::new_signed_with_payer(
&instructions,
Some(&context.payer.pubkey()),
&[&context.payer, &accounts.bob],
context.last_blockhash,
);

let e = context
.banks_client
.process_transaction(transaction)
.await
.unwrap_err();
check_error(e, SinglePoolError::ReplenishRequired);
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

you know, i really thought this would incorrectly succeed against the current version of svsp. but when i patched this test back into master, i discovered it gives SinglePoolError::DepositTooSmall. there is no svsp error blocking the inactive/inactive merge. there is no stake program error either! the merge succeeds

... but then, because merge does not modify the delegation of the inactive stake, svsp decides every lamport is an excess lamport, resulting in a zero-deposit

its times like this i appreciate what man truly hath wrought when he invented computer programming

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What's honestly more impressive is that we can make sense of these things 😅

@2501babe 2501babe force-pushed the 20260401_stakehistory branch from ae62518 to 31c85c9 Compare April 1, 2026 20:31
2501babe added 2 commits April 1, 2026 14:03
* `DepositStake` allows all mergeable sources when pool is activating
* `DepositStake` properly aborts when pool is inactive
* `DepositStake` errors helpfully on user lockup instead of falling through to `Merge`
* `DepositStake` takes stake history into account correctly
@2501babe 2501babe force-pushed the 20260401_stakehistory branch from 31c85c9 to ebc37b2 Compare April 1, 2026 21:04
Comment on lines +1191 to +1197
// we deliberately do NOT validate the activation status of the pool account.
// neither snow nor rain nor warmup/cooldown nor validator delinquency prevents a user withdrawal
//
// NOTE this is fine for stake v4 but subtly wrong for stake v5 *if* the pool account was deactivated.
// stake v5 declines to (meaninglessly) adjust delegations of deactivated sources.
// this will (again) be correct with #581, which shifts to NEV accounting on lamports rather than stake.
// we should plan another SVSP release before stake v5 activation
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

pointing this out for attention. all the code changes in the pr concern DepositStake, this comment is in WithdrawStake

Comment on lines +956 to +957
let stake_history = &StakeHistorySysvar(clock.epoch);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

this syscall really is the gift that keeps on giving 😁

@2501babe 2501babe marked this pull request as ready for review April 1, 2026 21:33
@2501babe 2501babe requested a review from joncinque April 1, 2026 21:33
Copy link
Copy Markdown
Contributor

@joncinque joncinque left a comment

Choose a reason for hiding this comment

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

Looks great!

Comment on lines +133 to +154
// inactive deposit into deactivated pool fails
let instructions = instruction::deposit(
&id(),
&accounts.pool,
&accounts.bob_stake.pubkey(),
&accounts.bob_token,
&accounts.bob.pubkey(),
&accounts.bob.pubkey(),
);
let transaction = Transaction::new_signed_with_payer(
&instructions,
Some(&context.payer.pubkey()),
&[&context.payer, &accounts.bob],
context.last_blockhash,
);

let e = context
.banks_client
.process_transaction(transaction)
.await
.unwrap_err();
check_error(e, SinglePoolError::ReplenishRequired);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What's honestly more impressive is that we can make sense of these things 😅

.await
.unwrap_err();
check_error(e, SinglePoolError::WrongStakeState);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is very slick, and covers all of the cases, from what I can see. The comment about deactivating pools being covered with replenish is extremely helpful too.

@2501babe 2501babe merged commit d5f18a3 into solana-program:main Apr 3, 2026
22 checks passed
@2501babe 2501babe deleted the 20260401_stakehistory branch April 3, 2026 00:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

program: use stake history

2 participants