diff --git a/src/discriminator.rs b/src/discriminator.rs index 224e4163..e2025aa1 100644 --- a/src/discriminator.rs +++ b/src/discriminator.rs @@ -43,6 +43,8 @@ pub enum DlpDiscriminator { UndelegateConfinedAccount = 18, /// See [crate::processor::process_delegate_with_any_validator] for docs. DelegateWithAnyValidator = 19, + /// See [crate::processor::process_call_handler_v2] for docs. + CallHandlerV2 = 20, } impl DlpDiscriminator { diff --git a/src/error.rs b/src/error.rs index 4b4e6027..72361dd0 100644 --- a/src/error.rs +++ b/src/error.rs @@ -2,6 +2,9 @@ use num_enum::IntoPrimitive; use solana_program::program_error::ProgramError; use thiserror::Error; +pub const INVALID_ESCROW_PDA: &str = "invalid escrow pda in CallHandler"; +pub const INVALID_ESCROW_OWNER: &str = "escrow can not be delegated in CallHandler"; + #[derive(Debug, Error, Clone, Copy, PartialEq, Eq, IntoPrimitive)] #[repr(u32)] pub enum DlpError { diff --git a/src/instruction_builder/call_handler.rs b/src/instruction_builder/call_handler.rs index 81cffd1c..cf6eb336 100644 --- a/src/instruction_builder/call_handler.rs +++ b/src/instruction_builder/call_handler.rs @@ -8,6 +8,7 @@ use solana_program::{instruction::AccountMeta, pubkey::Pubkey}; /// Builds a call handler instruction. /// See [crate::processor::call_handler] for docs. +#[deprecated(since = "1.1.4", note = "Use `call_handler_v2` instead")] pub fn call_handler( validator: Pubkey, destination_program: Pubkey, diff --git a/src/instruction_builder/call_handler_v2.rs b/src/instruction_builder/call_handler_v2.rs new file mode 100644 index 00000000..e11e112e --- /dev/null +++ b/src/instruction_builder/call_handler_v2.rs @@ -0,0 +1,64 @@ +use crate::args::CallHandlerArgs; +use crate::discriminator::DlpDiscriminator; +use crate::pda::{ephemeral_balance_pda_from_payer, validator_fees_vault_pda_from_validator}; +use crate::{total_size_budget, AccountSizeClass, DLP_PROGRAM_DATA_SIZE_CLASS}; +use borsh::to_vec; +use solana_program::instruction::Instruction; +use solana_program::{instruction::AccountMeta, pubkey::Pubkey}; + +/// Builds a call handler v2 instruction. +/// See [crate::processor::call_handler_v2] for docs. +pub fn call_handler_v2( + validator: Pubkey, + destination_program: Pubkey, + source_program: Pubkey, + escrow_authority: Pubkey, + other_accounts: Vec, + args: CallHandlerArgs, +) -> Instruction { + let validator_fees_vault_pda = validator_fees_vault_pda_from_validator(&validator); + + // handler accounts + let escrow_account = ephemeral_balance_pda_from_payer(&escrow_authority, args.escrow_index); + let mut accounts = vec![ + AccountMeta::new(validator, true), + AccountMeta::new(validator_fees_vault_pda, false), + AccountMeta::new_readonly(destination_program, false), + AccountMeta::new_readonly(source_program, false), + AccountMeta::new(escrow_authority, false), + AccountMeta::new(escrow_account, false), + ]; + // append other accounts at the end + accounts.extend(other_accounts); + + Instruction { + program_id: crate::id(), + accounts, + data: [ + DlpDiscriminator::CallHandlerV2.to_vec(), + to_vec(&args).unwrap(), + ] + .concat(), + } +} + +/// +/// Returns accounts-data-size budget for call_handler_v2 instruction. +/// +/// This value can be used with ComputeBudgetInstruction::SetLoadedAccountsDataSizeLimit +/// +pub fn call_handler_v2_size_budget( + destination_program: AccountSizeClass, + source_program: AccountSizeClass, + other_accounts: u32, +) -> u32 { + total_size_budget(&[ + DLP_PROGRAM_DATA_SIZE_CLASS, + AccountSizeClass::Tiny, // validator + AccountSizeClass::Tiny, // validator_fees_vault_pda + destination_program, + source_program, + AccountSizeClass::Tiny, // escrow_authority + AccountSizeClass::Tiny, // escrow_account + ]) + other_accounts +} diff --git a/src/instruction_builder/mod.rs b/src/instruction_builder/mod.rs index 2ef70507..5e9025c6 100644 --- a/src/instruction_builder/mod.rs +++ b/src/instruction_builder/mod.rs @@ -1,4 +1,5 @@ mod call_handler; +mod call_handler_v2; mod close_ephemeral_balance; mod close_validator_fees_vault; mod commit_diff; @@ -18,6 +19,7 @@ mod validator_claim_fees; mod whitelist_validator_for_program; pub use call_handler::*; +pub use call_handler_v2::*; pub use close_ephemeral_balance::*; pub use close_validator_fees_vault::*; pub use commit_diff::*; diff --git a/src/lib.rs b/src/lib.rs index 50d54afe..7dd7d8de 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -173,6 +173,9 @@ pub fn slow_process_instruction( DlpDiscriminator::CallHandler => { processor::process_call_handler(program_id, accounts, data)? } + DlpDiscriminator::CallHandlerV2 => { + processor::process_call_handler_v2(program_id, accounts, data)? + } _ => { #[cfg(feature = "logging")] msg!("PANIC: Instruction must be processed by fast_process_instruction"); diff --git a/src/processor/call_handler.rs b/src/processor/call_handler.rs index 5ddd66de..8c21a829 100644 --- a/src/processor/call_handler.rs +++ b/src/processor/call_handler.rs @@ -1,5 +1,6 @@ use crate::args::CallHandlerArgs; use crate::ephemeral_balance_seeds_from_payer; +use crate::error::{INVALID_ESCROW_OWNER, INVALID_ESCROW_PDA}; use crate::processor::utils::loaders::{ load_initialized_validator_fees_vault, load_owned_pda, load_pda, load_signer, }; @@ -11,10 +12,7 @@ use solana_program::instruction::{AccountMeta, Instruction}; use solana_program::program::invoke_signed; use solana_program::program_error::ProgramError; use solana_program::pubkey::Pubkey; -use solana_program::{msg, system_program}; - -pub const INVALID_ESCROW_PDA: &str = "invalid escrow pda in CallHandler"; -pub const INVALID_ESCROW_OWNER: &str = "escrow can not be delegated in CallHandler"; +use solana_program::system_program; /// Calls a handler on user specified program /// @@ -50,10 +48,6 @@ pub fn process_call_handler( ) -> ProgramResult { const OTHER_ACCOUNTS_OFFSET: usize = 5; - if accounts.len() < OTHER_ACCOUNTS_OFFSET { - return Err(ProgramError::NotEnoughAccountKeys); - } - let ( [validator, validator_fees_vault, destination_program, escrow_authority_account, escrow_account], other_accounts, @@ -68,14 +62,6 @@ pub fn process_call_handler( load_signer(validator, "validator")?; // verify signer is a registered validator load_initialized_validator_fees_vault(validator, validator_fees_vault, true)?; - // Check if destination program is executable - if !destination_program.executable { - msg!( - "{} program is not executable: destination program", - destination_program.key - ); - return Err(ProgramError::InvalidAccountData); - } // verify passed escrow_account derived from escrow authority let escrow_seeds: &[&[u8]] = diff --git a/src/processor/call_handler_v2.rs b/src/processor/call_handler_v2.rs new file mode 100644 index 00000000..4c6cac24 --- /dev/null +++ b/src/processor/call_handler_v2.rs @@ -0,0 +1,110 @@ +use crate::args::CallHandlerArgs; +use crate::ephemeral_balance_seeds_from_payer; +use crate::error::{INVALID_ESCROW_OWNER, INVALID_ESCROW_PDA}; +use crate::processor::utils::loaders::{ + load_initialized_validator_fees_vault, load_owned_pda, load_pda, load_signer, +}; + +use borsh::BorshDeserialize; +use solana_program::account_info::AccountInfo; +use solana_program::entrypoint::ProgramResult; +use solana_program::instruction::{AccountMeta, Instruction}; +use solana_program::program::invoke_signed; +use solana_program::program_error::ProgramError; +use solana_program::pubkey::Pubkey; +use solana_program::system_program; + +/// Calls a handler on user specified program +/// +/// Accounts: +/// 0: `[signer]` validator +/// 1: `[]` validator fee vault to verify its registration +/// 2: `[]` destination program of an action +/// 3: `[]` source program of an action +/// 4: `[]` escrow authority account which created escrow account +/// 5: `[writable]` non delegated escrow PDA created from escrow_authority (index 4) +/// 6: `[readonly/writable]` other accounts needed for action +/// 7: `[readonly/writable]` other accounts needed for action +/// 8: ... +/// +/// Requirements: +/// +/// - escrow account initialized +/// - escrow account not delegated +/// - validator as a caller +/// +/// Steps: +/// 1. Verify that signer is a valid registered validator +/// 2. Verify escrow pda exists and not delegated +/// 3. Invoke signed on behalf of escrow pda user specified action +/// +/// Usage: +/// +/// This instruction is meant to be called via CPI with the owning program signing for the +/// delegated account. +pub fn process_call_handler_v2( + _program_id: &Pubkey, + accounts: &[AccountInfo], + data: &[u8], +) -> ProgramResult { + const OTHER_ACCOUNTS_OFFSET: usize = 6; + + let ( + [validator, validator_fees_vault, destination_program, source_program, escrow_authority_account, escrow_account], + other_accounts, + ) = accounts.split_at(OTHER_ACCOUNTS_OFFSET) + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + let args = CallHandlerArgs::try_from_slice(data)?; + + // verify account is a signer + load_signer(validator, "validator")?; + // verify signer is a registered validator + load_initialized_validator_fees_vault(validator, validator_fees_vault, true)?; + + // verify passed escrow_account derived from escrow authority + let escrow_seeds: &[&[u8]] = + ephemeral_balance_seeds_from_payer!(escrow_authority_account.key, args.escrow_index); + let escrow_bump = load_pda( + escrow_account, + escrow_seeds, + &crate::id(), + true, + INVALID_ESCROW_PDA, + )?; + load_owned_pda(escrow_account, &system_program::id(), INVALID_ESCROW_OWNER)?; + + // deduce necessary accounts for CPI + let (accounts_meta, handler_accounts): (Vec, Vec) = other_accounts + .iter() + .chain([source_program, escrow_authority_account, escrow_account]) + .filter(|account| account.key != validator.key) + .map(|account| { + ( + // We enable only escrow to be a signer + AccountMeta { + pubkey: *account.key, + is_writable: account.is_writable, + is_signer: account.key == escrow_account.key, + }, + account.clone(), + ) + }) + .collect(); + + let handler_instruction = Instruction { + program_id: *destination_program.key, + data: args.data, + accounts: accounts_meta, + }; + let bump_slice = &[escrow_bump]; + let escrow_signer_seeds = [escrow_seeds, &[bump_slice]].concat(); + + invoke_signed( + &handler_instruction, + &handler_accounts, + &[&escrow_signer_seeds], + ) +} diff --git a/src/processor/mod.rs b/src/processor/mod.rs index 46dc4df1..08be1d70 100644 --- a/src/processor/mod.rs +++ b/src/processor/mod.rs @@ -1,4 +1,5 @@ mod call_handler; +mod call_handler_v2; mod close_ephemeral_balance; mod close_validator_fees_vault; mod delegate_ephemeral_balance; @@ -13,6 +14,7 @@ mod whitelist_validator_for_program; pub mod fast; pub use call_handler::*; +pub use call_handler_v2::*; pub use close_ephemeral_balance::*; pub use close_validator_fees_vault::*; pub use delegate_ephemeral_balance::*; diff --git a/tests/buffers/test_delegation.so b/tests/buffers/test_delegation.so index 352b3c1a..7c40a34b 100755 Binary files a/tests/buffers/test_delegation.so and b/tests/buffers/test_delegation.so differ diff --git a/tests/integration/programs/test-delegation/src/lib.rs b/tests/integration/programs/test-delegation/src/lib.rs index db02c7ba..4f2efe6e 100644 --- a/tests/integration/programs/test-delegation/src/lib.rs +++ b/tests/integration/programs/test-delegation/src/lib.rs @@ -117,6 +117,48 @@ pub mod test_delegation { Ok(()) } + + /// Delegation program call handler v2 (commit variant) + #[instruction(discriminator = [1, 0, 3, 0])] + pub fn commit_base_action_handler_v2( + ctx: Context, + amount: u64, + ) -> Result<()> { + msg!("commit_base_action_handler_v2!"); + let transfer_ctx = CpiContext::new( + ctx.accounts.system_program.to_account_info(), + Transfer { + from: ctx.accounts.escrow_account.to_account_info(), + to: ctx.accounts.destination_account.to_account_info(), + }, + ); + + transfer(transfer_ctx, amount) + } + + /// Delegation program call handler v2 (undelegate variant) + #[instruction(discriminator = [1, 0, 4, 0])] + pub fn undelegate_base_action_handler_v2( + ctx: Context, + amount: u64, + ) -> Result<()> { + msg!("undelegate_base_action_handler_v2"); + let transfer_ctx = CpiContext::new( + ctx.accounts.system_program.to_account_info(), + Transfer { + from: ctx.accounts.escrow_account.to_account_info(), + to: ctx.accounts.destination_account.to_account_info(), + }, + ); + transfer(transfer_ctx, amount)?; + + let counter_data = &mut ctx.accounts.counter.try_borrow_mut_data()?; + let mut counter = Counter::try_from_slice(&counter_data)?; + counter.count += 1; + counter_data.copy_from_slice(&counter.try_to_vec()?); + + Ok(()) + } } pub fn transfer_from_undelegated( @@ -210,6 +252,34 @@ pub struct UndelegateBaseActionHandler<'info> { pub escrow_account: Signer<'info>, } +#[derive(Accounts)] +pub struct CommitBaseActionHandlerV2<'info> { + /// CHECK: The destination account to transfer lamports to + #[account(mut)] + pub destination_account: AccountInfo<'info>, + pub system_program: Program<'info, System>, + /// CHECK: The source program passed through by call_handler_v2 + pub source_program: UncheckedAccount<'info>, + /// CHECK: The authority that owns the escrow account + pub escrow_authority: UncheckedAccount<'info>, + pub escrow_account: Signer<'info>, +} + +#[derive(Accounts)] +pub struct UndelegateBaseActionHandlerV2<'info> { + /// CHECK: The destination account to transfer lamports to + #[account(mut)] + pub destination_account: AccountInfo<'info>, + /// CHECK: fails in finalize stage due to ownership by dlp + pub counter: UncheckedAccount<'info>, + pub system_program: Program<'info, System>, + /// CHECK: The source program passed through by call_handler_v2 + pub source_program: UncheckedAccount<'info>, + /// CHECK: The authority that owns the escrow account + pub escrow_authority: UncheckedAccount<'info>, + pub escrow_account: Signer<'info>, +} + #[account] pub struct Counter { pub count: u64, diff --git a/tests/integration/tests/test-delegation.ts b/tests/integration/tests/test-delegation.ts index c27f1bfd..cf7a7e19 100644 --- a/tests/integration/tests/test-delegation.ts +++ b/tests/integration/tests/test-delegation.ts @@ -241,6 +241,21 @@ describe("TestDelegation", () => { ); }); + // Use a separate escrow authority so it doesn't collide with validator + // (the v2 processor filters out accounts matching the validator key) + const escrowAuthority = web3.Keypair.generate(); + + it("Top up ephemeral balance for call handler v2", async () => { + const ix = createTopUpEphemeralBalanceInstruction( + payer, + escrowAuthority.publicKey, + 100_000_000, // 0.1 SOL + 255 + ); + const txId = await processInstruction(ix); + console.log("Top up ephemeral balance tx:", txId); + }); + it("Finalize account state", async () => { const ix = createFinalizeInstruction(validator, pda); const txId = await processInstruction(ix); @@ -259,6 +274,47 @@ describe("TestDelegation", () => { ); }); + it("Call handler v2 after finalize", async () => { + const destination = web3.Keypair.generate(); + const transferAmount = 1_000_000; // 0.001 SOL + + const callHandlerV2Ix = createCallHandlerV2Instruction( + validator, + ownerProgram, + ownerProgram, + escrowAuthority.publicKey, + 255, + [ + { pubkey: destination.publicKey, isSigner: false, isWritable: true }, + { + pubkey: web3.SystemProgram.programId, + isSigner: false, + isWritable: false, + }, + ], + // handler data: commit_base_action_handler_v2 discriminator + amount + Buffer.concat([ + Buffer.from([1, 0, 3, 0]), + Buffer.from( + new Uint8Array(new BigUint64Array([BigInt(transferAmount)]).buffer) + ), + ]) + ); + + const txId = await processInstruction(callHandlerV2Ix); + console.log("Call handler v2 tx:", txId); + + const destAccount = await provider.connection.getAccountInfo( + destination.publicKey + ); + assert.isNotNull(destAccount, "destination account should exist"); + assert.strictEqual( + destAccount.lamports, + transferAmount, + "destination should have received the transfer amount" + ); + }); + it("Commit a new state to the PDA", async () => { let account = await provider.connection.getAccountInfo(pda); let new_data = account.data; @@ -571,6 +627,74 @@ describe("TestDelegation", () => { return ix; } + function createTopUpEphemeralBalanceInstruction( + funder: web3.PublicKey, + authority: web3.PublicKey, + amount: number, + index: number + ) { + const ephemeralBalancePda = ephemeralBalancePdaFromPayer(authority, index); + const keys = [ + { pubkey: funder, isSigner: true, isWritable: true }, + { pubkey: authority, isSigner: false, isWritable: false }, + { pubkey: ephemeralBalancePda, isSigner: false, isWritable: true }, + { + pubkey: web3.SystemProgram.programId, + isSigner: false, + isWritable: false, + }, + ]; + // discriminator(8) + amount(u64) + index(u8) + const data = Buffer.alloc(8 + 8 + 1); + data.writeUint8(9, 0); // TopUpEphemeralBalance discriminator = 9 + data.writeBigUInt64LE(BigInt(amount), 8); + data.writeUint8(index, 16); + const ix = new web3.TransactionInstruction({ + programId: new web3.PublicKey(DELEGATION_PROGRAM_ID), + keys, + data, + }); + return ix; + } + + function createCallHandlerV2Instruction( + validator: web3.PublicKey, + destinationProgram: web3.PublicKey, + sourceProgram: web3.PublicKey, + escrowAuthority: web3.PublicKey, + escrowIndex: number, + otherAccounts: web3.AccountMeta[], + handlerData: Buffer + ) { + const validatorFeesVault = validatorFeesVaultPdaFromValidator(validator); + const escrowAccount = ephemeralBalancePdaFromPayer( + escrowAuthority, + escrowIndex + ); + const keys = [ + { pubkey: validator, isSigner: true, isWritable: true }, + { pubkey: validatorFeesVault, isSigner: false, isWritable: true }, + { pubkey: destinationProgram, isSigner: false, isWritable: false }, + { pubkey: sourceProgram, isSigner: false, isWritable: false }, + { pubkey: escrowAuthority, isSigner: false, isWritable: true }, + { pubkey: escrowAccount, isSigner: false, isWritable: true }, + ...otherAccounts, + ]; + // discriminator(8) + escrow_index(u8) + data_len(u32 LE) + data + const dataLen = handlerData.length; + const data = Buffer.alloc(8 + 1 + 4 + dataLen); + data.writeUint8(20, 0); // CallHandlerV2 discriminator = 20 + data.writeUint8(escrowIndex, 8); + data.writeUint32LE(dataLen, 9); + handlerData.copy(data, 13); + const ix = new web3.TransactionInstruction({ + programId: new web3.PublicKey(DELEGATION_PROGRAM_ID), + keys, + data, + }); + return ix; + } + function createWhitelistValidatorForProgramInstruction( admin: web3.PublicKey, validator: web3.PublicKey, @@ -643,3 +767,13 @@ function programConfigPdaFromProgramId(programId: web3.PublicKey) { new web3.PublicKey(DELEGATION_PROGRAM_ID) )[0]; } + +function ephemeralBalancePdaFromPayer( + payer: web3.PublicKey, + index: number +) { + return web3.PublicKey.findProgramAddressSync( + [Buffer.from("balance"), payer.toBuffer(), Buffer.from([index])], + new web3.PublicKey(DELEGATION_PROGRAM_ID) + )[0]; +} diff --git a/tests/test_call_handler_v2.rs b/tests/test_call_handler_v2.rs new file mode 100644 index 00000000..caeb414c --- /dev/null +++ b/tests/test_call_handler_v2.rs @@ -0,0 +1,485 @@ +use crate::fixtures::{ + create_delegation_metadata_data, create_delegation_record_data, get_commit_record_account_data, + get_delegation_metadata_data, get_delegation_record_data, DELEGATED_PDA_ID, + DELEGATED_PDA_OWNER_ID, TEST_AUTHORITY, +}; +use borsh::{to_vec, BorshDeserialize, BorshSerialize}; +use dlp::args::CallHandlerArgs; +use dlp::ephemeral_balance_seeds_from_payer; +use dlp::pda::{ + commit_record_pda_from_delegated_account, commit_state_pda_from_delegated_account, + delegation_metadata_pda_from_delegated_account, delegation_record_pda_from_delegated_account, + ephemeral_balance_pda_from_payer, fees_vault_pda, validator_fees_vault_pda_from_validator, +}; +use solana_program::instruction::AccountMeta; +use solana_program::rent::Rent; +use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, system_program}; +use solana_program_test::{read_file, BanksClient, ProgramTest}; +use solana_sdk::pubkey::Pubkey; +use solana_sdk::{ + account::Account, + signature::{Keypair, Signer}, + transaction::Transaction, +}; + +mod fixtures; + +const COMMIT_HANDLER_V2_DISCRIMINATOR: [u8; 4] = [1, 0, 3, 0]; +const UNDELEGATE_HANDLER_V2_DISCRIMINATOR: [u8; 4] = [1, 0, 4, 0]; + +// Mimic counter from test_delegation program +#[derive(BorshSerialize, BorshDeserialize)] +pub struct Counter { + pub count: u64, +} + +async fn setup_delegated_pda(program_test: &mut ProgramTest, authority_pubkey: &Pubkey) { + let state = to_vec(&Counter { count: 100 }).unwrap(); + // Setup a delegated PDA + program_test.add_account( + DELEGATED_PDA_ID, + Account { + lamports: LAMPORTS_PER_SOL, + data: state, + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Setup the delegation record PDA + let delegation_record_data = get_delegation_record_data(*authority_pubkey, None); + program_test.add_account( + delegation_record_pda_from_delegated_account(&DELEGATED_PDA_ID), + Account { + lamports: Rent::default().minimum_balance(delegation_record_data.len()), + data: delegation_record_data.clone(), + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Setup the delegated account metadata PDA + let delegation_metadata_data = get_delegation_metadata_data(*authority_pubkey, Some(true)); + program_test.add_account( + delegation_metadata_pda_from_delegated_account(&DELEGATED_PDA_ID), + Account { + lamports: Rent::default().minimum_balance(delegation_metadata_data.len()), + data: delegation_metadata_data, + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); +} + +async fn setup_commit_state(program_test: &mut ProgramTest, authority_pubkey: &Pubkey) { + // Setup the commit state PDA + let commit_state = to_vec(&Counter { count: 101 }).unwrap(); + program_test.add_account( + commit_state_pda_from_delegated_account(&DELEGATED_PDA_ID), + Account { + lamports: LAMPORTS_PER_SOL, + data: commit_state, + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let commit_record_data = get_commit_record_account_data(*authority_pubkey); + program_test.add_account( + commit_record_pda_from_delegated_account(&DELEGATED_PDA_ID), + Account { + lamports: Rent::default().minimum_balance(commit_record_data.len()), + data: commit_record_data, + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); +} + +async fn setup_invalid_escrow_account(program_test: &mut ProgramTest, authority_pubkey: &Pubkey) { + let ephemeral_balance_pda = ephemeral_balance_pda_from_payer(&DELEGATED_PDA_ID, 0); + + // Setup the delegated account PDA + program_test.add_account( + ephemeral_balance_pda, + Account { + lamports: LAMPORTS_PER_SOL, + data: vec![], + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Setup the delegated record PDA + let delegation_record_data = + create_delegation_record_data(*authority_pubkey, dlp::id(), Some(LAMPORTS_PER_SOL)); + program_test.add_account( + delegation_record_pda_from_delegated_account(&ephemeral_balance_pda), + Account { + lamports: Rent::default().minimum_balance(delegation_record_data.len()), + data: delegation_record_data, + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Setup the delegated account metadata PDA + let delegation_metadata_data = create_delegation_metadata_data( + *authority_pubkey, + ephemeral_balance_seeds_from_payer!(DELEGATED_PDA_ID, 0), + true, + ); + program_test.add_account( + delegation_metadata_pda_from_delegated_account(&ephemeral_balance_pda), + Account { + lamports: Rent::default().minimum_balance(delegation_metadata_data.len()), + data: delegation_metadata_data, + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); +} + +async fn setup_delegated_ephemeral_balance( + program_test: &mut ProgramTest, + validator: &Keypair, + payer: &Keypair, +) { + let ephemeral_balance_pda = ephemeral_balance_pda_from_payer(&payer.pubkey(), 1); + + // Setup the delegated account PDA + program_test.add_account( + ephemeral_balance_pda, + Account { + lamports: LAMPORTS_PER_SOL, + data: vec![], + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Setup the delegated record PDA + let delegation_record_data = create_delegation_record_data( + validator.pubkey(), + system_program::id(), + Some(LAMPORTS_PER_SOL), + ); + program_test.add_account( + delegation_record_pda_from_delegated_account(&ephemeral_balance_pda), + Account { + lamports: Rent::default().minimum_balance(delegation_record_data.len()), + data: delegation_record_data, + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Setup the delegated account metadata PDA + let delegation_metadata_data = create_delegation_metadata_data( + validator.pubkey(), + ephemeral_balance_seeds_from_payer!(payer.pubkey(), 0), + true, + ); + program_test.add_account( + delegation_metadata_pda_from_delegated_account(&ephemeral_balance_pda), + Account { + lamports: Rent::default().minimum_balance(delegation_metadata_data.len()), + data: delegation_metadata_data, + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); +} + +async fn setup_ephemeral_balance(program_test: &mut ProgramTest, payer: &Keypair) { + let ephemeral_balance_pda = ephemeral_balance_pda_from_payer(&payer.pubkey(), 2); + + // Setup the delegated account PDA + program_test.add_account( + ephemeral_balance_pda, + Account { + lamports: LAMPORTS_PER_SOL, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); +} + +async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { + let mut program_test = ProgramTest::new("dlp", dlp::ID, None); + program_test.prefer_bpf(true); + + let payer = Keypair::new(); + let validator = Keypair::from_bytes(&TEST_AUTHORITY).unwrap(); + + // Setup authority + program_test.add_account( + validator.pubkey(), + Account { + lamports: LAMPORTS_PER_SOL, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Setup necessary accounts + setup_delegated_pda(&mut program_test, &validator.pubkey()).await; + setup_commit_state(&mut program_test, &validator.pubkey()).await; + setup_invalid_escrow_account(&mut program_test, &validator.pubkey()).await; + setup_delegated_ephemeral_balance(&mut program_test, &validator, &payer).await; + setup_ephemeral_balance(&mut program_test, &payer).await; + + // Setup the protocol fees vault + program_test.add_account( + fees_vault_pda(), + Account { + lamports: Rent::default().minimum_balance(0), + data: vec![], + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + // Setup the validator fees vault + program_test.add_account( + validator_fees_vault_pda_from_validator(&validator.pubkey()), + Account { + lamports: LAMPORTS_PER_SOL, + data: vec![], + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Setup program to test delegation + let data = read_file("tests/buffers/test_delegation.so"); + program_test.add_account( + DELEGATED_PDA_OWNER_ID, + Account { + lamports: Rent::default().minimum_balance(data.len()).max(1), + data, + owner: solana_sdk::bpf_loader::id(), + executable: true, + rent_epoch: 0, + }, + ); + + let (banks, _, blockhash) = program_test.start().await; + (banks, payer, validator, blockhash) +} + +/// Test call_handler_v2 in finalize context +#[tokio::test] +async fn test_finalize_call_handler_v2() { + const PRIZE: u64 = LAMPORTS_PER_SOL / 1000; + + let (banks, payer, validator, blockhash) = setup_program_test_env().await; + + let transfer_destination = Keypair::new(); + let finalize_ix = dlp::instruction_builder::finalize(validator.pubkey(), DELEGATED_PDA_ID); + let call_handler_v2_ix = dlp::instruction_builder::call_handler_v2( + validator.pubkey(), + DELEGATED_PDA_OWNER_ID, // destination program + DELEGATED_PDA_OWNER_ID, // source program + payer.pubkey(), // escrow authority + vec![ + AccountMeta::new(transfer_destination.pubkey(), false), + AccountMeta::new_readonly(system_program::id(), false), + ], + CallHandlerArgs { + escrow_index: 2, // undelegated escrow index + data: [ + COMMIT_HANDLER_V2_DISCRIMINATOR.to_vec(), + to_vec(&PRIZE).unwrap(), + ] + .concat(), + }, + ); + + let tx = Transaction::new_signed_with_payer( + &[finalize_ix, call_handler_v2_ix], + Some(&validator.pubkey()), + &[&validator], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); + + // Prize transferred + let transfer_destination = banks + .get_account(transfer_destination.pubkey()) + .await + .unwrap() + .unwrap(); + assert_eq!(transfer_destination.lamports, PRIZE); +} + +/// Test call_handler_v2 in undelegate context +#[tokio::test] +async fn test_undelegate_call_handler_v2() { + const PRIZE: u64 = LAMPORTS_PER_SOL / 1000; + + let (banks, payer, validator, blockhash) = setup_program_test_env().await; + + let transfer_destination = Keypair::new(); + let finalize_ix = dlp::instruction_builder::finalize(validator.pubkey(), DELEGATED_PDA_ID); + let undelegate_ix = dlp::instruction_builder::undelegate( + validator.pubkey(), + DELEGATED_PDA_ID, + DELEGATED_PDA_OWNER_ID, + validator.pubkey(), + ); + let call_handler_v2_ix = dlp::instruction_builder::call_handler_v2( + validator.pubkey(), + DELEGATED_PDA_OWNER_ID, // destination program + DELEGATED_PDA_OWNER_ID, // source program + payer.pubkey(), // escrow authority + vec![ + AccountMeta::new(transfer_destination.pubkey(), false), + AccountMeta::new(DELEGATED_PDA_ID, false), + AccountMeta::new_readonly(system_program::id(), false), + ], + CallHandlerArgs { + escrow_index: 2, // undelegated escrow index + data: [ + UNDELEGATE_HANDLER_V2_DISCRIMINATOR.to_vec(), + to_vec(&PRIZE).unwrap(), + ] + .concat(), + }, + ); + + let tx = Transaction::new_signed_with_payer( + &[finalize_ix, undelegate_ix, call_handler_v2_ix], + Some(&validator.pubkey()), + &[&validator], + blockhash, + ); + + let counter_before = banks.get_account(DELEGATED_PDA_ID).await.unwrap().unwrap(); + let counter_before = Counter::try_from_slice(&counter_before.data).unwrap(); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); + + // Prize transferred + let transfer_destination = banks + .get_account(transfer_destination.pubkey()) + .await + .unwrap() + .unwrap(); + assert_eq!(transfer_destination.lamports, PRIZE); + + let counter_after = banks.get_account(DELEGATED_PDA_ID).await.unwrap().unwrap(); + let counter_after = Counter::try_from_slice(&counter_after.data).unwrap(); + // Committing state from count 100 to 101, and then increasing in handler on 1 + assert_eq!(counter_before.count + 2, counter_after.count); +} + +/// Testing call_handler_v2 in finalize context with invalid escrow +#[tokio::test] +async fn test_finalize_invalid_escrow_call_handler_v2() { + // Setup + let (banks, _, authority, blockhash) = setup_program_test_env().await; + + // Submit the finalize with handler tx + let transfer_destination = Keypair::new(); + let finalize_ix = dlp::instruction_builder::finalize(authority.pubkey(), DELEGATED_PDA_ID); + let call_handler_v2_ix = dlp::instruction_builder::call_handler_v2( + authority.pubkey(), + DELEGATED_PDA_OWNER_ID, // destination program + DELEGATED_PDA_OWNER_ID, // source program + DELEGATED_PDA_ID, + vec![AccountMeta::new(transfer_destination.pubkey(), false)], + CallHandlerArgs { + escrow_index: 0, + data: COMMIT_HANDLER_V2_DISCRIMINATOR.to_vec(), + }, + ); + let tx = Transaction::new_signed_with_payer( + &[finalize_ix, call_handler_v2_ix], + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res + .unwrap_err() + .to_string() + .contains("Invalid account owner")); +} + +#[tokio::test] +async fn test_undelegate_invalid_escrow_call_handler_v2() { + const PRIZE: u64 = LAMPORTS_PER_SOL / 1000; + + let (banks, _, authority, blockhash) = setup_program_test_env().await; + + // Submit the finalize with handler tx + let destination = Keypair::new(); + let finalize_ix = dlp::instruction_builder::finalize(authority.pubkey(), DELEGATED_PDA_ID); + let finalize_call_handler_v2_ix = dlp::instruction_builder::call_handler_v2( + authority.pubkey(), + DELEGATED_PDA_OWNER_ID, // handler program + DELEGATED_PDA_OWNER_ID, // source program + DELEGATED_PDA_ID, + vec![AccountMeta::new(destination.pubkey(), false)], + CallHandlerArgs { + escrow_index: 0, + data: UNDELEGATE_HANDLER_V2_DISCRIMINATOR.to_vec(), + }, + ); + + let undelegate_ix = dlp::instruction_builder::undelegate( + authority.pubkey(), + DELEGATED_PDA_ID, + DELEGATED_PDA_OWNER_ID, + authority.pubkey(), + ); + let undelegate_call_handler_v2_ix = dlp::instruction_builder::call_handler_v2( + authority.pubkey(), + DELEGATED_PDA_OWNER_ID, // handler program + DELEGATED_PDA_OWNER_ID, // source program + DELEGATED_PDA_ID, + vec![AccountMeta::new(destination.pubkey(), false)], + CallHandlerArgs { + escrow_index: 0, + data: [ + UNDELEGATE_HANDLER_V2_DISCRIMINATOR.to_vec(), + to_vec(&PRIZE).unwrap(), + ] + .concat(), + }, + ); + let tx = Transaction::new_signed_with_payer( + &[ + finalize_ix, + finalize_call_handler_v2_ix, + undelegate_ix, + undelegate_call_handler_v2_ix, + ], + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res + .unwrap_err() + .to_string() + .contains("Invalid account owner")); +}