diff --git a/Cargo.lock b/Cargo.lock index 3d09ac61520..7e2cb6dfbd9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2424,6 +2424,38 @@ dependencies = [ "syn 1.0.91", ] +[[package]] +name = "mpl-token-metadata" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8b06b6275bd3f6444e22b03de7bdf6145ee6d6fa3e14415ddd317473b9ef807" +dependencies = [ + "arrayref", + "borsh", + "mpl-token-vault", + "num-derive", + "num-traits", + "shank", + "solana-program", + "spl-associated-token-account 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "spl-token 3.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "thiserror", +] + +[[package]] +name = "mpl-token-vault" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ade4ef15bc06a6033076c4ff28cba9b42521df5ec61211d6f419415ace2746a" +dependencies = [ + "borsh", + "num-derive", + "num-traits", + "solana-program", + "spl-token 3.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "thiserror", +] + [[package]] name = "multimap" version = "0.8.3" @@ -3983,6 +4015,40 @@ dependencies = [ "keccak", ] +[[package]] +name = "shank" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62c7f8aac4c67081e718ff2a7754a6264774a0eaa6ed64a6e94b1ec1c17af200" +dependencies = [ + "shank_macro", +] + +[[package]] +name = "shank_macro" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a7538be7f2a6530a37d4e03de34569a1f27a2287b3efe06732d7e5648a9105" +dependencies = [ + "proc-macro2 1.0.36", + "quote 1.0.14", + "shank_macro_impl", + "syn 1.0.91", +] + +[[package]] +name = "shank_macro_impl" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4679c7294e45b98031bade3d50f5b01a562962176d76058af69e21e195919190" +dependencies = [ + "anyhow", + "proc-macro2 1.0.36", + "quote 1.0.14", + "serde", + "syn 1.0.91", +] + [[package]] name = "sharded-slab" version = "0.1.4" @@ -5716,6 +5782,7 @@ dependencies = [ "arrayref", "bincode", "borsh", + "mpl-token-metadata", "num-derive", "num-traits", "num_enum", diff --git a/stake-pool/program/Cargo.toml b/stake-pool/program/Cargo.toml index 9220479c99a..1f5744bf470 100644 --- a/stake-pool/program/Cargo.toml +++ b/stake-pool/program/Cargo.toml @@ -14,6 +14,7 @@ test-bpf = [] [dependencies] arrayref = "0.3.6" borsh = "0.9" +mpl-token-metadata = { version = "1.3.1", features = [ "no-entrypoint" ] } num-derive = "0.3" num-traits = "0.2" num_enum = "0.5.4" diff --git a/stake-pool/program/src/error.rs b/stake-pool/program/src/error.rs index 8ffa32e1f8d..908b947d0a3 100644 --- a/stake-pool/program/src/error.rs +++ b/stake-pool/program/src/error.rs @@ -132,6 +132,9 @@ pub enum StakePoolError { /// Too much SOL withdrawn from the stake pool's reserve account #[error("SolWithdrawalTooLarge")] SolWithdrawalTooLarge, + /// Provided metadata account does not match metadata account derived for pool mint + #[error("InvalidMetadataAccount")] + InvalidMetadataAccount, } impl From for ProgramError { fn from(e: StakePoolError) -> Self { diff --git a/stake-pool/program/src/instruction.rs b/stake-pool/program/src/instruction.rs index 85c81a4755d..04353fbbff9 100644 --- a/stake-pool/program/src/instruction.rs +++ b/stake-pool/program/src/instruction.rs @@ -1,6 +1,7 @@ //! Instruction types #![allow(clippy::too_many_arguments)] + use { crate::{ find_deposit_authority_program_address, find_stake_program_address, @@ -9,6 +10,7 @@ use { MAX_VALIDATORS_TO_UPDATE, }, borsh::{BorshDeserialize, BorshSchema, BorshSerialize}, + mpl_token_metadata::pda::find_metadata_account, solana_program::{ instruction::{AccountMeta, Instruction}, pubkey::Pubkey, @@ -374,6 +376,48 @@ pub enum StakePoolInstruction { /// 11. `[]` Token program id /// 12. `[s]` (Optional) Stake pool sol withdraw authority WithdrawSol(u64), + + /// Create token metadata for the stake-pool token in the + /// metaplex-token program + /// 0. `[]` Stake pool + /// 1. `[s]` Manager + /// 2. `[]` Stake pool withdraw authority + /// 3. `[]` Pool token mint account + /// 4. `[s, w]` Payer for creation of token metadata account + /// 5. `[w]` Token metadata account + /// 6. `[]` Metadata program id + /// 7. `[]` System program id + /// 8. `[]` Rent sysvar + CreateTokenMetadata { + #[allow(dead_code)] + /// Token name + name: String, + #[allow(dead_code)] + /// Token symbol e.g. stkSOL + symbol: String, + /// URI of the uploaded metadata of the spl-token + #[allow(dead_code)] + uri: String, + }, + /// Update token metadata for the stake-pool token in the + /// metaplex-token program + /// + /// 0. `[]` Stake pool + /// 1. `[s]` Manager + /// 2. `[]` Stake pool withdraw authority + /// 3. `[w]` Token metadata account + /// 4. `[]` Metadata program id + UpdateTokenMetadata { + #[allow(dead_code)] + /// Token name + name: String, + #[allow(dead_code)] + /// Token symbol e.g. stkSOL + symbol: String, + /// URI of the uploaded metadata of the spl-token + #[allow(dead_code)] + uri: String, + }, } /// Creates an 'initialize' instruction. @@ -1276,3 +1320,72 @@ pub fn set_funding_authority( .unwrap(), } } + +/// Creates an instruction to update metadata in the mpl token metadata program account for +/// the pool token +pub fn update_token_metadata( + program_id: &Pubkey, + stake_pool: &Pubkey, + manager: &Pubkey, + pool_mint: &Pubkey, + name: String, + symbol: String, + uri: String, +) -> Instruction { + let (stake_pool_withdraw_authority, _) = + find_withdraw_authority_program_address(program_id, stake_pool); + let (token_metadata, _) = find_metadata_account(pool_mint); + + let accounts = vec![ + AccountMeta::new_readonly(*stake_pool, false), + AccountMeta::new_readonly(*manager, true), + AccountMeta::new_readonly(stake_pool_withdraw_authority, false), + AccountMeta::new(token_metadata, false), + AccountMeta::new_readonly(mpl_token_metadata::id(), false), + ]; + + Instruction { + program_id: *program_id, + accounts, + data: StakePoolInstruction::UpdateTokenMetadata { name, symbol, uri } + .try_to_vec() + .unwrap(), + } +} + +/// Creates an instruction to create metadata using the mpl token metadata program for +/// the pool token +pub fn create_token_metadata( + program_id: &Pubkey, + stake_pool: &Pubkey, + manager: &Pubkey, + pool_mint: &Pubkey, + payer: &Pubkey, + name: String, + symbol: String, + uri: String, +) -> Instruction { + let (stake_pool_withdraw_authority, _) = + find_withdraw_authority_program_address(program_id, stake_pool); + let (token_metadata, _) = find_metadata_account(pool_mint); + + let accounts = vec![ + AccountMeta::new_readonly(*stake_pool, false), + AccountMeta::new_readonly(*manager, true), + AccountMeta::new_readonly(stake_pool_withdraw_authority, false), + AccountMeta::new_readonly(*pool_mint, false), + AccountMeta::new(*payer, true), + AccountMeta::new(token_metadata, false), + AccountMeta::new_readonly(mpl_token_metadata::id(), false), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(sysvar::rent::id(), false), + ]; + + Instruction { + program_id: *program_id, + accounts, + data: StakePoolInstruction::CreateTokenMetadata { name, symbol, uri } + .try_to_vec() + .unwrap(), + } +} diff --git a/stake-pool/program/src/processor.rs b/stake-pool/program/src/processor.rs index 2b194e2b16f..d26c4efbae5 100644 --- a/stake-pool/program/src/processor.rs +++ b/stake-pool/program/src/processor.rs @@ -13,6 +13,11 @@ use { AUTHORITY_DEPOSIT, AUTHORITY_WITHDRAW, MINIMUM_ACTIVE_STAKE, TRANSIENT_STAKE_SEED_PREFIX, }, borsh::{BorshDeserialize, BorshSerialize}, + mpl_token_metadata::{ + instruction::{create_metadata_accounts_v3, update_metadata_accounts_v2}, + pda::find_metadata_account, + state::DataV2, + }, num_traits::FromPrimitive, solana_program::{ account_info::{next_account_info, AccountInfo}, @@ -89,6 +94,19 @@ fn check_transient_stake_address( } } +/// Check mpl metadata account address for the pool mint +fn check_mpl_metadata_account_address( + metadata_address: &Pubkey, + pool_mint: &Pubkey, +) -> Result<(), ProgramError> { + let (metadata_account_pubkey, _) = find_metadata_account(pool_mint); + if metadata_account_pubkey != *metadata_address { + Err(StakePoolError::InvalidMetadataAccount.into()) + } else { + Ok(()) + } +} + /// Check system program address fn check_system_program(program_id: &Pubkey) -> Result<(), ProgramError> { if *program_id != system_program::id() { @@ -117,6 +135,34 @@ fn check_stake_program(program_id: &Pubkey) -> Result<(), ProgramError> { } } +/// Check mpl metadata program +fn check_mpl_metadata_program(program_id: &Pubkey) -> Result<(), ProgramError> { + if *program_id != mpl_token_metadata::id() { + msg!( + "Expected mpl metadata program {}, received {}", + mpl_token_metadata::id(), + program_id + ); + Err(ProgramError::IncorrectProgramId) + } else { + Ok(()) + } +} + +/// Check rent sysvar correctness +fn check_rent_sysvar(sysvar_key: &Pubkey) -> Result<(), ProgramError> { + if *sysvar_key != solana_program::sysvar::rent::id() { + msg!( + "Expected rent sysvar {}, received {}", + solana_program::sysvar::rent::id(), + sysvar_key + ); + Err(ProgramError::InvalidArgument) + } else { + Ok(()) + } +} + /// Check account owner is the given program fn check_account_owner( account_info: &AccountInfo, @@ -2676,6 +2722,175 @@ impl Processor { Ok(()) } + #[inline(never)] + fn process_create_pool_token_metadata( + program_id: &Pubkey, + accounts: &[AccountInfo], + name: String, + symbol: String, + uri: String, + ) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let stake_pool_info = next_account_info(account_info_iter)?; + let manager_info = next_account_info(account_info_iter)?; + let withdraw_authority_info = next_account_info(account_info_iter)?; + let pool_mint_info = next_account_info(account_info_iter)?; + let payer_info = next_account_info(account_info_iter)?; + let metadata_info = next_account_info(account_info_iter)?; + let mpl_token_metadata_program_info = next_account_info(account_info_iter)?; + let system_program_info = next_account_info(account_info_iter)?; + let rent_sysvar_info = next_account_info(account_info_iter)?; + + if !payer_info.is_signer { + msg!("Payer did not sign metadata creation"); + return Err(StakePoolError::SignatureMissing.into()); + } + + check_system_program(system_program_info.key)?; + check_rent_sysvar(rent_sysvar_info.key)?; + check_account_owner(payer_info, &system_program::id())?; + check_account_owner(stake_pool_info, program_id)?; + check_mpl_metadata_program(mpl_token_metadata_program_info.key)?; + + let stake_pool = try_from_slice_unchecked::(&stake_pool_info.data.borrow())?; + if !stake_pool.is_valid() { + return Err(StakePoolError::InvalidState.into()); + } + + stake_pool.check_manager(manager_info)?; + stake_pool.check_authority_withdraw( + withdraw_authority_info.key, + program_id, + stake_pool_info.key, + )?; + stake_pool.check_mint(pool_mint_info)?; + check_mpl_metadata_account_address(metadata_info.key, &stake_pool.pool_mint)?; + + // Token mint authority for stake-pool token is stake-pool withdraw authority + let token_mint_authority = withdraw_authority_info; + + let new_metadata_instruction = create_metadata_accounts_v3( + *mpl_token_metadata_program_info.key, + *metadata_info.key, + *pool_mint_info.key, + *token_mint_authority.key, + *payer_info.key, + *token_mint_authority.key, + name, + symbol, + uri, + None, + 0, + true, + true, + None, + None, + None, + ); + + let (_, stake_withdraw_bump_seed) = + crate::find_withdraw_authority_program_address(program_id, stake_pool_info.key); + + let token_mint_authority_signer_seeds: &[&[_]] = &[ + &stake_pool_info.key.to_bytes()[..32], + AUTHORITY_WITHDRAW, + &[stake_withdraw_bump_seed], + ]; + + invoke_signed( + &new_metadata_instruction, + &[ + metadata_info.clone(), + pool_mint_info.clone(), + withdraw_authority_info.clone(), + payer_info.clone(), + withdraw_authority_info.clone(), + system_program_info.clone(), + rent_sysvar_info.clone(), + mpl_token_metadata_program_info.clone(), + ], + &[token_mint_authority_signer_seeds], + )?; + + Ok(()) + } + + #[inline(never)] + fn process_update_pool_token_metadata( + program_id: &Pubkey, + accounts: &[AccountInfo], + name: String, + symbol: String, + uri: String, + ) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + + let stake_pool_info = next_account_info(account_info_iter)?; + let manager_info = next_account_info(account_info_iter)?; + let withdraw_authority_info = next_account_info(account_info_iter)?; + let metadata_info = next_account_info(account_info_iter)?; + let mpl_token_metadata_program_info = next_account_info(account_info_iter)?; + + check_account_owner(stake_pool_info, program_id)?; + + check_mpl_metadata_program(mpl_token_metadata_program_info.key)?; + + let stake_pool = try_from_slice_unchecked::(&stake_pool_info.data.borrow())?; + if !stake_pool.is_valid() { + return Err(StakePoolError::InvalidState.into()); + } + + stake_pool.check_manager(manager_info)?; + stake_pool.check_authority_withdraw( + withdraw_authority_info.key, + program_id, + stake_pool_info.key, + )?; + check_mpl_metadata_account_address(metadata_info.key, &stake_pool.pool_mint)?; + + // Token mint authority for stake-pool token is withdraw authority only + let token_mint_authority = withdraw_authority_info; + + let update_metadata_accounts_instruction = update_metadata_accounts_v2( + *mpl_token_metadata_program_info.key, + *metadata_info.key, + *token_mint_authority.key, + None, + Some(DataV2 { + name, + symbol, + uri, + seller_fee_basis_points: 0, + creators: None, + collection: None, + uses: None, + }), + None, + Some(true), + ); + + let (_, stake_withdraw_bump_seed) = + crate::find_withdraw_authority_program_address(program_id, stake_pool_info.key); + + let token_mint_authority_signer_seeds: &[&[_]] = &[ + &stake_pool_info.key.to_bytes()[..32], + AUTHORITY_WITHDRAW, + &[stake_withdraw_bump_seed], + ]; + + invoke_signed( + &update_metadata_accounts_instruction, + &[ + metadata_info.clone(), + withdraw_authority_info.clone(), + mpl_token_metadata_program_info.clone(), + ], + &[token_mint_authority_signer_seeds], + )?; + + Ok(()) + } + /// Processes [SetManager](enum.Instruction.html). #[inline(never)] // needed to avoid stack size violation fn process_set_manager(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { @@ -2916,6 +3131,14 @@ impl Processor { msg!("Instruction: WithdrawSol"); Self::process_withdraw_sol(program_id, accounts, pool_tokens) } + StakePoolInstruction::CreateTokenMetadata { name, symbol, uri } => { + msg!("Instruction: CreateTokenMetadata"); + Self::process_create_pool_token_metadata(program_id, accounts, name, symbol, uri) + } + StakePoolInstruction::UpdateTokenMetadata { name, symbol, uri } => { + msg!("Instruction: UpdateTokenMetadata"); + Self::process_update_pool_token_metadata(program_id, accounts, name, symbol, uri) + } } } } @@ -2964,6 +3187,7 @@ impl PrintProgramError for StakePoolError { StakePoolError::TransientAccountInUse => msg!("Error: Provided validator stake account already has a transient stake account in use"), StakePoolError::InvalidSolWithdrawAuthority => msg!("Error: Provided sol withdraw authority does not match the program's"), StakePoolError::SolWithdrawalTooLarge => msg!("Error: Too much SOL withdrawn from the stake pool's reserve account"), + StakePoolError::InvalidMetadataAccount => msg!("Error: Metadata account derived from pool mint account does not match the one passed to program") } } } diff --git a/stake-pool/program/tests/create_pool_token_metadata.rs b/stake-pool/program/tests/create_pool_token_metadata.rs new file mode 100644 index 00000000000..3068df62524 --- /dev/null +++ b/stake-pool/program/tests/create_pool_token_metadata.rs @@ -0,0 +1,276 @@ +#![cfg(feature = "test-bpf")] +mod helpers; + +use { + helpers::*, + mpl_token_metadata::{ + state::{MAX_NAME_LENGTH, MAX_SYMBOL_LENGTH, MAX_URI_LENGTH}, + utils::puffed_out_string, + }, + solana_program::{instruction::InstructionError, pubkey::Pubkey}, + solana_program_test::*, + solana_sdk::{ + signature::{Keypair, Signer}, + transaction::{Transaction, TransactionError}, + }, + spl_stake_pool::{ + error::StakePoolError::{AlreadyInUse, SignatureMissing, WrongManager}, + instruction, MINIMUM_RESERVE_LAMPORTS, + }, +}; + +async fn setup() -> (ProgramTestContext, StakePoolAccounts) { + let mut context = program_test_with_metadata_program() + .start_with_context() + .await; + let stake_pool_accounts = StakePoolAccounts::new(); + stake_pool_accounts + .initialize_stake_pool( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + MINIMUM_RESERVE_LAMPORTS, + ) + .await + .unwrap(); + + (context, stake_pool_accounts) +} + +#[tokio::test] +async fn success_create_pool_token_metadata() { + let (mut context, stake_pool_accounts) = setup().await; + + let name = "test_name"; + let symbol = "SYM"; + let uri = "test_uri"; + + let puffed_name = puffed_out_string(&name, MAX_NAME_LENGTH); + let puffed_symbol = puffed_out_string(&symbol, MAX_SYMBOL_LENGTH); + let puffed_uri = puffed_out_string(&uri, MAX_URI_LENGTH); + + let ix = instruction::create_token_metadata( + &spl_stake_pool::id(), + &stake_pool_accounts.stake_pool.pubkey(), + &stake_pool_accounts.manager.pubkey(), + &stake_pool_accounts.pool_mint.pubkey(), + &context.payer.pubkey(), + name.to_string(), + symbol.to_string(), + uri.to_string(), + ); + + let transaction = Transaction::new_signed_with_payer( + &[ix], + Some(&context.payer.pubkey()), + &[&context.payer, &stake_pool_accounts.manager], + context.last_blockhash, + ); + + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + let metadata = get_metadata_account( + &mut context.banks_client, + &stake_pool_accounts.pool_mint.pubkey(), + ) + .await; + + assert_eq!(metadata.data.name.to_string(), puffed_name); + assert_eq!(metadata.data.symbol.to_string(), puffed_symbol); + assert_eq!(metadata.data.uri.to_string(), puffed_uri); +} + +#[tokio::test] +async fn fail_manager_did_not_sign() { + let (mut context, stake_pool_accounts) = setup().await; + + let name = "test_name"; + let symbol = "SYM"; + let uri = "test_uri"; + + let mut ix = instruction::create_token_metadata( + &spl_stake_pool::id(), + &stake_pool_accounts.stake_pool.pubkey(), + &stake_pool_accounts.manager.pubkey(), + &stake_pool_accounts.pool_mint.pubkey(), + &context.payer.pubkey(), + name.to_string(), + symbol.to_string(), + uri.to_string(), + ); + ix.accounts[1].is_signer = false; + + let transaction = Transaction::new_signed_with_payer( + &[ix], + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + + let error = context + .banks_client + .process_transaction(transaction) + .await + .err() + .unwrap() + .unwrap(); + + match error { + TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { + let program_error = SignatureMissing as u32; + assert_eq!(error_index, program_error); + } + _ => panic!("Wrong error occurs while manager signature missing"), + } +} + +#[tokio::test] +async fn fail_wrong_manager_signed() { + let (mut context, stake_pool_accounts) = setup().await; + + let name = "test_name"; + let symbol = "SYM"; + let uri = "test_uri"; + + let random_keypair = Keypair::new(); + let ix = instruction::create_token_metadata( + &spl_stake_pool::id(), + &stake_pool_accounts.stake_pool.pubkey(), + &random_keypair.pubkey(), + &stake_pool_accounts.pool_mint.pubkey(), + &context.payer.pubkey(), + name.to_string(), + symbol.to_string(), + uri.to_string(), + ); + + let transaction = Transaction::new_signed_with_payer( + &[ix], + Some(&context.payer.pubkey()), + &[&context.payer, &random_keypair], + context.last_blockhash, + ); + + let error = context + .banks_client + .process_transaction(transaction) + .await + .err() + .unwrap() + .unwrap(); + + match error { + TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { + let program_error = WrongManager as u32; + assert_eq!(error_index, program_error); + } + _ => panic!("Wrong error occurs while signing with the wrong manager"), + } +} + +#[tokio::test] +async fn fail_wrong_mpl_metadata_program() { + let (mut context, stake_pool_accounts) = setup().await; + + let name = "test_name"; + let symbol = "SYM"; + let uri = "test_uri"; + + let random_keypair = Keypair::new(); + let mut ix = instruction::create_token_metadata( + &spl_stake_pool::id(), + &stake_pool_accounts.stake_pool.pubkey(), + &random_keypair.pubkey(), + &stake_pool_accounts.pool_mint.pubkey(), + &context.payer.pubkey(), + name.to_string(), + symbol.to_string(), + uri.to_string(), + ); + ix.accounts[7].pubkey = Pubkey::new_unique(); + + let transaction = Transaction::new_signed_with_payer( + &[ix], + Some(&context.payer.pubkey()), + &[&context.payer, &random_keypair], + context.last_blockhash, + ); + + let error = context + .banks_client + .process_transaction(transaction) + .await + .err() + .unwrap() + .unwrap(); + + match error { + TransactionError::InstructionError(_, error) => { + assert_eq!(error, InstructionError::IncorrectProgramId); + } + _ => panic!( + "Wrong error occurs while try to create metadata with wrong mpl token metadata program ID" + ), + } +} + +#[tokio::test] +async fn fail_create_metadata_twice() { + let (mut context, stake_pool_accounts) = setup().await; + + let name = "test_name"; + let symbol = "SYM"; + let uri = "test_uri"; + + let ix = instruction::create_token_metadata( + &spl_stake_pool::id(), + &stake_pool_accounts.stake_pool.pubkey(), + &stake_pool_accounts.manager.pubkey(), + &stake_pool_accounts.pool_mint.pubkey(), + &context.payer.pubkey(), + name.to_string(), + symbol.to_string(), + uri.to_string(), + ); + + let transaction = Transaction::new_signed_with_payer( + &[ix.clone()], + Some(&context.payer.pubkey()), + &[&context.payer, &stake_pool_accounts.manager], + context.last_blockhash, + ); + + let latest_blockhash = context.banks_client.get_latest_blockhash().await.unwrap(); + let transaction_2 = Transaction::new_signed_with_payer( + &[ix], + Some(&context.payer.pubkey()), + &[&context.payer, &stake_pool_accounts.manager], + latest_blockhash, + ); + + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + let error = context + .banks_client + .process_transaction(transaction_2) + .await + .err() + .unwrap() + .unwrap(); + + match error { + TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { + let program_error = AlreadyInUse as u32; + assert_eq!(error_index, program_error); + } + _ => panic!("Wrong error occurs while trying to create pool token metadata twice"), + } +} diff --git a/stake-pool/program/tests/fixtures/mpl_token_metadata.so b/stake-pool/program/tests/fixtures/mpl_token_metadata.so new file mode 100755 index 00000000000..f7b3c9f1793 Binary files /dev/null and b/stake-pool/program/tests/fixtures/mpl_token_metadata.so differ diff --git a/stake-pool/program/tests/helpers/mod.rs b/stake-pool/program/tests/helpers/mod.rs index d211afd9b47..346483130d0 100644 --- a/stake-pool/program/tests/helpers/mod.rs +++ b/stake-pool/program/tests/helpers/mod.rs @@ -2,6 +2,7 @@ use { borsh::BorshSerialize, + mpl_token_metadata::{pda::find_metadata_account, state::Metadata}, solana_program::{ borsh::{get_instance_packed_len, get_packed_len, try_from_slice_unchecked}, hash::Hash, @@ -42,6 +43,13 @@ pub fn program_test() -> ProgramTest { ProgramTest::new("spl_stake_pool", id(), processor!(Processor::process)) } +pub fn program_test_with_metadata_program() -> ProgramTest { + let mut program_test = ProgramTest::default(); + program_test.add_program("spl_stake_pool", id(), processor!(Processor::process)); + program_test.add_program("mpl_token_metadata", mpl_token_metadata::id(), None); + program_test +} + pub async fn get_account(banks_client: &mut BanksClient, pubkey: &Pubkey) -> Account { banks_client .get_account(*pubkey) @@ -293,6 +301,16 @@ pub async fn get_token_balance(banks_client: &mut BanksClient, token: &Pubkey) - account_info.amount } +pub async fn get_metadata_account(banks_client: &mut BanksClient, token_mint: &Pubkey) -> Metadata { + let (token_metadata, _) = find_metadata_account(token_mint); + let token_metadata_account = banks_client + .get_account(token_metadata) + .await + .unwrap() + .unwrap(); + try_from_slice_unchecked(token_metadata_account.data.as_slice()).unwrap() +} + pub async fn get_token_supply(banks_client: &mut BanksClient, mint: &Pubkey) -> u64 { let mint_account = banks_client.get_account(*mint).await.unwrap().unwrap(); let account_info = diff --git a/stake-pool/program/tests/update_pool_token_metadata.rs b/stake-pool/program/tests/update_pool_token_metadata.rs new file mode 100644 index 00000000000..fd27f806296 --- /dev/null +++ b/stake-pool/program/tests/update_pool_token_metadata.rs @@ -0,0 +1,198 @@ +#![cfg(feature = "test-bpf")] +mod helpers; + +use { + helpers::*, + mpl_token_metadata::{ + state::{MAX_NAME_LENGTH, MAX_SYMBOL_LENGTH, MAX_URI_LENGTH}, + utils::puffed_out_string, + }, + solana_program::instruction::InstructionError, + solana_program_test::*, + solana_sdk::{ + signature::{Keypair, Signer}, + transaction::{Transaction, TransactionError}, + }, + spl_stake_pool::{ + error::StakePoolError::{SignatureMissing, WrongManager}, + instruction, MINIMUM_RESERVE_LAMPORTS, + }, +}; + +async fn setup() -> (ProgramTestContext, StakePoolAccounts) { + let mut context = program_test_with_metadata_program() + .start_with_context() + .await; + let stake_pool_accounts = StakePoolAccounts::new(); + stake_pool_accounts + .initialize_stake_pool( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + MINIMUM_RESERVE_LAMPORTS, + ) + .await + .unwrap(); + + let name = "test_name"; + let symbol = "SYM"; + let uri = "test_uri"; + + let ix = instruction::create_token_metadata( + &spl_stake_pool::id(), + &stake_pool_accounts.stake_pool.pubkey(), + &stake_pool_accounts.manager.pubkey(), + &stake_pool_accounts.pool_mint.pubkey(), + &context.payer.pubkey(), + name.to_string(), + symbol.to_string(), + uri.to_string(), + ); + + let transaction = Transaction::new_signed_with_payer( + &[ix], + Some(&context.payer.pubkey()), + &[&context.payer, &stake_pool_accounts.manager], + context.last_blockhash, + ); + + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + (context, stake_pool_accounts) +} + +#[tokio::test] +async fn success_update_pool_token_metadata() { + let (mut context, stake_pool_accounts) = setup().await; + + let updated_name = "updated_name"; + let updated_symbol = "USYM"; + let updated_uri = "updated_uri"; + + let puffed_name = puffed_out_string(&updated_name, MAX_NAME_LENGTH); + let puffed_symbol = puffed_out_string(&updated_symbol, MAX_SYMBOL_LENGTH); + let puffed_uri = puffed_out_string(&updated_uri, MAX_URI_LENGTH); + + let ix = instruction::update_token_metadata( + &spl_stake_pool::id(), + &stake_pool_accounts.stake_pool.pubkey(), + &stake_pool_accounts.manager.pubkey(), + &stake_pool_accounts.pool_mint.pubkey(), + updated_name.to_string(), + updated_symbol.to_string(), + updated_uri.to_string(), + ); + + let transaction = Transaction::new_signed_with_payer( + &[ix], + Some(&context.payer.pubkey()), + &[&context.payer, &stake_pool_accounts.manager], + context.last_blockhash, + ); + + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + let metadata = get_metadata_account( + &mut context.banks_client, + &stake_pool_accounts.pool_mint.pubkey(), + ) + .await; + + assert_eq!(metadata.data.name.to_string(), puffed_name); + assert_eq!(metadata.data.symbol.to_string(), puffed_symbol); + assert_eq!(metadata.data.uri.to_string(), puffed_uri); +} + +#[tokio::test] +async fn fail_manager_did_not_sign() { + let (mut context, stake_pool_accounts) = setup().await; + + let updated_name = "updated_name"; + let updated_symbol = "USYM"; + let updated_uri = "updated_uri"; + + let mut ix = instruction::update_token_metadata( + &spl_stake_pool::id(), + &stake_pool_accounts.stake_pool.pubkey(), + &stake_pool_accounts.manager.pubkey(), + &stake_pool_accounts.pool_mint.pubkey(), + updated_name.to_string(), + updated_symbol.to_string(), + updated_uri.to_string(), + ); + ix.accounts[1].is_signer = false; + + let transaction = Transaction::new_signed_with_payer( + &[ix], + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + + let error = context + .banks_client + .process_transaction(transaction) + .await + .err() + .unwrap() + .unwrap(); + + match error { + TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { + let program_error = SignatureMissing as u32; + assert_eq!(error_index, program_error); + } + _ => panic!("Wrong error occurs while manager signature missing"), + } +} + +#[tokio::test] +async fn fail_wrong_manager_signed() { + let (mut context, stake_pool_accounts) = setup().await; + + let updated_name = "updated_name"; + let updated_symbol = "USYM"; + let updated_uri = "updated_uri"; + + let random_keypair = Keypair::new(); + let ix = instruction::update_token_metadata( + &spl_stake_pool::id(), + &stake_pool_accounts.stake_pool.pubkey(), + &random_keypair.pubkey(), + &stake_pool_accounts.pool_mint.pubkey(), + updated_name.to_string(), + updated_symbol.to_string(), + updated_uri.to_string(), + ); + + let transaction = Transaction::new_signed_with_payer( + &[ix], + Some(&context.payer.pubkey()), + &[&context.payer, &random_keypair], + context.last_blockhash, + ); + + let error = context + .banks_client + .process_transaction(transaction) + .await + .err() + .unwrap() + .unwrap(); + + match error { + TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { + let program_error = WrongManager as u32; + assert_eq!(error_index, program_error); + } + _ => panic!("Wrong error occurs while signing with the wrong manager"), + } +}