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` and `InitializePoolOnRamp`.
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 also enable depositing liquid sol for pool tokens via `DepositSol`.
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. Note the on-ramp IS necessary for the pool to operate.
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    ///   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
190    ///  15. `[]` Single-validator stake pool program
191    DepositSol {
192        /// Amount of sol to deposit
193        lamports: u64,
194    },
195}
196
197/// Creates all necessary instructions to initialize the stake pool.
198pub fn initialize(
199    program_id: &Pubkey,
200    vote_account_address: &Pubkey,
201    payer: &Pubkey,
202    rent: &Rent,
203    minimum_pool_balance: u64,
204) -> Vec<Instruction> {
205    let pool_address = find_pool_address(program_id, vote_account_address);
206    let pool_rent = rent.minimum_balance(SinglePool::size_of());
207
208    let stake_address = find_pool_stake_address(program_id, &pool_address);
209    let onramp_address = find_pool_onramp_address(program_id, &pool_address);
210    let stake_space = stake::state::StakeStateV2::size_of();
211    let stake_rent = rent.minimum_balance(stake_space);
212    let stake_rent_plus_minimum = stake_rent.saturating_add(minimum_pool_balance);
213
214    let mint_address = find_pool_mint_address(program_id, &pool_address);
215    let mint_rent = rent.minimum_balance(spl_token::state::Mint::LEN);
216
217    vec![
218        system_instruction::transfer(payer, &pool_address, pool_rent),
219        system_instruction::transfer(payer, &stake_address, stake_rent_plus_minimum),
220        system_instruction::transfer(payer, &onramp_address, stake_rent),
221        system_instruction::transfer(payer, &mint_address, mint_rent),
222        initialize_pool(program_id, vote_account_address),
223        initialize_pool_onramp(program_id, &pool_address),
224        create_token_metadata(program_id, &pool_address, payer),
225    ]
226}
227
228/// Creates an `InitializePool` instruction.
229pub fn initialize_pool(program_id: &Pubkey, vote_account_address: &Pubkey) -> Instruction {
230    let pool_address = find_pool_address(program_id, vote_account_address);
231    let mint_address = find_pool_mint_address(program_id, &pool_address);
232
233    let data = borsh::to_vec(&SinglePoolInstruction::InitializePool).unwrap();
234    let accounts = vec![
235        AccountMeta::new_readonly(*vote_account_address, false),
236        AccountMeta::new(pool_address, false),
237        AccountMeta::new(find_pool_stake_address(program_id, &pool_address), false),
238        AccountMeta::new(mint_address, false),
239        AccountMeta::new_readonly(
240            find_pool_stake_authority_address(program_id, &pool_address),
241            false,
242        ),
243        AccountMeta::new_readonly(
244            find_pool_mint_authority_address(program_id, &pool_address),
245            false,
246        ),
247        AccountMeta::new_readonly(sysvar::rent::id(), false),
248        AccountMeta::new_readonly(sysvar::clock::id(), false),
249        AccountMeta::new_readonly(stake_history::id(), false),
250        #[allow(deprecated)]
251        AccountMeta::new_readonly(stake::config::id(), false),
252        AccountMeta::new_readonly(system_program::id(), false),
253        AccountMeta::new_readonly(spl_token::id(), false),
254        AccountMeta::new_readonly(stake::program::id(), false),
255    ];
256
257    Instruction {
258        program_id: *program_id,
259        accounts,
260        data,
261    }
262}
263
264/// Creates a `ReplenishPool` instruction.
265pub fn replenish_pool(program_id: &Pubkey, vote_account_address: &Pubkey) -> Instruction {
266    let pool_address = find_pool_address(program_id, vote_account_address);
267
268    let data = borsh::to_vec(&SinglePoolInstruction::ReplenishPool).unwrap();
269    let accounts = vec![
270        AccountMeta::new_readonly(*vote_account_address, false),
271        AccountMeta::new_readonly(pool_address, false),
272        AccountMeta::new(find_pool_stake_address(program_id, &pool_address), false),
273        AccountMeta::new(find_pool_onramp_address(program_id, &pool_address), false),
274        AccountMeta::new_readonly(
275            find_pool_stake_authority_address(program_id, &pool_address),
276            false,
277        ),
278        AccountMeta::new_readonly(sysvar::clock::id(), false),
279        AccountMeta::new_readonly(stake_history::id(), false),
280        #[allow(deprecated)]
281        AccountMeta::new_readonly(stake::config::id(), false),
282        AccountMeta::new_readonly(stake::program::id(), false),
283    ];
284
285    Instruction {
286        program_id: *program_id,
287        accounts,
288        data,
289    }
290}
291
292/// Creates all necessary instructions to deposit stake.
293pub fn deposit(
294    program_id: &Pubkey,
295    pool_address: &Pubkey,
296    user_stake_account: &Pubkey,
297    user_token_account: &Pubkey,
298    user_lamport_account: &Pubkey,
299    user_withdraw_authority: &Pubkey,
300) -> Vec<Instruction> {
301    let pool_stake_authority = find_pool_stake_authority_address(program_id, pool_address);
302
303    vec![
304        stake::instruction::authorize(
305            user_stake_account,
306            user_withdraw_authority,
307            &pool_stake_authority,
308            stake::state::StakeAuthorize::Staker,
309            None,
310        ),
311        stake::instruction::authorize(
312            user_stake_account,
313            user_withdraw_authority,
314            &pool_stake_authority,
315            stake::state::StakeAuthorize::Withdrawer,
316            None,
317        ),
318        deposit_stake(
319            program_id,
320            pool_address,
321            user_stake_account,
322            user_token_account,
323            user_lamport_account,
324        ),
325    ]
326}
327
328/// Creates a `DepositStake` instruction.
329pub fn deposit_stake(
330    program_id: &Pubkey,
331    pool_address: &Pubkey,
332    user_stake_account: &Pubkey,
333    user_token_account: &Pubkey,
334    user_lamport_account: &Pubkey,
335) -> Instruction {
336    let data = borsh::to_vec(&SinglePoolInstruction::DepositStake).unwrap();
337
338    let accounts = vec![
339        AccountMeta::new_readonly(*pool_address, false),
340        AccountMeta::new(find_pool_stake_address(program_id, pool_address), false),
341        AccountMeta::new_readonly(find_pool_onramp_address(program_id, pool_address), false),
342        AccountMeta::new(find_pool_mint_address(program_id, pool_address), false),
343        AccountMeta::new_readonly(
344            find_pool_stake_authority_address(program_id, pool_address),
345            false,
346        ),
347        AccountMeta::new_readonly(
348            find_pool_mint_authority_address(program_id, pool_address),
349            false,
350        ),
351        AccountMeta::new(*user_stake_account, false),
352        AccountMeta::new(*user_token_account, false),
353        AccountMeta::new(*user_lamport_account, false),
354        AccountMeta::new_readonly(sysvar::clock::id(), false),
355        AccountMeta::new_readonly(stake_history::id(), false),
356        AccountMeta::new_readonly(spl_token::id(), false),
357        AccountMeta::new_readonly(stake::program::id(), false),
358    ];
359
360    Instruction {
361        program_id: *program_id,
362        accounts,
363        data,
364    }
365}
366
367/// Creates the necessary instructions to deposit liquid sol.
368/// `escrow_deposit_account` should be the pubkey of an unused keypair.
369/// This avoids passing a wallet signature into an opaque program,
370/// a best practice for safety given its untrammeled authority.
371/// The escrow account does not have to meet rent exemption because it is
372/// opened and closed in the span of one transaction.
373pub fn deposit_liquid(
374    program_id: &Pubkey,
375    vote_account_address: &Pubkey,
376    user_wallet: &Pubkey,
377    escrow_deposit_account: &Pubkey,
378    user_token_account: &Pubkey,
379    lamports: u64,
380) -> Vec<Instruction> {
381    vec![
382        system_instruction::transfer(user_wallet, escrow_deposit_account, lamports),
383        deposit_sol(
384            program_id,
385            vote_account_address,
386            escrow_deposit_account,
387            user_token_account,
388            lamports,
389        ),
390    ]
391}
392
393/// Creates a `DepositSol` instruction.
394/// It is recommended as a matter of hygiene to use the `deposit_liquid()` helper,
395/// to isolate user wallet signing authority from the program.
396pub fn deposit_sol(
397    program_id: &Pubkey,
398    vote_account_address: &Pubkey,
399    user_deposit_account: &Pubkey,
400    user_token_account: &Pubkey,
401    lamports: u64,
402) -> Instruction {
403    let pool_address = find_pool_address(program_id, vote_account_address);
404
405    let data = borsh::to_vec(&SinglePoolInstruction::DepositSol { lamports }).unwrap();
406    let accounts = vec![
407        AccountMeta::new_readonly(*vote_account_address, false),
408        AccountMeta::new_readonly(pool_address, false),
409        AccountMeta::new(find_pool_stake_address(program_id, &pool_address), false),
410        AccountMeta::new(find_pool_onramp_address(program_id, &pool_address), false),
411        AccountMeta::new(find_pool_mint_address(program_id, &pool_address), false),
412        AccountMeta::new_readonly(
413            find_pool_stake_authority_address(program_id, &pool_address),
414            false,
415        ),
416        AccountMeta::new_readonly(
417            find_pool_mint_authority_address(program_id, &pool_address),
418            false,
419        ),
420        AccountMeta::new(*user_deposit_account, true),
421        AccountMeta::new(*user_token_account, false),
422        AccountMeta::new_readonly(sysvar::clock::id(), false),
423        AccountMeta::new_readonly(stake_history::id(), false),
424        #[allow(deprecated)]
425        AccountMeta::new_readonly(stake::config::id(), false),
426        AccountMeta::new_readonly(system_program::id(), false),
427        AccountMeta::new_readonly(spl_token::id(), false),
428        AccountMeta::new_readonly(stake::program::id(), false),
429        AccountMeta::new_readonly(*program_id, false),
430    ];
431
432    Instruction {
433        program_id: *program_id,
434        accounts,
435        data,
436    }
437}
438
439/// Creates all necessary instructions to withdraw stake into a given stake
440/// account. If a new stake account is required, the user should first include
441/// `system_instruction::create_account` with account size
442/// `stake::state::StakeStateV2::size_of()` and owner `stake::program::id()`.
443pub fn withdraw(
444    program_id: &Pubkey,
445    pool_address: &Pubkey,
446    user_stake_account: &Pubkey,
447    user_stake_authority: &Pubkey,
448    user_token_account: &Pubkey,
449    user_token_authority: &Pubkey,
450    token_amount: u64,
451) -> Vec<Instruction> {
452    vec![
453        spl_token::instruction::approve(
454            &spl_token::id(),
455            user_token_account,
456            &find_pool_mint_authority_address(program_id, pool_address),
457            user_token_authority,
458            &[],
459            token_amount,
460        )
461        .unwrap(),
462        withdraw_stake(
463            program_id,
464            pool_address,
465            user_stake_account,
466            user_stake_authority,
467            user_token_account,
468            token_amount,
469        ),
470    ]
471}
472
473/// Creates a `WithdrawStake` instruction.
474pub fn withdraw_stake(
475    program_id: &Pubkey,
476    pool_address: &Pubkey,
477    user_stake_account: &Pubkey,
478    user_stake_authority: &Pubkey,
479    user_token_account: &Pubkey,
480    token_amount: u64,
481) -> Instruction {
482    let data = borsh::to_vec(&SinglePoolInstruction::WithdrawStake {
483        user_stake_authority: *user_stake_authority,
484        token_amount,
485    })
486    .unwrap();
487
488    let accounts = vec![
489        AccountMeta::new_readonly(*pool_address, false),
490        AccountMeta::new(find_pool_stake_address(program_id, pool_address), false),
491        AccountMeta::new_readonly(find_pool_onramp_address(program_id, pool_address), false),
492        AccountMeta::new(find_pool_mint_address(program_id, pool_address), false),
493        AccountMeta::new_readonly(
494            find_pool_stake_authority_address(program_id, pool_address),
495            false,
496        ),
497        AccountMeta::new_readonly(
498            find_pool_mint_authority_address(program_id, pool_address),
499            false,
500        ),
501        AccountMeta::new(*user_stake_account, false),
502        AccountMeta::new(*user_token_account, false),
503        AccountMeta::new_readonly(sysvar::clock::id(), false),
504        AccountMeta::new_readonly(spl_token::id(), false),
505        AccountMeta::new_readonly(stake::program::id(), false),
506    ];
507
508    Instruction {
509        program_id: *program_id,
510        accounts,
511        data,
512    }
513}
514
515/// Creates a `CreateTokenMetadata` instruction.
516pub fn create_token_metadata(
517    program_id: &Pubkey,
518    pool_address: &Pubkey,
519    payer: &Pubkey,
520) -> Instruction {
521    let pool_mint = find_pool_mint_address(program_id, pool_address);
522    let (token_metadata, _) = find_metadata_account(&pool_mint);
523    let data = borsh::to_vec(&SinglePoolInstruction::CreateTokenMetadata).unwrap();
524
525    let accounts = vec![
526        AccountMeta::new_readonly(*pool_address, false),
527        AccountMeta::new_readonly(pool_mint, false),
528        AccountMeta::new_readonly(
529            find_pool_mint_authority_address(program_id, pool_address),
530            false,
531        ),
532        AccountMeta::new_readonly(
533            find_pool_mpl_authority_address(program_id, pool_address),
534            false,
535        ),
536        AccountMeta::new(*payer, true),
537        AccountMeta::new(token_metadata, false),
538        AccountMeta::new_readonly(inline_mpl_token_metadata::id(), false),
539        AccountMeta::new_readonly(system_program::id(), false),
540    ];
541
542    Instruction {
543        program_id: *program_id,
544        accounts,
545        data,
546    }
547}
548
549/// Creates an `UpdateTokenMetadata` instruction.
550pub fn update_token_metadata(
551    program_id: &Pubkey,
552    vote_account_address: &Pubkey,
553    authorized_withdrawer: &Pubkey,
554    name: String,
555    symbol: String,
556    uri: String,
557) -> Instruction {
558    let pool_address = find_pool_address(program_id, vote_account_address);
559    let pool_mint = find_pool_mint_address(program_id, &pool_address);
560    let (token_metadata, _) = find_metadata_account(&pool_mint);
561    let data =
562        borsh::to_vec(&SinglePoolInstruction::UpdateTokenMetadata { name, symbol, uri }).unwrap();
563
564    let accounts = vec![
565        AccountMeta::new_readonly(*vote_account_address, false),
566        AccountMeta::new_readonly(pool_address, false),
567        AccountMeta::new_readonly(
568            find_pool_mpl_authority_address(program_id, &pool_address),
569            false,
570        ),
571        AccountMeta::new_readonly(*authorized_withdrawer, true),
572        AccountMeta::new(token_metadata, false),
573        AccountMeta::new_readonly(inline_mpl_token_metadata::id(), false),
574    ];
575
576    Instruction {
577        program_id: *program_id,
578        accounts,
579        data,
580    }
581}
582
583/// Creates a `InitializePoolOnRamp` instruction.
584pub fn initialize_pool_onramp(program_id: &Pubkey, pool_address: &Pubkey) -> Instruction {
585    let data = borsh::to_vec(&SinglePoolInstruction::InitializePoolOnRamp).unwrap();
586    let accounts = vec![
587        AccountMeta::new_readonly(*pool_address, false),
588        AccountMeta::new(find_pool_onramp_address(program_id, pool_address), false),
589        AccountMeta::new_readonly(
590            find_pool_stake_authority_address(program_id, pool_address),
591            false,
592        ),
593        AccountMeta::new_readonly(sysvar::rent::id(), false),
594        AccountMeta::new_readonly(system_program::id(), false),
595        AccountMeta::new_readonly(stake::program::id(), false),
596    ];
597
598    Instruction {
599        program_id: *program_id,
600        accounts,
601        data,
602    }
603}
604
605/// Creates a `InitializePoolOnRamp` instruction plus the transfer to fund it.
606/// This is for convenience, for users who need to create an on-ramp for existing pools.
607/// We don't use it internally, because `initialize()` carries the necessary logic.
608pub fn create_pool_onramp(
609    program_id: &Pubkey,
610    pool_address: &Pubkey,
611    payer: &Pubkey,
612    rent: &Rent,
613) -> Vec<Instruction> {
614    let onramp_address = find_pool_onramp_address(program_id, pool_address);
615    let stake_space = stake::state::StakeStateV2::size_of();
616    let stake_rent = rent.minimum_balance(stake_space);
617
618    vec![
619        system_instruction::transfer(payer, &onramp_address, stake_rent),
620        initialize_pool_onramp(program_id, pool_address),
621    ]
622}