spl_single_pool/
instruction.rs

1//! Instruction types
2
3#![allow(clippy::too_many_arguments)]
4
5use {
6    crate::{
7        find_default_deposit_account_address_and_seed, find_pool_address, find_pool_mint_address,
8        find_pool_mint_authority_address, find_pool_mpl_authority_address,
9        find_pool_onramp_address, find_pool_stake_address, 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_program::{
15        instruction::{AccountMeta, Instruction},
16        program_pack::Pack,
17        pubkey::Pubkey,
18        rent::Rent,
19        stake, system_instruction, system_program, sysvar,
20    },
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. `[w]` Pool token mint
87    ///   3. `[]` Pool stake authority
88    ///   4. `[]` Pool mint authority
89    ///   5. `[w]` User stake account to join to the pool
90    ///   6. `[w]` User account to receive pool tokens
91    ///   7. `[w]` User account to receive lamports
92    ///   8. `[]` Clock sysvar
93    ///   9. `[]` Stake history sysvar
94    ///  10. `[]` Token program
95    ///  11. `[]` Stake program
96    DepositStake,
97
98    ///   Redeem tokens issued by this pool for stake at the current ratio.
99    ///
100    ///   0. `[]` Pool account
101    ///   1. `[w]` Pool stake account
102    ///   2. `[w]` Pool token mint
103    ///   3. `[]` Pool stake authority
104    ///   4. `[]` Pool mint authority
105    ///   5. `[w]` User stake account to receive stake at
106    ///   6. `[w]` User account to take pool tokens from
107    ///   7. `[]` Clock sysvar
108    ///   8. `[]` Token program
109    ///   9. `[]` Stake program
110    WithdrawStake {
111        /// User authority for the new stake account
112        user_stake_authority: Pubkey,
113        /// Amount of tokens to redeem for stake
114        token_amount: u64,
115    },
116
117    ///   Create token metadata for the stake-pool token in the metaplex-token
118    ///   program. Step three of the permissionless three-stage initialization
119    ///   flow.
120    ///   Note this instruction is not necessary for the pool to operate, to
121    ///   ensure we cannot be broken by upstream.
122    ///
123    ///   0. `[]` Pool account
124    ///   1. `[]` Pool token mint
125    ///   2. `[]` Pool mint authority
126    ///   3. `[]` Pool MPL authority
127    ///   4. `[s, w]` Payer for creation of token metadata account
128    ///   5. `[w]` Token metadata account
129    ///   6. `[]` Metadata program id
130    ///   7. `[]` System program id
131    CreateTokenMetadata,
132
133    ///   Update token metadata for the stake-pool token in the metaplex-token
134    ///   program.
135    ///
136    ///   0. `[]` Validator vote account
137    ///   1. `[]` Pool account
138    ///   2. `[]` Pool MPL authority
139    ///   3. `[s]` Vote account authorized withdrawer
140    ///   4. `[w]` Token metadata account
141    ///   5. `[]` Metadata program id
142    UpdateTokenMetadata {
143        /// Token name
144        name: String,
145        /// Token symbol e.g. `stkSOL`
146        symbol: String,
147        /// URI of the uploaded metadata of the spl-token
148        uri: String,
149    },
150
151    ///   Create the on-ramp account for a single-validator stake pool, which
152    ///   is used to delegate liquid sol so that it can be merged into the main
153    ///   pool account as active stake.
154    ///
155    ///   New pools created with `initialize()` will include this instruction
156    ///   automatically. Existing pools must use `InitializePoolOnRamp` to upgrade to
157    ///   the latest version.
158    ///
159    ///   This is a temporary instruction that will be deprecated some time after all
160    ///   existing pools have upgraded. Its logic is intended to be incorporated
161    ///   into `InitializePool` itself.
162    ///
163    ///   0. `[]` Pool account
164    ///   1. `[w]` Pool on-ramp account
165    ///   2. `[]` Pool stake authority
166    ///   3. `[]` Rent sysvar
167    ///   4. `[]` System program
168    ///   5. `[]` Stake program
169    InitializePoolOnRamp,
170}
171
172/// Creates all necessary instructions to initialize the stake pool.
173pub fn initialize(
174    program_id: &Pubkey,
175    vote_account_address: &Pubkey,
176    payer: &Pubkey,
177    rent: &Rent,
178    minimum_pool_balance: u64,
179) -> Vec<Instruction> {
180    let pool_address = find_pool_address(program_id, vote_account_address);
181    let pool_rent = rent.minimum_balance(std::mem::size_of::<SinglePool>());
182
183    let stake_address = find_pool_stake_address(program_id, &pool_address);
184    let onramp_address = find_pool_onramp_address(program_id, &pool_address);
185    let stake_space = std::mem::size_of::<stake::state::StakeStateV2>();
186    let stake_rent = rent.minimum_balance(stake_space);
187    let stake_rent_plus_minimum = stake_rent.saturating_add(minimum_pool_balance);
188
189    let mint_address = find_pool_mint_address(program_id, &pool_address);
190    let mint_rent = rent.minimum_balance(spl_token::state::Mint::LEN);
191
192    vec![
193        system_instruction::transfer(payer, &pool_address, pool_rent),
194        system_instruction::transfer(payer, &stake_address, stake_rent_plus_minimum),
195        system_instruction::transfer(payer, &onramp_address, stake_rent),
196        system_instruction::transfer(payer, &mint_address, mint_rent),
197        initialize_pool(program_id, vote_account_address),
198        initialize_pool_onramp(program_id, &pool_address),
199        create_token_metadata(program_id, &pool_address, payer),
200    ]
201}
202
203/// Creates an `InitializePool` instruction.
204pub fn initialize_pool(program_id: &Pubkey, vote_account_address: &Pubkey) -> Instruction {
205    let pool_address = find_pool_address(program_id, vote_account_address);
206    let mint_address = find_pool_mint_address(program_id, &pool_address);
207
208    let data = borsh::to_vec(&SinglePoolInstruction::InitializePool).unwrap();
209    let accounts = vec![
210        AccountMeta::new_readonly(*vote_account_address, false),
211        AccountMeta::new(pool_address, false),
212        AccountMeta::new(find_pool_stake_address(program_id, &pool_address), false),
213        AccountMeta::new(mint_address, false),
214        AccountMeta::new_readonly(
215            find_pool_stake_authority_address(program_id, &pool_address),
216            false,
217        ),
218        AccountMeta::new_readonly(
219            find_pool_mint_authority_address(program_id, &pool_address),
220            false,
221        ),
222        AccountMeta::new_readonly(sysvar::rent::id(), false),
223        AccountMeta::new_readonly(sysvar::clock::id(), false),
224        AccountMeta::new_readonly(sysvar::stake_history::id(), false),
225        #[allow(deprecated)]
226        AccountMeta::new_readonly(stake::config::id(), false),
227        AccountMeta::new_readonly(system_program::id(), false),
228        AccountMeta::new_readonly(spl_token::id(), false),
229        AccountMeta::new_readonly(stake::program::id(), false),
230    ];
231
232    Instruction {
233        program_id: *program_id,
234        accounts,
235        data,
236    }
237}
238
239/// Creates a `ReplenishPool` instruction.
240pub fn replenish_pool(program_id: &Pubkey, vote_account_address: &Pubkey) -> Instruction {
241    let pool_address = find_pool_address(program_id, vote_account_address);
242
243    let data = borsh::to_vec(&SinglePoolInstruction::ReplenishPool).unwrap();
244    let accounts = vec![
245        AccountMeta::new_readonly(*vote_account_address, false),
246        AccountMeta::new_readonly(pool_address, false),
247        AccountMeta::new(find_pool_stake_address(program_id, &pool_address), false),
248        AccountMeta::new(find_pool_onramp_address(program_id, &pool_address), false),
249        AccountMeta::new_readonly(
250            find_pool_stake_authority_address(program_id, &pool_address),
251            false,
252        ),
253        AccountMeta::new_readonly(sysvar::clock::id(), false),
254        AccountMeta::new_readonly(sysvar::stake_history::id(), false),
255        #[allow(deprecated)]
256        AccountMeta::new_readonly(stake::config::id(), false),
257        AccountMeta::new_readonly(stake::program::id(), false),
258    ];
259
260    Instruction {
261        program_id: *program_id,
262        accounts,
263        data,
264    }
265}
266
267/// Creates all necessary instructions to deposit stake.
268pub fn deposit(
269    program_id: &Pubkey,
270    pool_address: &Pubkey,
271    user_stake_account: &Pubkey,
272    user_token_account: &Pubkey,
273    user_lamport_account: &Pubkey,
274    user_withdraw_authority: &Pubkey,
275) -> Vec<Instruction> {
276    let pool_stake_authority = find_pool_stake_authority_address(program_id, pool_address);
277
278    vec![
279        stake::instruction::authorize(
280            user_stake_account,
281            user_withdraw_authority,
282            &pool_stake_authority,
283            stake::state::StakeAuthorize::Staker,
284            None,
285        ),
286        stake::instruction::authorize(
287            user_stake_account,
288            user_withdraw_authority,
289            &pool_stake_authority,
290            stake::state::StakeAuthorize::Withdrawer,
291            None,
292        ),
293        deposit_stake(
294            program_id,
295            pool_address,
296            user_stake_account,
297            user_token_account,
298            user_lamport_account,
299        ),
300    ]
301}
302
303/// Creates a `DepositStake` instruction.
304pub fn deposit_stake(
305    program_id: &Pubkey,
306    pool_address: &Pubkey,
307    user_stake_account: &Pubkey,
308    user_token_account: &Pubkey,
309    user_lamport_account: &Pubkey,
310) -> Instruction {
311    let data = borsh::to_vec(&SinglePoolInstruction::DepositStake).unwrap();
312
313    let accounts = vec![
314        AccountMeta::new_readonly(*pool_address, false),
315        AccountMeta::new(find_pool_stake_address(program_id, pool_address), false),
316        AccountMeta::new(find_pool_mint_address(program_id, pool_address), false),
317        AccountMeta::new_readonly(
318            find_pool_stake_authority_address(program_id, pool_address),
319            false,
320        ),
321        AccountMeta::new_readonly(
322            find_pool_mint_authority_address(program_id, pool_address),
323            false,
324        ),
325        AccountMeta::new(*user_stake_account, false),
326        AccountMeta::new(*user_token_account, false),
327        AccountMeta::new(*user_lamport_account, false),
328        AccountMeta::new_readonly(sysvar::clock::id(), false),
329        AccountMeta::new_readonly(sysvar::stake_history::id(), false),
330        AccountMeta::new_readonly(spl_token::id(), false),
331        AccountMeta::new_readonly(stake::program::id(), false),
332    ];
333
334    Instruction {
335        program_id: *program_id,
336        accounts,
337        data,
338    }
339}
340
341/// Creates all necessary instructions to withdraw stake into a given stake
342/// account. If a new stake account is required, the user should first include
343/// `system_instruction::create_account` with account size
344/// `std::mem::size_of::<stake::state::StakeStateV2>()` and owner
345/// `stake::program::id()`.
346pub fn withdraw(
347    program_id: &Pubkey,
348    pool_address: &Pubkey,
349    user_stake_account: &Pubkey,
350    user_stake_authority: &Pubkey,
351    user_token_account: &Pubkey,
352    user_token_authority: &Pubkey,
353    token_amount: u64,
354) -> Vec<Instruction> {
355    vec![
356        spl_token::instruction::approve(
357            &spl_token::id(),
358            user_token_account,
359            &find_pool_mint_authority_address(program_id, pool_address),
360            user_token_authority,
361            &[],
362            token_amount,
363        )
364        .unwrap(),
365        withdraw_stake(
366            program_id,
367            pool_address,
368            user_stake_account,
369            user_stake_authority,
370            user_token_account,
371            token_amount,
372        ),
373    ]
374}
375
376/// Creates a `WithdrawStake` instruction.
377pub fn withdraw_stake(
378    program_id: &Pubkey,
379    pool_address: &Pubkey,
380    user_stake_account: &Pubkey,
381    user_stake_authority: &Pubkey,
382    user_token_account: &Pubkey,
383    token_amount: u64,
384) -> Instruction {
385    let data = borsh::to_vec(&SinglePoolInstruction::WithdrawStake {
386        user_stake_authority: *user_stake_authority,
387        token_amount,
388    })
389    .unwrap();
390
391    let accounts = vec![
392        AccountMeta::new_readonly(*pool_address, false),
393        AccountMeta::new(find_pool_stake_address(program_id, pool_address), false),
394        AccountMeta::new(find_pool_mint_address(program_id, pool_address), false),
395        AccountMeta::new_readonly(
396            find_pool_stake_authority_address(program_id, pool_address),
397            false,
398        ),
399        AccountMeta::new_readonly(
400            find_pool_mint_authority_address(program_id, pool_address),
401            false,
402        ),
403        AccountMeta::new(*user_stake_account, false),
404        AccountMeta::new(*user_token_account, false),
405        AccountMeta::new_readonly(sysvar::clock::id(), false),
406        AccountMeta::new_readonly(spl_token::id(), false),
407        AccountMeta::new_readonly(stake::program::id(), false),
408    ];
409
410    Instruction {
411        program_id: *program_id,
412        accounts,
413        data,
414    }
415}
416
417/// Creates necessary instructions to create and delegate a new stake account to
418/// a given validator. Uses a fixed address for each wallet and vote account
419/// combination to make it easier to find for deposits. This is an optional
420/// helper function; deposits can come from any owned stake account without
421/// lockup.
422pub fn create_and_delegate_user_stake(
423    program_id: &Pubkey,
424    vote_account_address: &Pubkey,
425    user_wallet: &Pubkey,
426    rent: &Rent,
427    stake_amount: u64,
428) -> Vec<Instruction> {
429    let pool_address = find_pool_address(program_id, vote_account_address);
430    let stake_space = std::mem::size_of::<stake::state::StakeStateV2>();
431    let lamports = rent
432        .minimum_balance(stake_space)
433        .saturating_add(stake_amount);
434    let (deposit_address, deposit_seed) =
435        find_default_deposit_account_address_and_seed(&pool_address, user_wallet);
436
437    stake::instruction::create_account_with_seed_and_delegate_stake(
438        user_wallet,
439        &deposit_address,
440        user_wallet,
441        &deposit_seed,
442        vote_account_address,
443        &stake::state::Authorized::auto(user_wallet),
444        &stake::state::Lockup::default(),
445        lamports,
446    )
447}
448
449/// Creates a `CreateTokenMetadata` instruction.
450pub fn create_token_metadata(
451    program_id: &Pubkey,
452    pool_address: &Pubkey,
453    payer: &Pubkey,
454) -> Instruction {
455    let pool_mint = find_pool_mint_address(program_id, pool_address);
456    let (token_metadata, _) = find_metadata_account(&pool_mint);
457    let data = borsh::to_vec(&SinglePoolInstruction::CreateTokenMetadata).unwrap();
458
459    let accounts = vec![
460        AccountMeta::new_readonly(*pool_address, false),
461        AccountMeta::new_readonly(pool_mint, false),
462        AccountMeta::new_readonly(
463            find_pool_mint_authority_address(program_id, pool_address),
464            false,
465        ),
466        AccountMeta::new_readonly(
467            find_pool_mpl_authority_address(program_id, pool_address),
468            false,
469        ),
470        AccountMeta::new(*payer, true),
471        AccountMeta::new(token_metadata, false),
472        AccountMeta::new_readonly(inline_mpl_token_metadata::id(), false),
473        AccountMeta::new_readonly(system_program::id(), false),
474    ];
475
476    Instruction {
477        program_id: *program_id,
478        accounts,
479        data,
480    }
481}
482
483/// Creates an `UpdateTokenMetadata` instruction.
484pub fn update_token_metadata(
485    program_id: &Pubkey,
486    vote_account_address: &Pubkey,
487    authorized_withdrawer: &Pubkey,
488    name: String,
489    symbol: String,
490    uri: String,
491) -> Instruction {
492    let pool_address = find_pool_address(program_id, vote_account_address);
493    let pool_mint = find_pool_mint_address(program_id, &pool_address);
494    let (token_metadata, _) = find_metadata_account(&pool_mint);
495    let data =
496        borsh::to_vec(&SinglePoolInstruction::UpdateTokenMetadata { name, symbol, uri }).unwrap();
497
498    let accounts = vec![
499        AccountMeta::new_readonly(*vote_account_address, false),
500        AccountMeta::new_readonly(pool_address, false),
501        AccountMeta::new_readonly(
502            find_pool_mpl_authority_address(program_id, &pool_address),
503            false,
504        ),
505        AccountMeta::new_readonly(*authorized_withdrawer, true),
506        AccountMeta::new(token_metadata, false),
507        AccountMeta::new_readonly(inline_mpl_token_metadata::id(), false),
508    ];
509
510    Instruction {
511        program_id: *program_id,
512        accounts,
513        data,
514    }
515}
516
517/// Creates a `InitializePoolOnRamp` instruction.
518pub fn initialize_pool_onramp(program_id: &Pubkey, pool_address: &Pubkey) -> Instruction {
519    let data = borsh::to_vec(&SinglePoolInstruction::InitializePoolOnRamp).unwrap();
520    let accounts = vec![
521        AccountMeta::new_readonly(*pool_address, false),
522        AccountMeta::new(find_pool_onramp_address(program_id, pool_address), false),
523        AccountMeta::new_readonly(
524            find_pool_stake_authority_address(program_id, pool_address),
525            false,
526        ),
527        AccountMeta::new_readonly(sysvar::rent::id(), false),
528        AccountMeta::new_readonly(system_program::id(), false),
529        AccountMeta::new_readonly(stake::program::id(), false),
530    ];
531
532    Instruction {
533        program_id: *program_id,
534        accounts,
535        data,
536    }
537}
538
539/// Creates a `InitializePoolOnRamp` instruction plus the transfer to fund it.
540/// This is for convenience, for users who need to create an on-ramp for existing pools.
541/// We don't use it internally, because `initialize()` carries the necessary logic.
542pub fn create_pool_onramp(
543    program_id: &Pubkey,
544    pool_address: &Pubkey,
545    payer: &Pubkey,
546    rent: &Rent,
547) -> Vec<Instruction> {
548    let onramp_address = find_pool_onramp_address(program_id, pool_address);
549    let stake_space = std::mem::size_of::<stake::state::StakeStateV2>();
550    let stake_rent = rent.minimum_balance(stake_space);
551
552    vec![
553        system_instruction::transfer(payer, &onramp_address, stake_rent),
554        initialize_pool_onramp(program_id, pool_address),
555    ]
556}