Skip to main content

spl_single_pool/
instruction.rs

1//! Instruction types
2
3#![allow(clippy::too_many_arguments)]
4
5use {
6    crate::{
7        find_pool_address, find_pool_mint_address, find_pool_mint_authority_address,
8        find_pool_mpl_authority_address, find_pool_onramp_address, find_pool_stake_address,
9        find_pool_stake_authority_address,
10        inline_mpl_token_metadata::{self, pda::find_metadata_account},
11        state::SinglePool,
12    },
13    borsh::{BorshDeserialize, BorshSerialize},
14    solana_instruction::{AccountMeta, Instruction},
15    solana_program_pack::Pack,
16    solana_pubkey::Pubkey,
17    solana_rent::Rent,
18    solana_stake_interface::{self as stake, sysvar::stake_history},
19    solana_system_interface::{instruction as system_instruction, program as system_program},
20    solana_sysvar as sysvar, spl_token_interface as spl_token,
21};
22
23/// Instructions supported by the `SinglePool` program.
24#[repr(C)]
25#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize)]
26pub enum SinglePoolInstruction {
27    ///   Initialize the mint and main stake account for a new single-validator
28    ///   stake pool. The pool stake account must contain the rent-exempt
29    ///   minimum plus the minimum balance of 1 sol. No tokens will be minted;
30    ///   to deposit more, use `Deposit` after `InitializeStake`.
31    ///
32    ///   0. `[]` Validator vote account
33    ///   1. `[w]` Pool account
34    ///   2. `[w]` Pool stake account
35    ///   3. `[w]` Pool token mint
36    ///   4. `[]` Pool stake authority
37    ///   5. `[]` Pool mint authority
38    ///   6. `[]` Rent sysvar
39    ///   7. `[]` Clock sysvar
40    ///   8. `[]` Stake history sysvar
41    ///   9. `[]` Stake config sysvar
42    ///  10. `[]` System program
43    ///  11. `[]` Token program
44    ///  12. `[]` Stake program
45    InitializePool,
46
47    ///   Bring the pool stake accounts to their optimal state. This performs
48    ///   several operations:
49    ///   * If the main stake account has been deactivated by
50    ///     `DeactivateDelinquent`, reactivate it.
51    ///   * Then, if the main stake account is already fully active:
52    ///     - If the on-ramp is fully active, move its stake to the main account.
53    ///     - If the main account has excess lamports, move them to the on-ramp.
54    ///     - Delegate the on-ramp if it has excess lamports to activate.
55    ///
56    ///   Combined, these operations allow harvesting and delegating MEV rewards
57    ///   and will eventually allow depositing liquid sol for pool tokens.
58    ///
59    ///   This instruction is idempotent and gracefully skips operations that
60    ///   would fail or have no effect, up to no-op. This allows it to be
61    ///   executed speculatively or as part of arbitrary flows involving the pool.
62    ///   If the on-ramp account is already activating, and there are excess lamports
63    ///   beyond the activating delegation, it increases the delegation to include them.
64    ///
65    ///   This instruction will fail with an error if the on-ramp account does not
66    ///   exist. If the pool does not have the account, `InitializePoolOnRamp` must
67    ///   be called to create it.
68    ///
69    ///   0. `[]` Validator vote account
70    ///   1. `[]` Pool account
71    ///   2. `[w]` Pool stake account
72    ///   3. `[w]` Pool on-ramp account
73    ///   4. `[]` Pool stake authority
74    ///   5. `[]` Clock sysvar
75    ///   6. `[]` Stake history sysvar
76    ///   7. `[]` Stake config sysvar
77    ///   8. `[]` Stake program
78    ReplenishPool,
79
80    ///   Deposit stake into the pool. The output is a "pool" token
81    ///   representing fractional ownership of the pool stake. Inputs are
82    ///   converted to the current ratio.
83    ///
84    ///   0. `[]` Pool account
85    ///   1. `[w]` Pool stake account
86    ///   2. `[]` Pool on-ramp account
87    ///   3. `[w]` Pool token mint
88    ///   4. `[]` Pool stake authority
89    ///   5. `[]` Pool mint authority
90    ///   6. `[w]` User stake account to join to the pool
91    ///   7. `[w]` User account to receive pool tokens
92    ///   8. `[w]` User account to receive lamports
93    ///   9. `[]` Clock sysvar
94    ///  10. `[]` Stake history sysvar
95    ///  11. `[]` Token program
96    ///  12. `[]` Stake program
97    DepositStake,
98
99    ///   Redeem tokens issued by this pool for stake at the current ratio.
100    ///
101    ///   0. `[]` Pool account
102    ///   1. `[w]` Pool stake account
103    ///   2. `[]` Pool on-ramp account
104    ///   3. `[w]` Pool token mint
105    ///   4. `[]` Pool stake authority
106    ///   5. `[]` Pool mint authority
107    ///   6. `[w]` User stake account to receive stake at
108    ///   7. `[w]` User account to take pool tokens from
109    ///   8. `[]` Clock sysvar
110    ///   9. `[]` Token program
111    ///  10. `[]` Stake program
112    WithdrawStake {
113        /// User authority for the new stake account
114        user_stake_authority: Pubkey,
115        /// Amount of tokens to redeem for stake
116        token_amount: u64,
117    },
118
119    ///   Create token metadata for the stake-pool token in the metaplex-token
120    ///   program. Step three of the permissionless three-stage initialization
121    ///   flow.
122    ///   Note this instruction is not necessary for the pool to operate, to
123    ///   ensure we cannot be broken by upstream.
124    ///
125    ///   0. `[]` Pool account
126    ///   1. `[]` Pool token mint
127    ///   2. `[]` Pool mint authority
128    ///   3. `[]` Pool MPL authority
129    ///   4. `[s, w]` Payer for creation of token metadata account
130    ///   5. `[w]` Token metadata account
131    ///   6. `[]` Metadata program id
132    ///   7. `[]` System program id
133    CreateTokenMetadata,
134
135    ///   Update token metadata for the stake-pool token in the metaplex-token
136    ///   program.
137    ///
138    ///   0. `[]` Validator vote account
139    ///   1. `[]` Pool account
140    ///   2. `[]` Pool MPL authority
141    ///   3. `[s]` Vote account authorized withdrawer
142    ///   4. `[w]` Token metadata account
143    ///   5. `[]` Metadata program id
144    UpdateTokenMetadata {
145        /// Token name
146        name: String,
147        /// Token symbol e.g. `stkSOL`
148        symbol: String,
149        /// URI of the uploaded metadata of the spl-token
150        uri: String,
151    },
152
153    ///   Create the on-ramp account for a single-validator stake pool, which
154    ///   is used to delegate liquid sol so that it can be merged into the main
155    ///   pool account as active stake.
156    ///
157    ///   New pools created with `initialize()` will include this instruction
158    ///   automatically. Existing pools must use `InitializePoolOnRamp` to upgrade to
159    ///   the latest version.
160    ///
161    ///   0. `[]` Pool account
162    ///   1. `[w]` Pool on-ramp account
163    ///   2. `[]` Pool stake authority
164    ///   3. `[]` Rent sysvar
165    ///   4. `[]` System program
166    ///   5. `[]` Stake program
167    InitializePoolOnRamp,
168
169    /// (reserved for future use)
170    DepositSol {
171        /// Amount of sol to deposit
172        lamports: u64,
173    },
174}
175
176/// Creates all necessary instructions to initialize the stake pool.
177pub fn initialize(
178    program_id: &Pubkey,
179    vote_account_address: &Pubkey,
180    payer: &Pubkey,
181    rent: &Rent,
182    minimum_pool_balance: u64,
183) -> Vec<Instruction> {
184    let pool_address = find_pool_address(program_id, vote_account_address);
185    let pool_rent = rent.minimum_balance(SinglePool::size_of());
186
187    let stake_address = find_pool_stake_address(program_id, &pool_address);
188    let onramp_address = find_pool_onramp_address(program_id, &pool_address);
189    let stake_space = stake::state::StakeStateV2::size_of();
190    let stake_rent = rent.minimum_balance(stake_space);
191    let stake_rent_plus_minimum = stake_rent.saturating_add(minimum_pool_balance);
192
193    let mint_address = find_pool_mint_address(program_id, &pool_address);
194    let mint_rent = rent.minimum_balance(spl_token::state::Mint::LEN);
195
196    vec![
197        system_instruction::transfer(payer, &pool_address, pool_rent),
198        system_instruction::transfer(payer, &stake_address, stake_rent_plus_minimum),
199        system_instruction::transfer(payer, &onramp_address, stake_rent),
200        system_instruction::transfer(payer, &mint_address, mint_rent),
201        initialize_pool(program_id, vote_account_address),
202        initialize_pool_onramp(program_id, &pool_address),
203        create_token_metadata(program_id, &pool_address, payer),
204    ]
205}
206
207/// Creates an `InitializePool` instruction.
208pub fn initialize_pool(program_id: &Pubkey, vote_account_address: &Pubkey) -> Instruction {
209    let pool_address = find_pool_address(program_id, vote_account_address);
210    let mint_address = find_pool_mint_address(program_id, &pool_address);
211
212    let data = borsh::to_vec(&SinglePoolInstruction::InitializePool).unwrap();
213    let accounts = vec![
214        AccountMeta::new_readonly(*vote_account_address, false),
215        AccountMeta::new(pool_address, false),
216        AccountMeta::new(find_pool_stake_address(program_id, &pool_address), false),
217        AccountMeta::new(mint_address, false),
218        AccountMeta::new_readonly(
219            find_pool_stake_authority_address(program_id, &pool_address),
220            false,
221        ),
222        AccountMeta::new_readonly(
223            find_pool_mint_authority_address(program_id, &pool_address),
224            false,
225        ),
226        AccountMeta::new_readonly(sysvar::rent::id(), false),
227        AccountMeta::new_readonly(sysvar::clock::id(), false),
228        AccountMeta::new_readonly(stake_history::id(), false),
229        #[allow(deprecated)]
230        AccountMeta::new_readonly(stake::config::id(), false),
231        AccountMeta::new_readonly(system_program::id(), false),
232        AccountMeta::new_readonly(spl_token::id(), false),
233        AccountMeta::new_readonly(stake::program::id(), false),
234    ];
235
236    Instruction {
237        program_id: *program_id,
238        accounts,
239        data,
240    }
241}
242
243/// Creates a `ReplenishPool` instruction.
244pub fn replenish_pool(program_id: &Pubkey, vote_account_address: &Pubkey) -> Instruction {
245    let pool_address = find_pool_address(program_id, vote_account_address);
246
247    let data = borsh::to_vec(&SinglePoolInstruction::ReplenishPool).unwrap();
248    let accounts = vec![
249        AccountMeta::new_readonly(*vote_account_address, false),
250        AccountMeta::new_readonly(pool_address, false),
251        AccountMeta::new(find_pool_stake_address(program_id, &pool_address), false),
252        AccountMeta::new(find_pool_onramp_address(program_id, &pool_address), false),
253        AccountMeta::new_readonly(
254            find_pool_stake_authority_address(program_id, &pool_address),
255            false,
256        ),
257        AccountMeta::new_readonly(sysvar::clock::id(), false),
258        AccountMeta::new_readonly(stake_history::id(), false),
259        #[allow(deprecated)]
260        AccountMeta::new_readonly(stake::config::id(), false),
261        AccountMeta::new_readonly(stake::program::id(), false),
262    ];
263
264    Instruction {
265        program_id: *program_id,
266        accounts,
267        data,
268    }
269}
270
271/// Creates all necessary instructions to deposit stake.
272pub fn deposit(
273    program_id: &Pubkey,
274    pool_address: &Pubkey,
275    user_stake_account: &Pubkey,
276    user_token_account: &Pubkey,
277    user_lamport_account: &Pubkey,
278    user_withdraw_authority: &Pubkey,
279) -> Vec<Instruction> {
280    let pool_stake_authority = find_pool_stake_authority_address(program_id, pool_address);
281
282    vec![
283        stake::instruction::authorize(
284            user_stake_account,
285            user_withdraw_authority,
286            &pool_stake_authority,
287            stake::state::StakeAuthorize::Staker,
288            None,
289        ),
290        stake::instruction::authorize(
291            user_stake_account,
292            user_withdraw_authority,
293            &pool_stake_authority,
294            stake::state::StakeAuthorize::Withdrawer,
295            None,
296        ),
297        deposit_stake(
298            program_id,
299            pool_address,
300            user_stake_account,
301            user_token_account,
302            user_lamport_account,
303        ),
304    ]
305}
306
307/// Creates a `DepositStake` instruction.
308pub fn deposit_stake(
309    program_id: &Pubkey,
310    pool_address: &Pubkey,
311    user_stake_account: &Pubkey,
312    user_token_account: &Pubkey,
313    user_lamport_account: &Pubkey,
314) -> Instruction {
315    let data = borsh::to_vec(&SinglePoolInstruction::DepositStake).unwrap();
316
317    let accounts = vec![
318        AccountMeta::new_readonly(*pool_address, false),
319        AccountMeta::new(find_pool_stake_address(program_id, pool_address), false),
320        AccountMeta::new_readonly(find_pool_onramp_address(program_id, pool_address), false),
321        AccountMeta::new(find_pool_mint_address(program_id, pool_address), false),
322        AccountMeta::new_readonly(
323            find_pool_stake_authority_address(program_id, pool_address),
324            false,
325        ),
326        AccountMeta::new_readonly(
327            find_pool_mint_authority_address(program_id, pool_address),
328            false,
329        ),
330        AccountMeta::new(*user_stake_account, false),
331        AccountMeta::new(*user_token_account, false),
332        AccountMeta::new(*user_lamport_account, false),
333        AccountMeta::new_readonly(sysvar::clock::id(), false),
334        AccountMeta::new_readonly(stake_history::id(), false),
335        AccountMeta::new_readonly(spl_token::id(), false),
336        AccountMeta::new_readonly(stake::program::id(), false),
337    ];
338
339    Instruction {
340        program_id: *program_id,
341        accounts,
342        data,
343    }
344}
345
346/// Creates all necessary instructions to withdraw stake into a given stake
347/// account. If a new stake account is required, the user should first include
348/// `system_instruction::create_account` with account size
349/// `stake::state::StakeStateV2::size_of()` and owner `stake::program::id()`.
350pub fn withdraw(
351    program_id: &Pubkey,
352    pool_address: &Pubkey,
353    user_stake_account: &Pubkey,
354    user_stake_authority: &Pubkey,
355    user_token_account: &Pubkey,
356    user_token_authority: &Pubkey,
357    token_amount: u64,
358) -> Vec<Instruction> {
359    vec![
360        spl_token::instruction::approve(
361            &spl_token::id(),
362            user_token_account,
363            &find_pool_mint_authority_address(program_id, pool_address),
364            user_token_authority,
365            &[],
366            token_amount,
367        )
368        .unwrap(),
369        withdraw_stake(
370            program_id,
371            pool_address,
372            user_stake_account,
373            user_stake_authority,
374            user_token_account,
375            token_amount,
376        ),
377    ]
378}
379
380/// Creates a `WithdrawStake` instruction.
381pub fn withdraw_stake(
382    program_id: &Pubkey,
383    pool_address: &Pubkey,
384    user_stake_account: &Pubkey,
385    user_stake_authority: &Pubkey,
386    user_token_account: &Pubkey,
387    token_amount: u64,
388) -> Instruction {
389    let data = borsh::to_vec(&SinglePoolInstruction::WithdrawStake {
390        user_stake_authority: *user_stake_authority,
391        token_amount,
392    })
393    .unwrap();
394
395    let accounts = vec![
396        AccountMeta::new_readonly(*pool_address, false),
397        AccountMeta::new(find_pool_stake_address(program_id, pool_address), false),
398        AccountMeta::new_readonly(find_pool_onramp_address(program_id, pool_address), false),
399        AccountMeta::new(find_pool_mint_address(program_id, pool_address), false),
400        AccountMeta::new_readonly(
401            find_pool_stake_authority_address(program_id, pool_address),
402            false,
403        ),
404        AccountMeta::new_readonly(
405            find_pool_mint_authority_address(program_id, pool_address),
406            false,
407        ),
408        AccountMeta::new(*user_stake_account, false),
409        AccountMeta::new(*user_token_account, false),
410        AccountMeta::new_readonly(sysvar::clock::id(), false),
411        AccountMeta::new_readonly(spl_token::id(), false),
412        AccountMeta::new_readonly(stake::program::id(), false),
413    ];
414
415    Instruction {
416        program_id: *program_id,
417        accounts,
418        data,
419    }
420}
421
422/// Creates a `CreateTokenMetadata` instruction.
423pub fn create_token_metadata(
424    program_id: &Pubkey,
425    pool_address: &Pubkey,
426    payer: &Pubkey,
427) -> Instruction {
428    let pool_mint = find_pool_mint_address(program_id, pool_address);
429    let (token_metadata, _) = find_metadata_account(&pool_mint);
430    let data = borsh::to_vec(&SinglePoolInstruction::CreateTokenMetadata).unwrap();
431
432    let accounts = vec![
433        AccountMeta::new_readonly(*pool_address, false),
434        AccountMeta::new_readonly(pool_mint, false),
435        AccountMeta::new_readonly(
436            find_pool_mint_authority_address(program_id, pool_address),
437            false,
438        ),
439        AccountMeta::new_readonly(
440            find_pool_mpl_authority_address(program_id, pool_address),
441            false,
442        ),
443        AccountMeta::new(*payer, true),
444        AccountMeta::new(token_metadata, false),
445        AccountMeta::new_readonly(inline_mpl_token_metadata::id(), false),
446        AccountMeta::new_readonly(system_program::id(), false),
447    ];
448
449    Instruction {
450        program_id: *program_id,
451        accounts,
452        data,
453    }
454}
455
456/// Creates an `UpdateTokenMetadata` instruction.
457pub fn update_token_metadata(
458    program_id: &Pubkey,
459    vote_account_address: &Pubkey,
460    authorized_withdrawer: &Pubkey,
461    name: String,
462    symbol: String,
463    uri: String,
464) -> Instruction {
465    let pool_address = find_pool_address(program_id, vote_account_address);
466    let pool_mint = find_pool_mint_address(program_id, &pool_address);
467    let (token_metadata, _) = find_metadata_account(&pool_mint);
468    let data =
469        borsh::to_vec(&SinglePoolInstruction::UpdateTokenMetadata { name, symbol, uri }).unwrap();
470
471    let accounts = vec![
472        AccountMeta::new_readonly(*vote_account_address, false),
473        AccountMeta::new_readonly(pool_address, false),
474        AccountMeta::new_readonly(
475            find_pool_mpl_authority_address(program_id, &pool_address),
476            false,
477        ),
478        AccountMeta::new_readonly(*authorized_withdrawer, true),
479        AccountMeta::new(token_metadata, false),
480        AccountMeta::new_readonly(inline_mpl_token_metadata::id(), false),
481    ];
482
483    Instruction {
484        program_id: *program_id,
485        accounts,
486        data,
487    }
488}
489
490/// Creates a `InitializePoolOnRamp` instruction.
491pub fn initialize_pool_onramp(program_id: &Pubkey, pool_address: &Pubkey) -> Instruction {
492    let data = borsh::to_vec(&SinglePoolInstruction::InitializePoolOnRamp).unwrap();
493    let accounts = vec![
494        AccountMeta::new_readonly(*pool_address, false),
495        AccountMeta::new(find_pool_onramp_address(program_id, pool_address), false),
496        AccountMeta::new_readonly(
497            find_pool_stake_authority_address(program_id, pool_address),
498            false,
499        ),
500        AccountMeta::new_readonly(sysvar::rent::id(), false),
501        AccountMeta::new_readonly(system_program::id(), false),
502        AccountMeta::new_readonly(stake::program::id(), false),
503    ];
504
505    Instruction {
506        program_id: *program_id,
507        accounts,
508        data,
509    }
510}
511
512/// Creates a `InitializePoolOnRamp` instruction plus the transfer to fund it.
513/// This is for convenience, for users who need to create an on-ramp for existing pools.
514/// We don't use it internally, because `initialize()` carries the necessary logic.
515pub fn create_pool_onramp(
516    program_id: &Pubkey,
517    pool_address: &Pubkey,
518    payer: &Pubkey,
519    rent: &Rent,
520) -> Vec<Instruction> {
521    let onramp_address = find_pool_onramp_address(program_id, pool_address);
522    let stake_space = stake::state::StakeStateV2::size_of();
523    let stake_rent = rent.minimum_balance(stake_space);
524
525    vec![
526        system_instruction::transfer(payer, &onramp_address, stake_rent),
527        initialize_pool_onramp(program_id, pool_address),
528    ]
529}