diff --git a/pinocchio/interface/src/instruction.rs b/pinocchio/interface/src/instruction.rs index 80b44c15..3bd85017 100644 --- a/pinocchio/interface/src/instruction.rs +++ b/pinocchio/interface/src/instruction.rs @@ -504,10 +504,17 @@ pub enum TokenInstruction { /// /// Accounts expected by this instruction: /// + /// * Single owner/delegate /// 0. `[writable]` The source account. /// 1. `[writable]` The destination account. /// 2. `[signer]` The source account's owner/delegate. /// + /// * Multisignature owner/delegate + /// 0. `[writable]` The source account. + /// 1. `[writable]` The destination account. + /// 2. `[]` The source account's multisignature owner/delegate. + /// 3. `..+M` `[signer]` M signer accounts. + /// /// Data expected by this instruction: /// /// - `Option` The amount of lamports to transfer. When an amount is diff --git a/pinocchio/program/src/processor/unwrap_lamports.rs b/pinocchio/program/src/processor/unwrap_lamports.rs index 163e268c..beb99b66 100644 --- a/pinocchio/program/src/processor/unwrap_lamports.rs +++ b/pinocchio/program/src/processor/unwrap_lamports.rs @@ -41,11 +41,6 @@ pub fn process_unwrap_lamports(accounts: &[AccountInfo], instruction_data: &[u8] return Err(TokenError::NonNativeNotSupported.into()); } - // SAFETY: `authority_info` is not currently borrowed; in the case - // `authority_info` is the same as `source_account_info`, then it cannot be - // a multisig. - unsafe { validate_owner(&source_account.owner, authority_info, remaining)? }; - // If we have an amount, we need to validate whether there are enough lamports // to unwrap or not; otherwise we just use the full amount. let (amount, remaining_amount) = if let Some(amount) = maybe_amount { @@ -60,6 +55,31 @@ pub fn process_unwrap_lamports(accounts: &[AccountInfo], instruction_data: &[u8] (source_account.amount(), 0) }; + // Validates the authority (delegate or owner). + + if source_account.delegate() == Some(authority_info.key()) { + // SAFETY: `authority_info` is not currently borrowed; in the case + // `authority_info` is the same as `source_account_info`, then it cannot be + // a multisig. + unsafe { validate_owner(authority_info.key(), authority_info, remaining)? }; + + let delegated_amount = source_account + .delegated_amount() + .checked_sub(amount) + .ok_or(TokenError::InsufficientFunds)?; + + source_account.set_delegated_amount(delegated_amount); + + if delegated_amount == 0 { + source_account.clear_delegate(); + } + } else { + // SAFETY: `authority_info` is not currently borrowed; in the case + // `authority_info` is the same as `source_account_info`, then it cannot be + // a multisig. + unsafe { validate_owner(&source_account.owner, authority_info, remaining)? }; + } + if unlikely(amount == 0) { // Validates the token account owner since we are not writing // to the account. diff --git a/pinocchio/program/tests/unwrap_lamports.rs b/pinocchio/program/tests/unwrap_lamports.rs index af4ab8bf..cbd1bb80 100644 --- a/pinocchio/program/tests/unwrap_lamports.rs +++ b/pinocchio/program/tests/unwrap_lamports.rs @@ -9,6 +9,7 @@ use { native_mint, state::{ account::Account as TokenAccount, account_state::AccountState, load_mut_unchecked, + multisig::Multisig, }, }, solana_account::Account, @@ -26,6 +27,7 @@ fn create_token_account( is_native: bool, amount: u64, program_owner: &Pubkey, + delegate_and_amount: Option<(&Pubkey, u64)>, ) -> Account { let space = size_of::(); let mut lamports = Rent::default().minimum_balance(space); @@ -38,6 +40,11 @@ fn create_token_account( token.set_amount(amount); token.set_native(is_native); + if let Some((delegate, delegated_amount)) = delegate_and_amount { + token.set_delegate(delegate.as_array()); + token.set_delegated_amount(delegated_amount); + } + if is_native { token.set_native_amount(lamports); lamports = lamports.saturating_add(amount); @@ -68,13 +75,18 @@ fn unwrap_lamports_instruction( destination: &Pubkey, authority: &Pubkey, amount: Option, + signers: &[&Pubkey], ) -> Result { - let accounts = vec![ + let mut accounts = vec![ AccountMeta::new(*source, false), AccountMeta::new(*destination, false), AccountMeta::new_readonly(*authority, true), ]; + for signer in signers { + accounts.push(AccountMeta::new_readonly(**signer, true)) + } + // Start with the batch discriminator let mut data: Vec = vec![TokenInstruction::UnwrapLamports as u8]; @@ -107,6 +119,7 @@ fn unwrap_lamports() { true, 2_000_000_000, &TOKEN_PROGRAM_ID, + None, ); let instruction = unwrap_lamports_instruction( @@ -114,6 +127,7 @@ fn unwrap_lamports() { &destination_account_key, &authority_key, None, + &[], ) .unwrap(); @@ -162,6 +176,7 @@ fn unwrap_lamports_with_amount() { true, 2_000_000_000, &TOKEN_PROGRAM_ID, + None, ); let instruction = unwrap_lamports_instruction( @@ -169,6 +184,7 @@ fn unwrap_lamports_with_amount() { &destination_account_key, &authority_key, Some(2_000_000_000), + &[], ) .unwrap(); @@ -217,6 +233,7 @@ fn fail_unwrap_lamports_with_insufficient_funds() { true, 1_000_000_000, &TOKEN_PROGRAM_ID, + None, ); let instruction = unwrap_lamports_instruction( @@ -224,6 +241,7 @@ fn fail_unwrap_lamports_with_insufficient_funds() { &destination_account_key, &authority_key, Some(2_000_000_000), + &[], ) .unwrap(); @@ -258,6 +276,7 @@ fn unwrap_lamports_with_parial_amount() { true, 2_000_000_000, &TOKEN_PROGRAM_ID, + None, ); let instruction = unwrap_lamports_instruction( @@ -265,6 +284,7 @@ fn unwrap_lamports_with_parial_amount() { &destination_account_key, &authority_key, Some(1_000_000_000), + &[], ) .unwrap(); @@ -316,13 +336,15 @@ fn fail_unwrap_lamports_with_invalid_authority() { true, 1_000_000_000, &TOKEN_PROGRAM_ID, + None, ); let instruction = unwrap_lamports_instruction( &source_account_key, &destination_account_key, &fake_authority_key, // <-- wrong authority - Some(2_000_000_000), + Some(1_000_000_000), + &[], ) .unwrap(); @@ -357,6 +379,7 @@ fn fail_unwrap_lamports_with_non_native_account() { false, // <-- non-native account 2_000_000_000, &TOKEN_PROGRAM_ID, + None, ); source_account.lamports += 2_000_000_000; @@ -365,6 +388,7 @@ fn fail_unwrap_lamports_with_non_native_account() { &destination_account_key, &authority_key, Some(1_000_000_000), + &[], ) .unwrap(); @@ -398,6 +422,7 @@ fn unwrap_lamports_with_self_transfer() { true, 2_000_000_000, &TOKEN_PROGRAM_ID, + None, ); let instruction = unwrap_lamports_instruction( @@ -405,6 +430,7 @@ fn unwrap_lamports_with_self_transfer() { &source_account_key, // <-- destination same as source &authority_key, Some(1_000_000_000), + &[], ) .unwrap(); @@ -452,6 +478,7 @@ fn fail_unwrap_lamports_with_invalid_native_account() { true, 2_000_000_000, &invalid_program_owner, // <-- invalid program owner + None, ); source_account.lamports += 2_000_000_000; @@ -460,6 +487,7 @@ fn fail_unwrap_lamports_with_invalid_native_account() { &destination_account_key, &authority_key, Some(1_000_000_000), + &[], ) .unwrap(); @@ -493,19 +521,27 @@ fn unwrap_lamports_to_native_account() { true, 2_000_000_000, &TOKEN_PROGRAM_ID, + None, ); // destination native account: // - amount: 0 let destination_account_key = Pubkey::new_unique(); - let destination_account = - create_token_account(&native_mint, &authority_key, true, 0, &TOKEN_PROGRAM_ID); + let destination_account = create_token_account( + &native_mint, + &authority_key, + true, + 0, + &TOKEN_PROGRAM_ID, + None, + ); let instruction = unwrap_lamports_instruction( &source_account_key, &destination_account_key, &authority_key, None, + &[], ) .unwrap(); @@ -566,6 +602,7 @@ fn unwrap_lamports_to_token_account() { true, 2_000_000_000, &TOKEN_PROGRAM_ID, + None, ); // destination non-native account: @@ -577,6 +614,7 @@ fn unwrap_lamports_to_token_account() { false, 0, &TOKEN_PROGRAM_ID, + None, ); let instruction = unwrap_lamports_instruction( @@ -584,6 +622,7 @@ fn unwrap_lamports_to_token_account() { &destination_account_key, &authority_key, None, + &[], ) .unwrap(); @@ -628,3 +667,401 @@ fn unwrap_lamports_to_token_account() { let token_account = spl_token_interface::state::Account::unpack(&account.data).unwrap(); assert_eq!(token_account.amount, 0); } + +#[test] +fn unwrap_lamports_with_delegate_and_amount() { + let native_mint = Pubkey::new_from_array(native_mint::ID); + let authority_key = Pubkey::new_unique(); + let delegate_key = Pubkey::new_unique(); + let destination_account_key = Pubkey::new_unique(); + + // native account: + // - amount: 2_000_000_000 + // - delegated_amount: 1_000_000_000 + let source_account_key = Pubkey::new_unique(); + let source_account = create_token_account( + &native_mint, + &authority_key, + true, + 2_000_000_000, + &TOKEN_PROGRAM_ID, + Some((&delegate_key, 1_000_000_000)), + ); + + let instruction = unwrap_lamports_instruction( + &source_account_key, + &destination_account_key, + &delegate_key, // <-- delegate authority + Some(500_000_000), + &[], + ) + .unwrap(); + + // It should succeed to unwrap 500_000_000 lamports. + let result = mollusk().process_and_validate_instruction( + &instruction, + &[ + (source_account_key, source_account), + (destination_account_key, Account::default()), + (delegate_key, Account::default()), + ], + &[ + Check::success(), + Check::account(&destination_account_key) + .lamports(500_000_000) + .build(), + Check::account(&source_account_key) + .lamports( + Rent::default().minimum_balance(size_of::()) + 1_500_000_000, + ) + .build(), + ], + ); + + // And the remaining amount must be 1_500_000_000 and the delegated amount + // reduced to 500_000_000. + + let account = result.get_account(&source_account_key); + assert!(account.is_some()); + + let account = account.unwrap(); + let token_account = spl_token_interface::state::Account::unpack(&account.data).unwrap(); + assert_eq!(token_account.amount, 1_500_000_000); + assert_eq!(token_account.delegate.unwrap(), delegate_key); + assert_eq!(token_account.delegated_amount, 500_000_000); +} + +#[test] +fn unwrap_lamports_with_delegate() { + let native_mint = Pubkey::new_from_array(native_mint::ID); + let authority_key = Pubkey::new_unique(); + let delegate_key = Pubkey::new_unique(); + let destination_account_key = Pubkey::new_unique(); + + // native account: + // - amount: 2_000_000_000 + // - delegated_amount: 2_000_000_000 + let source_account_key = Pubkey::new_unique(); + let source_account = create_token_account( + &native_mint, + &authority_key, + true, + 2_000_000_000, + &TOKEN_PROGRAM_ID, + Some((&delegate_key, 2_000_000_000)), + ); + + let instruction = unwrap_lamports_instruction( + &source_account_key, + &destination_account_key, + &delegate_key, // <-- delegate authority + None, // <-- unwrap full amount + &[], + ) + .unwrap(); + + // It should succeed to unwrap 2_000_000_000 lamports. + + let result = mollusk().process_and_validate_instruction( + &instruction, + &[ + (source_account_key, source_account), + (destination_account_key, Account::default()), + (delegate_key, Account::default()), + ], + &[ + Check::success(), + Check::account(&destination_account_key) + .lamports(2_000_000_000) + .build(), + Check::account(&source_account_key) + .lamports(Rent::default().minimum_balance(size_of::())) + .build(), + ], + ); + + // And the remaining amount must be 0 and the delegate cleared. + + let account = result.get_account(&source_account_key); + assert!(account.is_some()); + + let account = account.unwrap(); + let token_account = spl_token_interface::state::Account::unpack(&account.data).unwrap(); + assert_eq!(token_account.amount, 0); + assert!(token_account.delegate.is_none()); + assert_eq!(token_account.delegated_amount, 0); +} + +#[test] +fn fail_unwrap_lamports_with_delegate_and_insufficient_amount() { + let native_mint = Pubkey::new_from_array(native_mint::ID); + let authority_key = Pubkey::new_unique(); + let delegate_key = Pubkey::new_unique(); + let destination_account_key = Pubkey::new_unique(); + + // native account: + // - amount: 2_000_000_000 + // - delegated_amount: 1_000_000_000 + let source_account_key = Pubkey::new_unique(); + let source_account = create_token_account( + &native_mint, + &authority_key, + true, + 2_000_000_000, + &TOKEN_PROGRAM_ID, + Some((&delegate_key, 1_000_000_000)), + ); + + let instruction = unwrap_lamports_instruction( + &source_account_key, + &destination_account_key, + &delegate_key, // <-- delegate authority + None, + &[], + ) + .unwrap(); + + // When we try to unwrap 2_000_000_000 lamports, we expect a + // `TokenError::InsufficientFunds` error since only 1_000_000_000 + // lamports are delegated. + + mollusk().process_and_validate_instruction( + &instruction, + &[ + (source_account_key, source_account), + (destination_account_key, Account::default()), + (delegate_key, Account::default()), + ], + &[Check::err(ProgramError::Custom( + TokenError::InsufficientFunds as u32, + ))], + ); +} + +#[test] +fn fail_unwrap_lamports_with_wrong_delegate() { + let native_mint = Pubkey::new_from_array(native_mint::ID); + let authority_key = Pubkey::new_unique(); + let delegate_key = Pubkey::new_unique(); + let destination_account_key = Pubkey::new_unique(); + + // native account: + // - amount: 2_000_000_000 + // - delegated_amount: 1_000_000_000 + let source_account_key = Pubkey::new_unique(); + let source_account = create_token_account( + &native_mint, + &authority_key, + true, + 2_000_000_000, + &TOKEN_PROGRAM_ID, + Some((&delegate_key, 1_000_000_000)), + ); + + let fake_delegate_key = Pubkey::new_unique(); + + let instruction = unwrap_lamports_instruction( + &source_account_key, + &destination_account_key, + &fake_delegate_key, // <-- fake delegate authority + None, + &[], + ) + .unwrap(); + + // When we try to unwrap lamports with an invalid delegate, we expect + // a `TokenError::OwnerMismatch` error. + + mollusk().process_and_validate_instruction( + &instruction, + &[ + (source_account_key, source_account), + (destination_account_key, Account::default()), + (fake_delegate_key, Account::default()), + ], + &[Check::err(ProgramError::Custom( + TokenError::OwnerMismatch as u32, + ))], + ); +} + +#[test] +fn unwrap_lamports_from_multisig() { + let native_mint = Pubkey::new_from_array(native_mint::ID); + let destination_account_key = Pubkey::new_unique(); + let molusk_svm = mollusk(); + + // Given a multisig account. + + let multisig_key = Pubkey::new_unique(); + let signer1_key = Pubkey::new_unique(); + let signer2_key = Pubkey::new_unique(); + let signer3_key = Pubkey::new_unique(); + + let initialize_multisig_ix = spl_token_interface::instruction::initialize_multisig( + &spl_token_interface::ID, + &multisig_key, + &[&signer1_key, &signer2_key, &signer3_key], + 3, + ) + .unwrap(); + + let result = molusk_svm.process_and_validate_instruction( + &initialize_multisig_ix, + &[ + ( + multisig_key, + Account { + lamports: Rent::default().minimum_balance(size_of::()), + data: vec![0u8; size_of::()], + owner: TOKEN_PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + ), + molusk_svm.sysvars.keyed_account_for_rent_sysvar(), + (signer1_key, Account::default()), + (signer2_key, Account::default()), + (signer3_key, Account::default()), + ], + &[Check::success()], + ); + + let multisig = result.get_account(&multisig_key).unwrap(); + + // native account: + // - amount: 2_000_000_000 + // - owner: multisig + let source_account_key = Pubkey::new_unique(); + let source_account = create_token_account( + &native_mint, + &multisig_key, + true, + 2_000_000_000, + &TOKEN_PROGRAM_ID, + None, + ); + + let instruction = unwrap_lamports_instruction( + &source_account_key, + &destination_account_key, + &multisig_key, + None, + &[&signer1_key, &signer2_key, &signer3_key], + ) + .unwrap(); + + // It should succeed to unwrap 2_000_000_000 lamports. + + let result = molusk_svm.process_and_validate_instruction( + &instruction, + &[ + (source_account_key, source_account), + (destination_account_key, Account::default()), + (multisig_key, multisig.clone()), + (signer1_key, Account::default()), + (signer2_key, Account::default()), + (signer3_key, Account::default()), + ], + &[ + Check::success(), + Check::account(&destination_account_key) + .lamports(2_000_000_000) + .build(), + Check::account(&source_account_key) + .lamports(Rent::default().minimum_balance(size_of::())) + .build(), + ], + ); + + // And the remaining amount must be 0. + + let account = result.get_account(&source_account_key); + assert!(account.is_some()); + + let account = account.unwrap(); + let token_account = spl_token_interface::state::Account::unpack(&account.data).unwrap(); + assert_eq!(token_account.amount, 0); +} + +#[test] +fn fail_unwrap_lamports_from_multisig_with_missing_signer() { + let native_mint = Pubkey::new_from_array(native_mint::ID); + let destination_account_key = Pubkey::new_unique(); + let molusk_svm = mollusk(); + + // Given a multisig account. + + let multisig_key = Pubkey::new_unique(); + let signer1_key = Pubkey::new_unique(); + let signer2_key = Pubkey::new_unique(); + let signer3_key = Pubkey::new_unique(); + + let initialize_multisig_ix = spl_token_interface::instruction::initialize_multisig( + &spl_token_interface::ID, + &multisig_key, + &[&signer1_key, &signer2_key, &signer3_key], + 3, + ) + .unwrap(); + + let result = molusk_svm.process_and_validate_instruction( + &initialize_multisig_ix, + &[ + ( + multisig_key, + Account { + lamports: Rent::default().minimum_balance(size_of::()), + data: vec![0u8; size_of::()], + owner: TOKEN_PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + ), + molusk_svm.sysvars.keyed_account_for_rent_sysvar(), + (signer1_key, Account::default()), + (signer2_key, Account::default()), + (signer3_key, Account::default()), + ], + &[Check::success()], + ); + + let multisig = result.get_account(&multisig_key).unwrap(); + + // native account: + // - amount: 2_000_000_000 + // - owner: multisig + let source_account_key = Pubkey::new_unique(); + let source_account = create_token_account( + &native_mint, + &multisig_key, + true, + 2_000_000_000, + &TOKEN_PROGRAM_ID, + None, + ); + + let instruction = unwrap_lamports_instruction( + &source_account_key, + &destination_account_key, + &multisig_key, + None, + &[&signer1_key, &signer2_key], // <-- missing signer3 + ) + .unwrap(); + + // When we try to unwrap lamports with a missing signer, we expect a + // `InstructionError::MissingRequiredSignature` error. + + molusk_svm.process_and_validate_instruction( + &instruction, + &[ + (source_account_key, source_account), + (destination_account_key, Account::default()), + (multisig_key, multisig.clone()), + (signer1_key, Account::default()), + (signer2_key, Account::default()), + ], + &[Check::err(ProgramError::MissingRequiredSignature)], + ); +}