Skip to content

Commit fa0b257

Browse files
committed
program: implement DepositSol
`DepositSol` is a new instruction that allows depositing liquid sol directly into the pool onramp for a fee, calculated by underminting tokens to spread the benefit to all existing holders
1 parent 6159dba commit fa0b257

8 files changed

Lines changed: 599 additions & 27 deletions

File tree

program/src/error.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,9 @@ pub enum SinglePoolError {
110110
/// This can only occur if the Stake Program minimum delegation increases above 1 sol.
111111
#[error("WithdrawalViolatesPoolRequirements")]
112112
WithdrawalViolatesPoolRequirements,
113+
/// The user-owned lamport source cannot be validated for `DepositSol`.
114+
#[error("InvalidDepositSolSource")]
115+
InvalidDepositSolSource,
113116
}
114117
impl From<SinglePoolError> for ProgramError {
115118
fn from(e: SinglePoolError) -> Self {
@@ -171,6 +174,8 @@ impl ToStr for SinglePoolError {
171174
SinglePoolError::WithdrawalViolatesPoolRequirements =>
172175
"Error: Withdrawal would render the pool stake account impossible to redelegate. \
173176
This can only occur if the Stake Program minimum delegation increases above 1 sol.",
177+
SinglePoolError::InvalidDepositSolSource =>
178+
"Error: The user-owned lamport source cannot be validated for `DepositSol`.",
174179
}
175180
}
176181
}

