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_instruction::{AccountMeta, Instruction},
15    solana_program_pack::Pack,
16    solana_pubkey::Pubkey,
17    solana_rent::Rent,
18    solana_stake_interface as stake,
19    solana_system_interface::{instruction as system_instruction, program as system_program},
20    solana_sysvar as sysvar,
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 (not yet enforced)
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 (not yet enforced)
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    ///   This is a temporary instruction that will be deprecated some time after all
162    ///   existing pools have upgraded. Its logic is intended to be incorporated
163    ///   into `InitializePool` itself.
164    ///
165    ///   0. `[]` Pool account
166    ///   1. `[w]` Pool on-ramp account
167    ///   2. `[]` Pool stake authority
168    ///   3. `[]` Rent sysvar
169    ///   4. `[]` System program
170    ///   5. `[]` Stake program
171    InitializePoolOnRamp,
172}
173
174/// Creates all necessary instructions to initialize the stake pool.
175pub fn initialize(
176    program_id: &Pubkey,
177    vote_account_address: &Pubkey,
178    payer: &Pubkey,
179    rent: &Rent,
180    minimum_pool_balance: u64,
181) -> Vec<Instruction> {
182    let pool_address = find_pool_address(program_id, vote_account_address);
183    let pool_rent = rent.minimum_balance(std::mem::size_of::<SinglePool>());
184
185    let stake_address = find_pool_stake_address(program_id, &pool_address);
186    let onramp_address = find_pool_onramp_address(program_id, &pool_address);
187    let stake_space = std::mem::size_of::<stake::state::StakeStateV2>();
188    let stake_rent = rent.minimum_balance(stake_space);
189    let stake_rent_plus_minimum = stake_rent.saturating_add(minimum_pool_balance);
190
191    let mint_address = find_pool_mint_address(program_id, &pool_address);
192    let mint_rent = rent.minimum_balance(spl_token::state::Mint::LEN);
193
194    vec![
195        system_instruction::transfer(payer, &pool_address, pool_rent),
196        system_instruction::transfer(payer, &stake_address, stake_rent_plus_minimum),
197        system_instruction::transfer(payer, &onramp_address, stake_rent),
198        system_instruction::transfer(payer, &mint_address, mint_rent),
199        initialize_pool(program_id, vote_account_address),
200        initialize_pool_onramp(program_id, &pool_address),
201        create_token_metadata(program_id, &pool_address, payer),
202    ]
203}
204
205/// Creates an `InitializePool` instruction.
206pub fn initialize_pool(program_id: &Pubkey, vote_account_address: &Pubkey) -> Instruction {
207    let pool_address = find_pool_address(program_id, vote_account_address);
208    let mint_address = find_pool_mint_address(program_id, &pool_address);
209
210    let data = borsh::to_vec(&SinglePoolInstruction::InitializePool).unwrap();
211    let accounts = vec![
212        AccountMeta::new_readonly(*vote_account_address, false),
213        AccountMeta::new(pool_address, false),
214        AccountMeta::new(find_pool_stake_address(program_id, &pool_address), false),
215        AccountMeta::new(mint_address, false),
216        AccountMeta::new_readonly(
217            find_pool_stake_authority_address(program_id, &pool_address),
218            false,
219        ),
220        AccountMeta::new_readonly(
221            find_pool_mint_authority_address(program_id, &pool_address),
222            false,
223        ),
224        AccountMeta::new_readonly(sysvar::rent::id(), false),
225        AccountMeta::new_readonly(sysvar::clock::id(), false),
226        AccountMeta::new_readonly(sysvar::stake_history::id(), false),
227        #[allow(deprecated)]
228        AccountMeta::new_readonly(stake::config::id(), false),
229        AccountMeta::new_readonly(system_program::id(), false),
230        AccountMeta::new_readonly(spl_token::id(), false),
231        AccountMeta::new_readonly(stake::program::id(), false),
232    ];
233
234    Instruction {
235        program_id: *program_id,
236        accounts,
237        data,
238    }
239}
240
241/// Creates a `ReplenishPool` instruction.
242pub fn replenish_pool(program_id: &Pubkey, vote_account_address: &Pubkey) -> Instruction {
243    let pool_address = find_pool_address(program_id, vote_account_address);
244
245    let data = borsh::to_vec(&SinglePoolInstruction::ReplenishPool).unwrap();
246    let accounts = vec![
247        AccountMeta::new_readonly(*vote_account_address, false),
248        AccountMeta::new_readonly(pool_address, false),
249        AccountMeta::new(find_pool_stake_address(program_id, &pool_address), false),
250        AccountMeta::new(find_pool_onramp_address(program_id, &pool_address), false),
251        AccountMeta::new_readonly(
252            find_pool_stake_authority_address(program_id, &pool_address),
253            false,
254        ),
255        AccountMeta::new_readonly(sysvar::clock::id(), false),
256        AccountMeta::new_readonly(sysvar::stake_history::id(), false),
257        #[allow(deprecated)]
258        AccountMeta::new_readonly(stake::config::id(), false),
259        AccountMeta::new_readonly(stake::program::id(), false),
260    ];
261
262    Instruction {
263        program_id: *program_id,
264        accounts,
265        data,
266    }
267}
268
269/// Creates all necessary instructions to deposit stake.
270pub fn deposit(
271    program_id: &Pubkey,
272    pool_address: &Pubkey,
273    user_stake_account: &Pubkey,
274    user_token_account: &Pubkey,
275    user_lamport_account: &Pubkey,
276    user_withdraw_authority: &Pubkey,
277) -> Vec<Instruction> {
278    let pool_stake_authority = find_pool_stake_authority_address(program_id, pool_address);
279
280    vec![
281        stake::instruction::authorize(
282            user_stake_account,
283            user_withdraw_authority,
284            &pool_stake_authority,
285            stake::state::StakeAuthorize::Staker,
286            None,
287        ),
288        stake::instruction::authorize(
289            user_stake_account,
290            user_withdraw_authority,
291            &pool_stake_authority,
292            stake::state::StakeAuthorize::Withdrawer,
293            None,
294        ),
295        deposit_stake(
296            program_id,
297            pool_address,
298            user_stake_account,
299            user_token_account,
300            user_lamport_account,
301        ),
302    ]
303}
304
305/// Creates a `DepositStake` instruction.
306pub fn deposit_stake(
307    program_id: &Pubkey,
308    pool_address: &Pubkey,
309    user_stake_account: &Pubkey,
310    user_token_account: &Pubkey,
311    user_lamport_account: &Pubkey,
312) -> Instruction {
313    let data = borsh::to_vec(&SinglePoolInstruction::DepositStake).unwrap();
314
315    let accounts = vec![
316        AccountMeta::new_readonly(*pool_address, false),
317        AccountMeta::new(find_pool_stake_address(program_id, pool_address), false),
318        AccountMeta::new_readonly(find_pool_onramp_address(program_id, pool_address), false),
319        AccountMeta::new(find_pool_mint_address(program_id, pool_address), false),
320        AccountMeta::new_readonly(
321            find_pool_stake_authority_address(program_id, pool_address),
322            false,
323        ),
324        AccountMeta::new_readonly(
325            find_pool_mint_authority_address(program_id, pool_address),
326            false,
327        ),
328        AccountMeta::new(*user_stake_account, false),
329        AccountMeta::new(*user_token_account, false),
330        AccountMeta::new(*user_lamport_account, false),
331        AccountMeta::new_readonly(sysvar::clock::id(), false),
332        AccountMeta::new_readonly(sysvar::stake_history::id(), false),
333        AccountMeta::new_readonly(spl_token::id(), false),
334        AccountMeta::new_readonly(stake::program::id(), false),
335    ];
336
337    Instruction {
338        program_id: *program_id,
339        accounts,
340        data,
341    }
342}
343
344/// Creates all necessary instructions to withdraw stake into a given stake
345/// account. If a new stake account is required, the user should first include
346/// `system_instruction::create_account` with account size
347/// `std::mem::size_of::<stake::state::StakeStateV2>()` and owner
348/// `stake::program::id()`.
349pub fn withdraw(
350    program_id: &Pubkey,
351    pool_address: &Pubkey,
352    user_stake_account: &Pubkey,
353    user_stake_authority: &Pubkey,
354    user_token_account: &Pubkey,
355    user_token_authority: &Pubkey,
356    token_amount: u64,
357) -> Vec<Instruction> {
358    vec![
359        spl_token::instruction::approve(
360            &spl_token::id(),
361            user_token_account,
362            &find_pool_mint_authority_address(program_id, pool_address),
363            user_token_authority,
364            &[],
365            token_amount,
366        )
367        .unwrap(),
368        withdraw_stake(
369            program_id,
370            pool_address,
371            user_stake_account,
372            user_stake_authority,
373            user_token_account,
374            token_amount,
375        ),
376    ]
377}
378
379/// Creates a `WithdrawStake` instruction.
380pub fn withdraw_stake(
381    program_id: &Pubkey,
382    pool_address: &Pubkey,
383    user_stake_account: &Pubkey,
384    user_stake_authority: &Pubkey,
385    user_token_account: &Pubkey,
386    token_amount: u64,
387) -> Instruction {
388    let data = borsh::to_vec(&SinglePoolInstruction::WithdrawStake {
389        user_stake_authority: *user_stake_authority,
390        token_amount,
391    })
392    .unwrap();
393
394    let accounts = vec![
395        AccountMeta::new_readonly(*pool_address, false),
396        AccountMeta::new(find_pool_stake_address(program_id, pool_address), false),
397        AccountMeta::new_readonly(find_pool_onramp_address(program_id, pool_address), false),
398        AccountMeta::new(find_pool_mint_address(program_id, pool_address), false),
399        AccountMeta::new_readonly(
400            find_pool_stake_authority_address(program_id, pool_address),
401            false,
402        ),
403        AccountMeta::new_readonly(
404            find_pool_mint_authority_address(program_id, pool_address),
405            false,
406        ),
407        AccountMeta::new(*user_stake_account, false),
408        AccountMeta::new(*user_token_account, false),
409        AccountMeta::new_readonly(sysvar::clock::id(), false),
410        AccountMeta::new_readonly(spl_token::id(), false),
411        AccountMeta::new_readonly(stake::program::id(), false),
412    ];
413
414    Instruction {
415        program_id: *program_id,
416        accounts,
417        data,
418    }
419}
420
421/// Creates necessary instructions to create and delegate a new stake account to
422/// a given validator. Uses a fixed address for each wallet and vote account
423/// combination to make it easier to find for deposits. This is an optional
424/// helper function; deposits can come from any owned stake account without
425/// lockup.
426#[deprecated(
427    since = "3.0.0",
428    note = "Default deposit helpers will be removed in a future release; these were \
429    intended to support a wallet flow that never materialized. To set up a new stake \
430    account for deposit, use `instruction::create_account_and_delegate_stake` from \
431    `solana-stake-interface` using any normal keypair."
432)]
433pub fn create_and_delegate_user_stake(
434    program_id: &Pubkey,
435    vote_account_address: &Pubkey,
436    user_wallet: &Pubkey,
437    rent: &Rent,
438    stake_amount: u64,
439) -> Vec<Instruction> {
440    let pool_address = find_pool_address(program_id, vote_account_address);
441    let stake_space = std::mem::size_of::<stake::state::StakeStateV2>();
442    let lamports = rent
443        .minimum_balance(stake_space)
444        .saturating_add(stake_amount);
445    let (deposit_address, deposit_seed) =
446        find_default_deposit_account_address_and_seed(&pool_address, user_wallet);
447
448    stake::instruction::create_account_with_seed_and_delegate_stake(
449        user_wallet,
450        &deposit_address,
451        user_wallet,
452        &deposit_seed,
453        vote_account_address,
454        &stake::state::Authorized::auto(user_wallet),
455        &stake::state::Lockup::default(),
456        lamports,
457    )
458}
459
460/// Creates a `CreateTokenMetadata` instruction.
461pub fn create_token_metadata(
462    program_id: &Pubkey,
463    pool_address: &Pubkey,
464    payer: &Pubkey,
465) -> Instruction {
466    let pool_mint = find_pool_mint_address(program_id, pool_address);
467    let (token_metadata, _) = find_metadata_account(&pool_mint);
468    let data = borsh::to_vec(&SinglePoolInstruction::CreateTokenMetadata).unwrap();
469
470    let accounts = vec![
471        AccountMeta::new_readonly(*pool_address, false),
472        AccountMeta::new_readonly(pool_mint, false),
473        AccountMeta::new_readonly(
474            find_pool_mint_authority_address(program_id, pool_address),
475            false,
476        ),
477        AccountMeta::new_readonly(
478            find_pool_mpl_authority_address(program_id, pool_address),
479            false,
480        ),
481        AccountMeta::new(*payer, true),
482        AccountMeta::new(token_metadata, false),
483        AccountMeta::new_readonly(inline_mpl_token_metadata::id(), false),
484        AccountMeta::new_readonly(system_program::id(), false),
485    ];
486
487    Instruction {
488        program_id: *program_id,
489        accounts,
490        data,
491    }
492}
493
494/// Creates an `UpdateTokenMetadata` instruction.
495pub fn update_token_metadata(
496    program_id: &Pubkey,
497    vote_account_address: &Pubkey,
498    authorized_withdrawer: &Pubkey,
499    name: String,
500    symbol: String,
501    uri: String,
502) -> Instruction {
503    let pool_address = find_pool_address(program_id, vote_account_address);
504    let pool_mint = find_pool_mint_address(program_id, &pool_address);
505    let (token_metadata, _) = find_metadata_account(&pool_mint);
506    let data =
507        borsh::to_vec(&SinglePoolInstruction::UpdateTokenMetadata { name, symbol, uri }).unwrap();
508
509    let accounts = vec![
510        AccountMeta::new_readonly(*vote_account_address, false),
511        AccountMeta::new_readonly(pool_address, false),
512        AccountMeta::new_readonly(
513            find_pool_mpl_authority_address(program_id, &pool_address),
514            false,
515        ),
516        AccountMeta::new_readonly(*authorized_withdrawer, true),
517        AccountMeta::new(token_metadata, false),
518        AccountMeta::new_readonly(inline_mpl_token_metadata::id(), false),
519    ];
520
521    Instruction {
522        program_id: *program_id,
523        accounts,
524        data,
525    }
526}
527
528/// Creates a `InitializePoolOnRamp` instruction.
529pub fn initialize_pool_onramp(program_id: &Pubkey, pool_address: &Pubkey) -> Instruction {
530    let data = borsh::to_vec(&SinglePoolInstruction::InitializePoolOnRamp).unwrap();
531    let accounts = vec![
532        AccountMeta::new_readonly(*pool_address, false),
533        AccountMeta::new(find_pool_onramp_address(program_id, pool_address), false),
534        AccountMeta::new_readonly(
535            find_pool_stake_authority_address(program_id, pool_address),
536            false,
537        ),
538        AccountMeta::new_readonly(sysvar::rent::id(), false),
539        AccountMeta::new_readonly(system_program::id(), false),
540        AccountMeta::new_readonly(stake::program::id(), false),
541    ];
542
543    Instruction {
544        program_id: *program_id,
545        accounts,
546        data,
547    }
548}
549
550/// Creates a `InitializePoolOnRamp` instruction plus the transfer to fund it.
551/// This is for convenience, for users who need to create an on-ramp for existing pools.
552/// We don't use it internally, because `initialize()` carries the necessary logic.
553pub fn create_pool_onramp(
554    program_id: &Pubkey,
555    pool_address: &Pubkey,
556    payer: &Pubkey,
557    rent: &Rent,
558) -> Vec<Instruction> {
559    let onramp_address = find_pool_onramp_address(program_id, pool_address);
560    let stake_space = std::mem::size_of::<stake::state::StakeStateV2>();
561    let stake_rent = rent.minimum_balance(stake_space);
562
563    vec![
564        system_instruction::transfer(payer, &onramp_address, stake_rent),
565        initialize_pool_onramp(program_id, pool_address),
566    ]
567}