Uninitialized transient stake account can be stolen
Description
When a solo validator bond has matured, a user can call the handle_maturity instruction to mark the bond as matured. The instruction also handles transient stake accounts, merging the transient stake account into the bond's stake account if it is fully active:
// ...
let transient_stake_status = get_stake_status(
&ctx.accounts.transient_stake_account,
ctx.accounts.clock.epoch,
&ctx.accounts.stake_history,
)?;
// Handle existing transient account
match transient_stake_status {
StakeStatus::FullyActive => {
// merge it with the main stake account
ctx.accounts.stake_merge(
ctx.accounts.stake_account.to_account_info(),
ctx.accounts.transient_stake_account.to_account_info(),
ctx.accounts.bond.to_account_info(),
&ctx.accounts.bond,
)?;
}
StakeStatus::Empty => return Ok(()),
_ => return Err(ErrorCode::UnsupportedStakeState.into()),
}
// ...Merging the transient stake account sets its stake state to uninitialized and relocates all of its lamports to the bond's stake account.
Once the transaction is finished, since the transient stake account has zero lamports, the account will be closed as it does not have enough to cover rent.
However, an attacker can stop the transient stake account from being closed by sending enough lamports for rent in the same transaction. This leaves the transient stake account in the uninitialized state. An attacker could then reinitialize the transient stake account, stealing it by setting their own account as the authorized staker and withdrawer.
Impact
When redeeming yield tokens (YTs) for lamports or stake, the SoloValidatorBond::calculate_lamports_for_yt method is used to calculate the yield for YTs, which includes the lamports of the transient stake account in its calculations.
If an attacker owned the transient stake account, they could deposit lamports into the transient stake account, redeem their YT for lamports or stake, and then immediately withdraw their deposit from the transient stake account.
Since the deposited lamports are taken into account in the yield calculations, they get a larger return on their YT than intended at no cost.
This could be used to steal funds from the bond's stake account or drain the global counter party (GCP).
Recommendations
Rework the bond implementation to allow for multiple transient stake accounts and not depend on their balance for yield calculations.
Remediation
This issue has been acknowledged by Pye in the Sky Labs Ltd., and fixes were implemented in the following commits: