Skip to content
Open
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
5 changes: 5 additions & 0 deletions program/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ pub enum SinglePoolError {
/// Attempted to initialize a pool that is already initialized.
#[error("PoolAlreadyInitialized")]
PoolAlreadyInitialized,
/// There aren't enough excess lamps (i.e. MEV rewards) to create a temp account
#[error("NotEnoughExcessLamps")]
InsufficientExcessLamports,
}
impl From<SinglePoolError> for ProgramError {
fn from(e: SinglePoolError) -> Self {
Expand Down Expand Up @@ -152,6 +155,8 @@ impl PrintProgramError for SinglePoolError {
msg!("Error: Attempted to deposit from or withdraw to pool stake account."),
SinglePoolError::PoolAlreadyInitialized =>
msg!("Error: Attempted to initialize a pool that is already initialized."),
SinglePoolError::InsufficientExcessLamports =>
msg!("Error: There are not enough excess lamps in the pool to init a temporary staking account."),
}
}
}
23 changes: 23 additions & 0 deletions program/src/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,29 @@ pub enum SinglePoolInstruction {
/// URI of the uploaded metadata of the spl-token
uri: String,
},
// TODO comment
/// 0. `[]` Pool account
/// 1. `[w]` Temp stake account
/// 3. `[]` Pool stake authority
/// 4. `[]` Vote account
/// 5. `[]` Rent sysvar
/// 6. `[]` Clock sysvar
/// 7. `[]` Stake history sysvar
/// 8. `[]` Stake config sysvar
/// 9. `[]` System program
/// 10. `[]` Stake program
InitializeTempStake,

// TODO comment
/// 0. `[]` Pool account
/// 1. `[w]` Pool stake account
/// 2. `[w]` Temp stake account
/// 3. `[]` Pool stake authority
/// 4. `[]` Clock sysvar
/// 5. `[]` Stake history sysvar
/// 6. `[]` System program
/// 7. `[]` Stake program
ProcessMergeTempStake,
}

/// Creates all necessary instructions to initialize the stake pool.
Expand Down
5 changes: 5 additions & 0 deletions program/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ solana_program::declare_id!("SVSPxpvHdN29nkVg9rPapPNDddN5DipNLRUFhyjFThE");

const POOL_PREFIX: &[u8] = b"pool";
const POOL_STAKE_PREFIX: &[u8] = b"stake";
const POOL_TEMP_STAKE_PREFIX: &[u8] = b"temp_stake";
const POOL_MINT_PREFIX: &[u8] = b"mint";
const POOL_MINT_AUTHORITY_PREFIX: &[u8] = b"mint_authority";
const POOL_STAKE_AUTHORITY_PREFIX: &[u8] = b"stake_authority";
Expand All @@ -39,6 +40,10 @@ fn find_pool_stake_address_and_bump(program_id: &Pubkey, pool_address: &Pubkey)
Pubkey::find_program_address(&[POOL_STAKE_PREFIX, pool_address.as_ref()], program_id)
}

fn find_pool_temp_stake_address_and_bump(program_id: &Pubkey, pool_address: &Pubkey) -> (Pubkey, u8) {
Pubkey::find_program_address(&[POOL_TEMP_STAKE_PREFIX, pool_address.as_ref()], program_id)
}

fn find_pool_mint_address_and_bump(program_id: &Pubkey, pool_address: &Pubkey) -> (Pubkey, u8) {
Pubkey::find_program_address(&[POOL_MINT_PREFIX, pool_address.as_ref()], program_id)
}
Expand Down
243 changes: 242 additions & 1 deletion program/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use {
instruction::SinglePoolInstruction,
state::{SinglePool, SinglePoolAccountType},
MINT_DECIMALS, POOL_MINT_AUTHORITY_PREFIX, POOL_MINT_PREFIX, POOL_MPL_AUTHORITY_PREFIX,
POOL_PREFIX, POOL_STAKE_AUTHORITY_PREFIX, POOL_STAKE_PREFIX,
POOL_PREFIX, POOL_STAKE_AUTHORITY_PREFIX, POOL_STAKE_PREFIX, POOL_TEMP_STAKE_PREFIX,
VOTE_STATE_AUTHORIZED_WITHDRAWER_END, VOTE_STATE_AUTHORIZED_WITHDRAWER_START,
VOTE_STATE_DISCRIMINATOR_END,
},
Expand Down Expand Up @@ -128,6 +128,22 @@ fn check_pool_stake_address(
)
}

/// Check pool stake account address for the pool account
fn check_pool_temp_stake_address(
program_id: &Pubkey,
pool_address: &Pubkey,
check_address: &Pubkey,
) -> Result<u8, ProgramError> {
check_pool_pda(
program_id,
pool_address,
check_address,
&crate::find_pool_temp_stake_address_and_bump,
"temp stake account",
SinglePoolError::InvalidPoolStakeAccount,
)
}

/// Check pool mint address for the pool account
fn check_pool_mint_address(
program_id: &Pubkey,
Expand Down Expand Up @@ -711,6 +727,221 @@ impl Processor {
Ok(())
}

/*
Starts the MEV rewards flywheel
* Creates a "temp" staking account
* Move all the excess lamps from the pool into the temp account
* Delegate the temp staking account to the validator
*/
fn process_initialize_temp_stake(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
// The main svsp pool account (to tie the temp stake to the pool)
let pool_info = next_account_info(account_info_iter)?;
// The svsp pool's stake account (where MEV lamps are sent). Owned by the stake program (not svsp)
let pool_stake_info = next_account_info(account_info_iter)?;
// The temporary stake account that will stake the excess lamports while they activate
let temp_stake_info = next_account_info(account_info_iter)?;
// Temp pool uses the same authority as pool stake authority
let pool_stake_authority_info = next_account_info(account_info_iter)?;
let vote_account_info = next_account_info(account_info_iter)?;
let rent_info = next_account_info(account_info_iter)?;
let rent = &Rent::from_account_info(rent_info)?;
let clock_info = next_account_info(account_info_iter)?;
let stake_history_info = next_account_info(account_info_iter)?;
let stake_config_info = next_account_info(account_info_iter)?;
let system_program_info = next_account_info(account_info_iter)?;
let stake_program_info = next_account_info(account_info_iter)?;

let stake_authority_bump_seed = check_pool_stake_authority_address(
program_id,
pool_info.key,
pool_stake_authority_info.key,
)?;
check_pool_stake_address(program_id, pool_info.key, pool_stake_info.key)?;
check_system_program(system_program_info.key)?;
check_stake_program(stake_program_info.key)?;

// Derive the temporary stake account address using a new prefix (POOL_TEMP_STAKE_PREFIX)
let temp_stake_bump_seed =
check_pool_temp_stake_address(program_id, pool_info.key, temp_stake_info.key)?;
let temp_stake_seeds = &[
POOL_TEMP_STAKE_PREFIX,
pool_info.key.as_ref(),
&[temp_stake_bump_seed],
];
let temp_stake_signers = &[&temp_stake_seeds[..]];

let stake_authority_seeds = &[
POOL_STAKE_AUTHORITY_PREFIX,
pool_info.key.as_ref(),
&[stake_authority_bump_seed],
];
let stake_authority_signers = &[&stake_authority_seeds[..]];

let stake_space = std::mem::size_of::<stake::state::StakeStateV2>();

// Create the temp stake account (Note: this will fail if it already exists, by design, as
// the account is closed after merged back into the main pool. If it already exists, and the
// stake is active, do that instead. If the stake is activating, then there should be no
// lamps here, unless you sent them manually, and in that case, wait until the next epoch!)
let authorized = stake::state::Authorized::auto(pool_stake_authority_info.key);

invoke_signed(
&system_instruction::allocate(temp_stake_info.key, stake_space as u64),
&[temp_stake_info.clone()],
temp_stake_signers,
)?;
invoke_signed(
&system_instruction::assign(temp_stake_info.key, stake_program_info.key),
&[temp_stake_info.clone()],
temp_stake_signers,
)?;
// Note: Expects rent for temp_stake_info to already be paid!
invoke_signed(
&stake::instruction::initialize_checked(temp_stake_info.key, &authorized),
&[
temp_stake_info.clone(),
rent_info.clone(),
pool_stake_authority_info.clone(),
pool_stake_authority_info.clone(),
],
stake_authority_signers,
)?;

let (_, pool_stake_state) = get_stake_state(pool_stake_info)?;
let pre_pool_stake = pool_stake_state.delegation.stake;
let pre_pool_lamps = pool_stake_info.lamports();
let to_withdraw = pre_pool_lamps - pre_pool_stake - rent.minimum_balance(stake_space);

Self::stake_withdraw(
pool_info.key,
pool_stake_info.clone(),
pool_stake_authority_info.clone(),
stake_authority_bump_seed,
temp_stake_info.clone(),
clock_info.clone(),
stake_history_info.clone(),
to_withdraw,
)?;

// The temporary stake account must hold lamports in excess of the min delegation + rent If
// this is not true, wait until a future epoch after more MEV has accumulated.
let minimum_delegation = minimum_delegation()?;
let stake_rent_plus_initial = rent
.minimum_balance(stake_space)
.saturating_add(minimum_delegation);
if temp_stake_info.lamports() <= stake_rent_plus_initial {
return Err(SinglePoolError::InsufficientExcessLamports.into());
}

// TODO send the cranker a little SOL for bothering to crank this?

// Delegate the temporary stake account so that its excess lamports become activated.
invoke_signed(
&stake::instruction::delegate_stake(
temp_stake_info.key,
pool_stake_authority_info.key,
vote_account_info.key,
),
&[
temp_stake_info.clone(),
vote_account_info.clone(),
clock_info.clone(),
stake_history_info.clone(),
stake_config_info.clone(),
pool_stake_authority_info.clone(),
],
stake_authority_signers,
)?;

Ok(())
}

fn process_merge_temp_stake(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let pool_info = next_account_info(account_info_iter)?;
let pool_stake_info = next_account_info(account_info_iter)?;
let temp_stake_info = next_account_info(account_info_iter)?;
let pool_stake_authority_info = next_account_info(account_info_iter)?;
let clock_info = next_account_info(account_info_iter)?;
let clock = &Clock::from_account_info(clock_info)?;
let stake_history_info = next_account_info(account_info_iter)?;
let system_program_info = next_account_info(account_info_iter)?;
let stake_program_info = next_account_info(account_info_iter)?;

SinglePool::from_account_info(pool_info, program_id)?;

check_pool_temp_stake_address(program_id, pool_info.key, temp_stake_info.key)?;
let stake_authority_bump_seed = check_pool_stake_authority_address(
program_id,
pool_info.key,
pool_stake_authority_info.key,
)?;
check_system_program(system_program_info.key)?;
check_stake_program(stake_program_info.key)?;

let minimum_delegation = minimum_delegation()?;

let (_, pool_stake_state) = get_stake_state(pool_stake_info)?;
let pre_pool_stake = pool_stake_state
.delegation
.stake
.saturating_sub(minimum_delegation);
msg!("Available stake pre merge {}", pre_pool_stake);

// Fails if the temp stake isn't in the same activity state. Typically you must wait until
// the temp pool activates (i.e., one epoch) to call this ix
let (temp_stake_meta, temp_stake_state) = get_stake_state(temp_stake_info)?;
if temp_stake_meta.authorized
!= stake::state::Authorized::auto(pool_stake_authority_info.key)
|| is_stake_active_without_history(&pool_stake_state, clock.epoch)
!= is_stake_active_without_history(&temp_stake_state, clock.epoch)
{
return Err(SinglePoolError::WrongStakeStake.into());
}

// Merge the temporary stake into the main pool stake.
Self::stake_merge(
pool_info.key,
temp_stake_info.clone(),
pool_stake_authority_info.clone(),
stake_authority_bump_seed,
pool_stake_info.clone(),
clock_info.clone(),
stake_history_info.clone(),
)?;

// We keep this here for the program log...
let (_pool_stake_meta, pool_stake_state) = get_stake_state(pool_stake_info)?;
let post_pool_stake = pool_stake_state
.delegation
.stake
.saturating_sub(minimum_delegation);
// let post_pool_lamports = pool_stake_info.lamports();
msg!("Available stake post merge {}", post_pool_stake);

// Any remaining lamps in the temp account (like rent) are given back to the pool
let mut pool_lamps = pool_stake_info.try_borrow_mut_lamports()?;
let mut temp_lamps = temp_stake_info.try_borrow_mut_lamports()?;
let lamps_after = pool_lamps.clone().checked_add(temp_lamps.clone()).unwrap();
**pool_lamps = lamps_after;
**temp_lamps = 0;

// Reassign the now-drained temp stake account to the system program so it can be reclaimed.
invoke_signed(
&system_instruction::assign(temp_stake_info.key, &system_program::ID),
&[temp_stake_info.clone()],
&[],
)?;

Ok(())
}

// TODO we might need a reactivate for the temp pool...

fn process_reactivate_pool_stake(
program_id: &Pubkey,
accounts: &[AccountInfo],
Expand Down Expand Up @@ -772,6 +1003,8 @@ impl Processor {
Ok(())
}

// TODO we should make sure `process_merge_temp_stake` executes before this (otherwise an
// attacker can deposit -> merge temp -> withdraw to get unearned MEV)
fn process_deposit_stake(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let pool_info = next_account_info(account_info_iter)?;
Expand Down Expand Up @@ -1212,6 +1445,14 @@ impl Processor {
msg!("Instruction: UpdateTokenMetadata");
Self::process_update_pool_token_metadata(program_id, accounts, name, symbol, uri)
}
SinglePoolInstruction::InitializeTempStake => {
msg!("Instruction: InitializeTempPool");
Self::process_initialize_temp_stake(program_id, accounts)
}
SinglePoolInstruction::ProcessMergeTempStake => {
msg!("Instruction: MergeTempStake");
Self::process_merge_temp_stake(program_id, accounts)
}
}
}
}
Expand Down