diff --git a/src/instruction_builder/close_ephemeral_balance.rs b/src/instruction_builder/close_ephemeral_balance.rs index dece09b..cae2470 100644 --- a/src/instruction_builder/close_ephemeral_balance.rs +++ b/src/instruction_builder/close_ephemeral_balance.rs @@ -1,5 +1,5 @@ use solana_program::instruction::Instruction; -use solana_program::{instruction::AccountMeta, pubkey::Pubkey}; +use solana_program::{instruction::AccountMeta, pubkey::Pubkey, system_program}; use crate::discriminator::DlpDiscriminator; use crate::pda::ephemeral_balance_pda_from_payer; @@ -13,6 +13,7 @@ pub fn close_ephemeral_balance(payer: Pubkey, index: u8) -> Instruction { accounts: vec![ AccountMeta::new(payer, true), AccountMeta::new(ephemeral_balance_pda, false), + AccountMeta::new_readonly(system_program::id(), false), ], data: [ DlpDiscriminator::CloseEphemeralBalance.to_vec(), diff --git a/src/instruction_builder/delegate_ephemeral_balance.rs b/src/instruction_builder/delegate_ephemeral_balance.rs index 9c36847..b08d9b3 100644 --- a/src/instruction_builder/delegate_ephemeral_balance.rs +++ b/src/instruction_builder/delegate_ephemeral_balance.rs @@ -21,7 +21,7 @@ pub fn delegate_ephemeral_balance( let delegated_account = ephemeral_balance_pda_from_payer(&pubkey, args.index); let delegate_buffer_pda = delegate_buffer_pda_from_delegated_account_and_owner_program( &delegated_account, - &crate::id(), + &system_program::id(), ); let delegation_record_pda = delegation_record_pda_from_delegated_account(&delegated_account); let delegation_metadata_pda = diff --git a/src/processor/close_ephemeral_balance.rs b/src/processor/close_ephemeral_balance.rs index ac9d784..61e41a4 100644 --- a/src/processor/close_ephemeral_balance.rs +++ b/src/processor/close_ephemeral_balance.rs @@ -1,8 +1,12 @@ use crate::ephemeral_balance_seeds_from_payer; -use crate::processor::utils::loaders::{load_initialized_pda, load_signer}; -use crate::processor::utils::pda::close_pda; +use crate::processor::utils::loaders::{load_pda, load_signer}; +use solana_program::msg; +use solana_program::program::invoke_signed; use solana_program::program_error::ProgramError; -use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey}; +use solana_program::system_instruction::transfer; +use solana_program::{ + account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey, system_program, +}; /// Process the closing of an ephemeral balance account /// @@ -10,6 +14,7 @@ use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubke /// /// 0: `[signer]` payer to pay for the transaction and receive the refund /// 1: `[writable]` ephemeral balance account we are closing +/// 2: `[]` the system program /// /// Requirements: /// @@ -27,21 +32,45 @@ pub fn process_close_ephemeral_balance( let index = *data.first().ok_or(ProgramError::InvalidInstructionData)?; // Load Accounts - let [payer, ephemeral_balance_account] = accounts else { + let [payer, ephemeral_balance_account, system_program] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; load_signer(payer, "payer")?; - load_initialized_pda( + let ephemeral_balance_seeds: &[&[u8]] = ephemeral_balance_seeds_from_payer!(payer.key, index); + let ephemeral_balance_bump = load_pda( ephemeral_balance_account, - ephemeral_balance_seeds_from_payer!(payer.key, index), + ephemeral_balance_seeds, &crate::id(), true, "ephemeral balance", )?; + if ephemeral_balance_account.owner != &system_program::id() { + msg!( + "ephemeral balance expected to be owned by system program. got: {}", + ephemeral_balance_account.owner + ); + return Err(ProgramError::InvalidAccountOwner); + } - close_pda(ephemeral_balance_account, payer)?; + let amount = ephemeral_balance_account.lamports(); + if amount == 0 { + return Ok(()); + } + + let ephemeral_balance_bump_slice: &[u8] = &[ephemeral_balance_bump]; + let ephemeral_balance_signer_seeds = + [ephemeral_balance_seeds, &[ephemeral_balance_bump_slice]].concat(); + invoke_signed( + &transfer(ephemeral_balance_account.key, payer.key, amount), + &[ + ephemeral_balance_account.clone(), + payer.clone(), + system_program.clone(), + ], + &[&ephemeral_balance_signer_seeds], + )?; Ok(()) } diff --git a/src/processor/delegate.rs b/src/processor/delegate.rs index 00804d6..444f632 100644 --- a/src/processor/delegate.rs +++ b/src/processor/delegate.rs @@ -63,11 +63,21 @@ pub fn process_delegate( load_owned_pda(delegated_account, &crate::id(), "delegated account")?; load_program(system_program, system_program::id(), "system program")?; + msg!("Delegating: {}", delegated_account.key); + // Validate seeds if the delegate account is not on curve, i.e. is a PDA + // If the owner is the system program, we check if the account is derived from the delegation program, + // allowing delegation of escrow accounts if !is_on_curve(delegated_account.key) { let seeds_to_validate: Vec<&[u8]> = args.seeds.iter().map(|v| v.as_slice()).collect(); + let program_id = if owner_program.key.eq(&system_program::id()) { + crate::id() + } else { + *owner_program.key + }; let (derived_pda, _) = - Pubkey::find_program_address(seeds_to_validate.as_ref(), owner_program.key); + Pubkey::find_program_address(seeds_to_validate.as_ref(), &program_id); + if derived_pda.ne(delegated_account.key) { msg!( "Expected delegated PDA to be {}, but got {}", diff --git a/src/processor/delegate_ephemeral_balance.rs b/src/processor/delegate_ephemeral_balance.rs index cd467bc..f98bf15 100644 --- a/src/processor/delegate_ephemeral_balance.rs +++ b/src/processor/delegate_ephemeral_balance.rs @@ -76,7 +76,7 @@ pub fn process_delegate_ephemeral_balance( let ix = crate::instruction_builder::delegate( *payer.key, *ephemeral_balance_account.key, - Some(crate::id()), + Some(system_program::id()), args.delegate_args, ); diff --git a/src/processor/top_up_ephemeral_balance.rs b/src/processor/top_up_ephemeral_balance.rs index cca1e38..ccd0fc6 100644 --- a/src/processor/top_up_ephemeral_balance.rs +++ b/src/processor/top_up_ephemeral_balance.rs @@ -56,7 +56,7 @@ pub fn process_top_up_ephemeral_balance( create_pda( ephemeral_balance_account, &system_program::id(), - 8, + 0, ephemeral_balance_seeds_from_payer!(pubkey.key, args.index), bump_ephemeral_balance, system_program, diff --git a/src/state/utils/discriminator.rs b/src/state/utils/discriminator.rs index 7d283ec..6f0c832 100644 --- a/src/state/utils/discriminator.rs +++ b/src/state/utils/discriminator.rs @@ -10,7 +10,7 @@ pub enum AccountDiscriminator { } impl AccountDiscriminator { - pub fn to_bytes(&self) -> [u8; 8] { + pub const fn to_bytes(&self) -> [u8; 8] { let num = (*self) as u64; num.to_le_bytes() } diff --git a/src/state/utils/to_bytes.rs b/src/state/utils/to_bytes.rs index 1eca739..69a690e 100644 --- a/src/state/utils/to_bytes.rs +++ b/src/state/utils/to_bytes.rs @@ -23,10 +23,10 @@ macro_rules! impl_to_bytes_with_discriminator_borsh { impl $struct_name { pub fn to_bytes_with_discriminator( &self, - data: &mut W, + writer: &mut W, ) -> Result<(), ::solana_program::program_error::ProgramError> { - data.write_all(&Self::discriminator().to_bytes())?; - self.serialize(data)?; + writer.write_all(&Self::discriminator().to_bytes())?; + self.serialize(writer)?; Ok(()) } } diff --git a/tests/test_top_up.rs b/tests/test_top_up.rs index e880bd6..e55daf2 100644 --- a/tests/test_top_up.rs +++ b/tests/test_top_up.rs @@ -7,11 +7,12 @@ use dlp::pda::{ 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 dlp::state::DelegationRecord; use solana_program::rent::Rent; use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, system_program}; use solana_program_test::{processor, BanksClient, ProgramTest}; use solana_sdk::{ - account::Account, + account::{Account, ReadableAccount}, signature::{Keypair, Signer}, transaction::Transaction, }; @@ -45,6 +46,30 @@ async fn test_top_up_ephemeral_balance() { assert!(balance_account.lamports > 0); } +#[tokio::test] +async fn test_top_up_ephemeral_balance_for_pubkey() { + // Setup + let (banks, payer, _, blockhash) = setup_program_test_env().await; + + let pubkey = Keypair::new().pubkey(); + + let ix = dlp::instruction_builder::top_up_ephemeral_balance(payer.pubkey(), pubkey, None, None); + let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); + + // Check account exists and it's owned by the system program + let ephemeral_balance_pda = ephemeral_balance_pda_from_payer(&pubkey, 0); + let balance_account = banks + .get_account(ephemeral_balance_pda) + .await + .unwrap() + .unwrap(); + + assert_eq!(balance_account.owner, system_program::id()); + assert!(balance_account.lamports > 0); +} + #[tokio::test] async fn test_top_up_ephemeral_balance_and_delegate() { // Setup @@ -72,30 +97,30 @@ async fn test_top_up_ephemeral_balance_and_delegate() { ); let res = banks.process_transaction(tx).await; assert!(res.is_ok()); -} - -#[tokio::test] -async fn test_top_up_ephemeral_balance_for_pubkey() { - // Setup - let (banks, payer, _, blockhash) = setup_program_test_env().await; - - let pubkey = Keypair::new().pubkey(); - - let ix = dlp::instruction_builder::top_up_ephemeral_balance(payer.pubkey(), pubkey, None, None); - let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash); - let res = banks.process_transaction(tx).await; - assert!(res.is_ok()); - // Check account exists and it's owned by the system program - let ephemeral_balance_pda = ephemeral_balance_pda_from_payer(&pubkey, 0); + // Check account exists and it's owned by the delegation program (delegated) + let ephemeral_balance_pda = ephemeral_balance_pda_from_payer(&payer.pubkey(), 0); let balance_account = banks .get_account(ephemeral_balance_pda) .await .unwrap() .unwrap(); - assert_eq!(balance_account.owner, system_program::id()); + assert_eq!(balance_account.owner, dlp::id()); assert!(balance_account.lamports > 0); + + // Check the delegation record PDA has system program as owner + let delegation_record_pda = + delegation_record_pda_from_delegated_account(&ephemeral_balance_pda); + let delegation_record_account = banks + .get_account(delegation_record_pda) + .await + .unwrap() + .unwrap(); + let delegation_record = + DelegationRecord::try_from_bytes_with_discriminator(&delegation_record_account.data) + .unwrap(); + assert_eq!(delegation_record.owner, system_program::id()); } #[tokio::test] @@ -123,6 +148,58 @@ async fn test_top_up_ephemeral_balance_and_delegate_for_pubkey() { ); let res = banks.process_transaction(tx).await; assert!(res.is_ok()); + + // Check the accounts exists and it's owned by the delegation program + let ephemeral_balance_pda = ephemeral_balance_pda_from_payer(&pubkey, 0); + let balance_account = banks + .get_account(ephemeral_balance_pda) + .await + .unwrap() + .unwrap(); + + assert_eq!(balance_account.owner, dlp::id()); + assert!(balance_account.lamports > 0); +} + +#[tokio::test] +async fn test_undelegate() { + // Setup + let (banks, _, payer_alt, blockhash) = setup_program_test_env().await; + let validator = Keypair::from_bytes(&TEST_AUTHORITY).unwrap(); + + let ephemeral_balance_pda = ephemeral_balance_pda_from_payer(&payer_alt.pubkey(), 0); + let ephemeral_balance_owner = banks + .get_account(ephemeral_balance_pda) + .await + .unwrap() + .unwrap() + .owner; + + assert_eq!(ephemeral_balance_owner, dlp::id()); + + // Undelegate ephemeral balance Ix + let ix = dlp::instruction_builder::undelegate( + validator.pubkey(), + ephemeral_balance_pda, + system_program::id(), + validator.pubkey(), + ); + + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&validator.pubkey()), + &[&validator], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); + + // Assert that the ephemeral balance account still exists but is now owned by the system program + let ephemeral_balance_account = banks.get_account(ephemeral_balance_pda).await.unwrap(); + assert!(ephemeral_balance_account.is_some()); + + let actual_owner = *ephemeral_balance_account.unwrap().owner(); + assert_eq!(actual_owner, system_program::id()); } #[tokio::test] @@ -152,7 +229,7 @@ async fn test_undelegate_and_close() { let ix = dlp::instruction_builder::undelegate( validator.pubkey(), ephemeral_balance_pda, - dlp::id(), + system_program::id(), validator.pubkey(), ); @@ -201,87 +278,98 @@ async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { }, ); - let ephemeral_balance_pda = ephemeral_balance_pda_from_payer(&payer_alt.pubkey(), 0); + setup_ephemeral_balance(&mut program_test, &validator, &payer_alt).await; - // Setup the delegated account PDA + // Setup the validator keypair program_test.add_account( - ephemeral_balance_pda, + validator.pubkey(), Account { lamports: LAMPORTS_PER_SOL, data: vec![], - owner: dlp::id(), + owner: system_program::id(), executable: false, rent_epoch: 0, }, ); - // Setup the delegated record PDA - let delegation_record_data = - create_delegation_record_data(validator.pubkey(), dlp::id(), Some(LAMPORTS_PER_SOL)); + // Setup the protocol fees vault program_test.add_account( - delegation_record_pda_from_delegated_account(&ephemeral_balance_pda), + fees_vault_pda(), Account { - lamports: Rent::default().minimum_balance(delegation_record_data.len()), - data: delegation_record_data, + lamports: Rent::default().minimum_balance(0), + data: vec![], 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_alt.pubkey(), 0), - true, - ); + // Setup the validator fees vault program_test.add_account( - delegation_metadata_pda_from_delegated_account(&ephemeral_balance_pda), + validator_fees_vault_pda_from_validator(&validator.pubkey()), Account { - lamports: Rent::default().minimum_balance(delegation_metadata_data.len()), - data: delegation_metadata_data, + lamports: LAMPORTS_PER_SOL, + data: vec![], owner: dlp::id(), executable: false, rent_epoch: 0, }, ); - // Setup the validator keypair + let (banks, payer, blockhash) = program_test.start().await; + (banks, payer, payer_alt, blockhash) +} + +async fn setup_ephemeral_balance( + program_test: &mut ProgramTest, + validator: &Keypair, + payer: &Keypair, +) { + let ephemeral_balance_pda = ephemeral_balance_pda_from_payer(&payer.pubkey(), 0); + + // Setup the delegated account PDA program_test.add_account( - validator.pubkey(), + ephemeral_balance_pda, Account { lamports: LAMPORTS_PER_SOL, data: vec![], - owner: system_program::id(), + owner: dlp::id(), executable: false, rent_epoch: 0, }, ); - // Setup the protocol fees vault + // 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( - fees_vault_pda(), + delegation_record_pda_from_delegated_account(&ephemeral_balance_pda), Account { - lamports: Rent::default().minimum_balance(0), - data: vec![], + lamports: Rent::default().minimum_balance(delegation_record_data.len()), + data: delegation_record_data, owner: dlp::id(), executable: false, rent_epoch: 0, }, ); - // Setup the validator fees vault + // 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( - validator_fees_vault_pda_from_validator(&validator.pubkey()), + delegation_metadata_pda_from_delegated_account(&ephemeral_balance_pda), Account { - lamports: LAMPORTS_PER_SOL, - data: vec![], + lamports: Rent::default().minimum_balance(delegation_metadata_data.len()), + data: delegation_metadata_data, owner: dlp::id(), executable: false, rent_epoch: 0, }, ); - - let (banks, payer, blockhash) = program_test.start().await; - (banks, payer, payer_alt, blockhash) }