program/src/instruction.rs

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ pub enum SinglePoolInstruction {
5454
/// - Delegate the on-ramp if it has excess lamports to activate.
5555
///
5656
/// Combined, these operations allow harvesting and delegating MEV rewards
57-
/// and will eventually allow depositing liquid sol for pool tokens.
57+
/// and also enable depositing liquid sol for pool tokens via `DepositSol`.
5858
///
5959
/// This instruction is idempotent and gracefully skips operations that
6060
/// would fail or have no effect, up to no-op. This allows it to be
@@ -166,7 +166,27 @@ pub enum SinglePoolInstruction {
166166
/// 5. `[]` Stake program
167167
InitializePoolOnRamp,
168168

169-
/// (reserved for future use)
169+
/// Deposit liquid sol into the pool. The output is a "pool" token
170+
/// representing fractional ownership of the pool stake. Inputs are
171+
/// converted to the current ratio, less a fee of `DEPOSIT_SOL_FEE_BPS`.
172+
/// This instruction invokes `ReplenishPool` to immediately delegate
173+
/// any newly added sol if possible.
174+
///
175+
/// 0. `[]` Validator vote account
176+
/// 1. `[]` Pool account
177+
/// 2. `[w]` Pool stake account
178+
/// 3. `[w]` Pool on-ramp account
179+
/// 4. `[w]` Pool token mint
180+
/// 5. `[]` Pool stake authority
181+
/// 6. `[]` Pool mint authority
182+
/// 7. `[w, s]` User system account to deposit from
183+
/// 8. `[w]` User account to receive pool tokens
184+
/// 9. `[]` Clock sysvar
185+
/// 10. `[]` Stake history sysvar
186+
/// 11. `[]` Stake config sysvar
187+
/// 12. `[]` System program
188+
/// 13. `[]` Token program
189+
/// 14. `[]` Stake program
170190
DepositSol {
171191
/// Amount of sol to deposit
172192
lamports: u64,
@@ -343,6 +363,78 @@ pub fn deposit_stake(
343363
}
344364
}
345365

366+
/// Creates the necessary instructions to deposit liquid sol.
367+
/// `escrow_deposit_account` should be the pubkey of an unused keypair.
368+
/// This avoids passing a wallet signature into an opaque program,
369+
/// a best practice for safety given its untrammeled authority.
370+
/// The escrow account does not have to meet rent exemption because it is
371+
/// opened and closed in the span of one transaction.
372+
pub fn deposit_liquid(
373+
program_id: &Pubkey,
374+
vote_account_address: &Pubkey,
375+
user_wallet: &Pubkey,
376+
escrow_deposit_account: &Pubkey,
377+
user_token_account: &Pubkey,
378+
lamports: u64,
379+
) -> Vec<Instruction> {
380+
vec![
381+
system_instruction::transfer(user_wallet, escrow_deposit_account, lamports),
382+
deposit_sol(
383+
program_id,
384+
vote_account_address,
385+
escrow_deposit_account,
386+
user_token_account,
387+
lamports,
388+
),
389+
]
390+
}
391+
392+
/// Creates a `DepositSol` instruction.
393+
/// It is recommended as a matter of hygiene to use the `deposit_liquid()` helper,
394+
/// to isolate user wallet signing authority from the program.
395+
pub fn deposit_sol(
396+
program_id: &Pubkey,
397+
vote_account_address: &Pubkey,
398+
user_deposit_account: &Pubkey,
399+
user_token_account: &Pubkey,
400+
lamports: u64,
401+
) -> Instruction {
402+
let pool_address = find_pool_address(program_id, vote_account_address);
403+
404+
let data = borsh::to_vec(&SinglePoolInstruction::DepositSol { lamports }).unwrap();
405+
let accounts = vec![
406+
AccountMeta::new_readonly(*vote_account_address, false),
407+
AccountMeta::new_readonly(pool_address, false),
408+
AccountMeta::new(find_pool_stake_address(program_id, &pool_address), false),
409+
AccountMeta::new(find_pool_onramp_address(program_id, &pool_address), false),
410+
AccountMeta::new(find_pool_mint_address(program_id, &pool_address), false),
411+
AccountMeta::new_readonly(
412+
find_pool_stake_authority_address(program_id, &pool_address),
413+
false,
414+
),
415+
AccountMeta::new_readonly(
416+
find_pool_mint_authority_address(program_id, &pool_address),
417+
false,
418+
),
419+
AccountMeta::new(*user_deposit_account, true),
420+
AccountMeta::new(*user_token_account, false),
421+
AccountMeta::new_readonly(sysvar::clock::id(), false),
422+
AccountMeta::new_readonly(stake_history::id(), false),
423+
#[allow(deprecated)]
424+
AccountMeta::new_readonly(stake::config::id(), false),
425+
AccountMeta::new_readonly(system_program::id(), false),
426+
AccountMeta::new_readonly(spl_token::id(), false),
427+
AccountMeta::new_readonly(stake::program::id(), false),
428+
AccountMeta::new_readonly(*program_id, false),
429+
];
430+
431+
Instruction {
432+
program_id: *program_id,
433+
accounts,
434+
data,
435+
}
436+
}
437+
346438
/// Creates all necessary instructions to withdraw stake into a given stake
347439
/// account. If a new stake account is required, the user should first include
348440
/// `system_instruction::create_account` with account size

program/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ const PHANTOM_TOKEN_AMOUNT: u64 = LAMPORTS_PER_SOL;
2727
const MINT_DECIMALS: u8 = 9;
2828
const PERPETUAL_NEW_WARMUP_COOLDOWN_RATE_EPOCH: Option<u64> = Some(0);
2929

30+
const DEPOSIT_SOL_FEE_BPS: u64 = 100;
31+
const MAX_BPS: u64 = 10_000;
32+
3033
const VOTE_STATE_DISCRIMINATOR_END: usize = 4;
3134
const VOTE_STATE_AUTHORIZED_WITHDRAWER_START: usize = 36;
3235
const VOTE_STATE_AUTHORIZED_WITHDRAWER_END: usize = 68;

program/src/processor.rs

Lines changed: 164 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,19 @@ use {
99
pda::find_metadata_account,
1010
state::DataV2,
1111
},
12-
instruction::SinglePoolInstruction,
12+
instruction::{self as svsp_instruction, SinglePoolInstruction},
1313
state::{SinglePool, SinglePoolAccountType},
14-
MINT_DECIMALS, PERPETUAL_NEW_WARMUP_COOLDOWN_RATE_EPOCH, PHANTOM_TOKEN_AMOUNT,
15-
POOL_MINT_AUTHORITY_PREFIX, POOL_MINT_PREFIX, POOL_MPL_AUTHORITY_PREFIX,
16-
POOL_ONRAMP_PREFIX, POOL_PREFIX, POOL_STAKE_AUTHORITY_PREFIX, POOL_STAKE_PREFIX,
17-
VOTE_STATE_AUTHORIZED_WITHDRAWER_END, VOTE_STATE_AUTHORIZED_WITHDRAWER_START,
18-
VOTE_STATE_DISCRIMINATOR_END,
14+
DEPOSIT_SOL_FEE_BPS, MAX_BPS, MINT_DECIMALS, PERPETUAL_NEW_WARMUP_COOLDOWN_RATE_EPOCH,
15+
PHANTOM_TOKEN_AMOUNT, POOL_MINT_AUTHORITY_PREFIX, POOL_MINT_PREFIX,
16+
POOL_MPL_AUTHORITY_PREFIX, POOL_ONRAMP_PREFIX, POOL_PREFIX, POOL_STAKE_AUTHORITY_PREFIX,
17+
POOL_STAKE_PREFIX, VOTE_STATE_AUTHORIZED_WITHDRAWER_END,
18+
VOTE_STATE_AUTHORIZED_WITHDRAWER_START, VOTE_STATE_DISCRIMINATOR_END,
1919
},
2020
borsh::BorshDeserialize,
2121
solana_account_info::{next_account_info, AccountInfo},
2222
solana_borsh::v1::try_from_slice_unchecked,
2323
solana_clock::Clock,
24-
solana_cpi::invoke_signed,
24+
solana_cpi::{invoke, invoke_signed},
2525
solana_msg::msg,
2626
solana_native_token::LAMPORTS_PER_SOL,
2727
solana_program_entrypoint::ProgramResult,
@@ -1544,6 +1544,160 @@ impl Processor {
15441544
Ok(())
15451545
}
15461546

1547+
fn process_deposit_sol(
1548+
program_id: &Pubkey,
1549+
accounts: &[AccountInfo],
1550+
deposit_amount: u64,
1551+
) -> ProgramResult {
1552+
let account_info_iter = &mut accounts.iter();
1553+
let vote_account_info = next_account_info(account_info_iter)?;
1554+
let pool_info = next_account_info(account_info_iter)?;
1555+
let pool_stake_info = next_account_info(account_info_iter)?;
1556+
let pool_onramp_info = next_account_info(account_info_iter)?;
1557+
let pool_mint_info = next_account_info(account_info_iter)?;
1558+
let pool_stake_authority_info = next_account_info(account_info_iter)?;
1559+
let pool_mint_authority_info = next_account_info(account_info_iter)?;
1560+
let user_lamport_account_info = next_account_info(account_info_iter)?;
1561+
let user_token_account_info = next_account_info(account_info_iter)?;
1562+
let clock_info = next_account_info(account_info_iter)?;
1563+
let clock = &Clock::from_account_info(clock_info)?;
1564+
let stake_history_info = next_account_info(account_info_iter)?;
1565+
let stake_config_info = next_account_info(account_info_iter)?;
1566+
let system_program_info = next_account_info(account_info_iter)?;
1567+
let token_program_info = next_account_info(account_info_iter)?;
1568+
let stake_program_info = next_account_info(account_info_iter)?;
1569+
1570+
let rent = Rent::get()?;
1571+
let stake_history = &StakeHistorySysvar(clock.epoch);
1572+
1573+
check_vote_account(vote_account_info)?;
1574+
check_pool_address(program_id, vote_account_info.key, pool_info.key)?;
1575+
1576+
SinglePool::from_account_info(pool_info, program_id)?;
1577+
1578+
check_pool_stake_address(program_id, pool_info.key, pool_stake_info.key)?;
1579+
check_pool_onramp_address(program_id, pool_info.key, pool_onramp_info.key)?;
1580+
let token_supply = check_pool_mint_with_supply(program_id, pool_info.key, pool_mint_info)?;
1581+
check_pool_stake_authority_address(
1582+
program_id,
1583+
pool_info.key,
1584+
pool_stake_authority_info.key,
1585+
)?;
1586+
let mint_authority_bump_seed = check_pool_mint_authority_address(
1587+
program_id,
1588+
pool_info.key,
1589+
pool_mint_authority_info.key,
1590+
)?;
1591+
check_system_program(system_program_info.key)?;
1592+
check_token_program(token_program_info.key)?;
1593+
check_stake_program(stake_program_info.key)?;
1594+
1595+
if deposit_amount == 0 {
1596+
return Err(SinglePoolError::DepositTooSmall.into());
1597+
}
1598+
1599+
// we require the pool to be fully active for this instruction to minimize complexity
1600+
{
1601+
let (_, pool_stake_state) = get_stake_state(pool_stake_info)?;
1602+
let pool_stake_status = pool_stake_state
1603+
.delegation
1604+
.stake_activating_and_deactivating(
1605+
clock.epoch,
1606+
stake_history,
1607+
PERPETUAL_NEW_WARMUP_COOLDOWN_RATE_EPOCH,
1608+
);
1609+
1610+
if !is_stake_fully_active(&pool_stake_status) {
1611+
return Err(SinglePoolError::WrongStakeState.into());
1612+
}
1613+
};
1614+
1615+
// we require onramp to exist, though we dont care what state its in
1616+
match deserialize_stake(pool_onramp_info) {
1617+
Ok(StakeStateV2::Initialized(_)) | Ok(StakeStateV2::Stake(_, _, _)) => (),
1618+
_ => return Err(SinglePoolError::OnRampDoesntExist.into()),
1619+
}
1620+
1621+
// deposit source must be a system account for transfer to succeed
1622+
if !user_lamport_account_info.is_signer
1623+
|| user_lamport_account_info.owner != &system_program::id()
1624+
|| user_lamport_account_info.lamports() < deposit_amount
1625+
{
1626+
return Err(SinglePoolError::InvalidDepositSolSource.into());
1627+
}
1628+
1629+
let pre_total_nev = pool_net_asset_value(pool_stake_info, pool_onramp_info, &rent);
1630+
let pre_onramp_lamports = pool_onramp_info.lamports();
1631+
1632+
// transfer sol to pool onramp
1633+
invoke(
1634+
&system_instruction::transfer(
1635+
user_lamport_account_info.key,
1636+
pool_onramp_info.key,
1637+
deposit_amount,
1638+
),
1639+
&[user_lamport_account_info.clone(), pool_onramp_info.clone()],
1640+
)?;
1641+
1642+
// sanity, should be impossible
1643+
if pool_onramp_info.lamports() != pre_onramp_lamports.saturating_add(deposit_amount) {
1644+
return Err(SinglePoolError::UnexpectedMathError.into());
1645+
}
1646+
1647+
let new_pool_tokens = {
1648+
let raw_tokens = calculate_deposit_amount(token_supply, pre_total_nev, deposit_amount)
1649+
.ok_or(SinglePoolError::UnexpectedMathError)?;
1650+
1651+
// we round division down and reject deposits too small to generate a fee
1652+
// this is to avoid pathological cases where eg someone deposits 2 lamps and pays 50%
1653+
let deposit_sol_fee = raw_tokens
1654+
.checked_mul(DEPOSIT_SOL_FEE_BPS)
1655+
.and_then(|n| n.checked_div(MAX_BPS))
1656+
.ok_or(SinglePoolError::UnexpectedMathError)?;
1657+
1658+
// because we abort on no fee, we know new_pool_tokens gt 0
1659+
if deposit_sol_fee == 0 {
1660+
return Err(SinglePoolError::DepositTooSmall.into());
1661+
}
1662+
1663+
raw_tokens.saturating_sub(deposit_sol_fee)
1664+
};
1665+
1666+
// sanity, should be impossible per above
1667+
if new_pool_tokens == 0 {
1668+
return Err(SinglePoolError::UnexpectedMathError.into());
1669+
}
1670+
1671+
// mint tokens to the user corresponding to their sol deposit
1672+
Self::token_mint_to(
1673+
pool_info.key,
1674+
token_program_info.clone(),
1675+
pool_mint_info.clone(),
1676+
user_token_account_info.clone(),
1677+
pool_mint_authority_info.clone(),
1678+
mint_authority_bump_seed,
1679+
new_pool_tokens,
1680+
)?;
1681+
1682+
// replenish to delegate the deposit. this safely returns Ok if onramp doesnt meet minimum delegation
1683+
invoke(
1684+
&svsp_instruction::replenish_pool(program_id, vote_account_info.key),
1685+
&[
1686+
vote_account_info.clone(),
1687+
pool_info.clone(),
1688+
pool_stake_info.clone(),
1689+
pool_onramp_info.clone(),
1690+
pool_stake_authority_info.clone(),
1691+
clock_info.clone(),
1692+
stake_history_info.clone(),
1693+
stake_config_info.clone(),
1694+
stake_program_info.clone(),
1695+
],
1696+
)?;
1697+
1698+
Ok(())
1699+
}
1700+
15471701
/// Processes [Instruction](enum.Instruction.html).
15481702
pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult {
15491703
let instruction = SinglePoolInstruction::try_from_slice(input)?;
@@ -1584,9 +1738,9 @@ impl Processor {
15841738
msg!("Instruction: InitializePoolOnRamp");
15851739
Self::process_initialize_pool_onramp(program_id, accounts)
15861740
}
1587-
SinglePoolInstruction::DepositSol { lamports: _ } => {
1588-
msg!("Instruction: DepositSol (NOT IMPLEMENTED)");
1589-
Err(ProgramError::InvalidInstructionData)
1741+
SinglePoolInstruction::DepositSol { lamports } => {
1742+
msg!("Instruction: DepositSol");
1743+
Self::process_deposit_sol(program_id, accounts, lamports)
15901744
}
15911745
}
15921746
}

0 commit comments

Comments
 (0)