diff --git a/src/args/mod.rs b/src/args/mod.rs index 456d44d7..f79e1b83 100644 --- a/src/args/mod.rs +++ b/src/args/mod.rs @@ -2,6 +2,7 @@ mod call_handler; mod commit_state; mod delegate; mod delegate_ephemeral_balance; +mod set_fees_receiver; mod top_up_ephemeral_balance; mod validator_claim_fees; mod whitelist_validator_for_program; @@ -10,6 +11,7 @@ pub use call_handler::*; pub use commit_state::*; pub use delegate::*; pub use delegate_ephemeral_balance::*; +pub use set_fees_receiver::*; pub use top_up_ephemeral_balance::*; pub use validator_claim_fees::*; pub use whitelist_validator_for_program::*; diff --git a/src/args/set_fees_receiver.rs b/src/args/set_fees_receiver.rs new file mode 100644 index 00000000..70784343 --- /dev/null +++ b/src/args/set_fees_receiver.rs @@ -0,0 +1,7 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use solana_program::pubkey::Pubkey; + +#[derive(BorshSerialize, BorshDeserialize)] +pub struct SetFeesReceiverArgs { + pub fees_receiver: Pubkey, +} diff --git a/src/discriminator.rs b/src/discriminator.rs index 962d71f7..2b5d2e26 100644 --- a/src/discriminator.rs +++ b/src/discriminator.rs @@ -34,6 +34,8 @@ pub enum DlpDiscriminator { CloseValidatorFeesVault = 14, /// See [crate::processor::process_call_handler] for docs. CallHandler = 15, + /// See [crate::processor::process_set_fees_receiver] for docs. + SetFeesReceiver = 16, } impl DlpDiscriminator { diff --git a/src/instruction_builder/init_protocol_fees_vault.rs b/src/instruction_builder/init_protocol_fees_vault.rs index 7192a877..4b2517c7 100644 --- a/src/instruction_builder/init_protocol_fees_vault.rs +++ b/src/instruction_builder/init_protocol_fees_vault.rs @@ -1,6 +1,9 @@ -use solana_program::instruction::Instruction; -use solana_program::system_program; -use solana_program::{instruction::AccountMeta, pubkey::Pubkey}; +use solana_program::{ + bpf_loader_upgradeable, + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + system_program, +}; use crate::discriminator::DlpDiscriminator; use crate::pda::fees_vault_pda; @@ -9,11 +12,14 @@ use crate::pda::fees_vault_pda; /// See [crate::processor::process_init_protocol_fees_vault] for docs. pub fn init_protocol_fees_vault(payer: Pubkey) -> Instruction { let fees_vault_pda = fees_vault_pda(); + let delegation_program_data = + Pubkey::find_program_address(&[crate::ID.as_ref()], &bpf_loader_upgradeable::id()).0; Instruction { program_id: crate::id(), accounts: vec![ AccountMeta::new(payer, true), AccountMeta::new(fees_vault_pda, false), + AccountMeta::new_readonly(delegation_program_data, false), AccountMeta::new_readonly(system_program::id(), false), ], data: DlpDiscriminator::InitProtocolFeesVault.to_vec(), diff --git a/src/instruction_builder/mod.rs b/src/instruction_builder/mod.rs index 139f52de..bda59ef5 100644 --- a/src/instruction_builder/mod.rs +++ b/src/instruction_builder/mod.rs @@ -9,6 +9,7 @@ mod finalize; mod init_protocol_fees_vault; mod init_validator_fees_vault; mod protocol_claim_fees; +mod set_fees_receiver; mod top_up_ephemeral_balance; mod undelegate; mod validator_claim_fees; @@ -25,6 +26,7 @@ pub use finalize::*; pub use init_protocol_fees_vault::*; pub use init_validator_fees_vault::*; pub use protocol_claim_fees::*; +pub use set_fees_receiver::*; pub use top_up_ephemeral_balance::*; pub use undelegate::*; pub use validator_claim_fees::*; diff --git a/src/instruction_builder/protocol_claim_fees.rs b/src/instruction_builder/protocol_claim_fees.rs index 91753999..2e7f60d7 100644 --- a/src/instruction_builder/protocol_claim_fees.rs +++ b/src/instruction_builder/protocol_claim_fees.rs @@ -1,21 +1,18 @@ use solana_program::instruction::Instruction; -use solana_program::{bpf_loader_upgradeable, instruction::AccountMeta, pubkey::Pubkey}; +use solana_program::{instruction::AccountMeta, pubkey::Pubkey}; use crate::discriminator::DlpDiscriminator; use crate::pda::fees_vault_pda; /// Claim the accrued fees from the protocol fees vault. /// See [crate::processor::process_protocol_claim_fees] for docs. -pub fn protocol_claim_fees(admin: Pubkey) -> Instruction { +pub fn protocol_claim_fees(fees_receiver: Pubkey) -> Instruction { let fees_vault_pda = fees_vault_pda(); - let delegation_program_data = - Pubkey::find_program_address(&[crate::ID.as_ref()], &bpf_loader_upgradeable::id()).0; Instruction { program_id: crate::id(), accounts: vec![ - AccountMeta::new(admin, true), AccountMeta::new(fees_vault_pda, false), - AccountMeta::new_readonly(delegation_program_data, false), + AccountMeta::new(fees_receiver, false), ], data: DlpDiscriminator::ProtocolClaimFees.to_vec(), } diff --git a/src/instruction_builder/set_fees_receiver.rs b/src/instruction_builder/set_fees_receiver.rs new file mode 100644 index 00000000..e9bbcb4b --- /dev/null +++ b/src/instruction_builder/set_fees_receiver.rs @@ -0,0 +1,31 @@ +use borsh::to_vec; +use solana_program::instruction::Instruction; +use solana_program::{ + bpf_loader_upgradeable, instruction::AccountMeta, pubkey::Pubkey, system_program, +}; + +use crate::args::SetFeesReceiverArgs; +use crate::discriminator::DlpDiscriminator; +use crate::pda::fees_vault_pda; + +/// Set the fees receiver. +/// See [crate::processor::process_set_fees_receiver] for docs. +pub fn set_fees_receiver(admin: Pubkey, fees_receiver: Pubkey) -> Instruction { + let fees_vault_pda = fees_vault_pda(); + let delegation_program_data = + Pubkey::find_program_address(&[crate::ID.as_ref()], &bpf_loader_upgradeable::id()).0; + Instruction { + program_id: crate::id(), + accounts: vec![ + AccountMeta::new(admin, true), + AccountMeta::new(fees_vault_pda, false), + AccountMeta::new_readonly(delegation_program_data, false), + AccountMeta::new_readonly(system_program::id(), false), + ], + data: [ + DlpDiscriminator::SetFeesReceiver.to_vec(), + to_vec(&SetFeesReceiverArgs { fees_receiver }).unwrap(), + ] + .concat(), + } +} diff --git a/src/lib.rs b/src/lib.rs index b3cf4396..6b307aa5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -125,6 +125,9 @@ pub fn slow_process_instruction( DlpDiscriminator::CallHandler => { processor::process_call_handler(program_id, accounts, data)? } + DlpDiscriminator::SetFeesReceiver => { + processor::process_set_fees_receiver(program_id, accounts, data)? + } _ => { log!("PANIC: Instruction must be processed by fast_process_instruction"); return Err(ProgramError::InvalidInstructionData); diff --git a/src/processor/close_validator_fees_vault.rs b/src/processor/close_validator_fees_vault.rs index fa838aca..37d5b139 100644 --- a/src/processor/close_validator_fees_vault.rs +++ b/src/processor/close_validator_fees_vault.rs @@ -14,7 +14,7 @@ use crate::validator_fees_vault_seeds_from_validator; /// Accounts: /// /// 0; `[signer]` payer -/// 1; `[signer]` admin that controls the vault +/// 1; `[signer]` admin that is the program upgrade authority /// 2; `[]` validator_identity /// 3; `[]` validator_fees_vault_pda /// diff --git a/src/processor/init_protocol_fees_vault.rs b/src/processor/init_protocol_fees_vault.rs index 498c2c4f..e79835b8 100644 --- a/src/processor/init_protocol_fees_vault.rs +++ b/src/processor/init_protocol_fees_vault.rs @@ -1,18 +1,27 @@ -use solana_program::program_error::ProgramError; use solana_program::{ - account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey, system_program, + account_info::AccountInfo, entrypoint::ProgramResult, program_error::ProgramError, + pubkey::Pubkey, system_program, }; -use crate::fees_vault_seeds; -use crate::processor::utils::loaders::{load_program, load_signer, load_uninitialized_pda}; -use crate::processor::utils::pda::create_pda; +use crate::{ + error::DlpError::Unauthorized, + fees_vault_seeds, + processor::utils::{ + loaders::{ + load_program, load_program_upgrade_authority, load_signer, load_uninitialized_pda, + }, + pda::create_pda, + }, + state::FeesVault, +}; /// Initialize the global fees vault /// /// Accounts: /// 0: `[signer]` the account paying for the transaction /// 1: `[writable]` the fees vault PDA we are initializing -/// 2: `[]` the system program +/// 2: `[]` the delegation program data +/// 3: `[]` the system program /// /// Requirements: /// @@ -29,7 +38,7 @@ pub fn process_init_protocol_fees_vault( _data: &[u8], ) -> ProgramResult { // Load Accounts - let [payer, protocol_fees_vault, system_program] = accounts else { + let [payer, protocol_fees_vault, delegation_program_data, system_program] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; @@ -44,16 +53,28 @@ pub fn process_init_protocol_fees_vault( "fees vault", )?; + // Check if the admin is the correct one + let admin_pubkey = + load_program_upgrade_authority(&crate::ID, delegation_program_data)?.ok_or(Unauthorized)?; + + let fees_vault = FeesVault { + fees_receiver: admin_pubkey, + }; + // Create the fees vault account create_pda( protocol_fees_vault, &crate::id(), - 8, + fees_vault.size_with_discriminator(), fees_vault_seeds!(), bump_fees_vault, system_program, payer, )?; + // Write the fees vault data + let mut fees_vault_data = protocol_fees_vault.try_borrow_mut_data()?; + fees_vault.to_bytes_with_discriminator(&mut fees_vault_data.as_mut())?; + Ok(()) } diff --git a/src/processor/mod.rs b/src/processor/mod.rs index 46dc4df1..99b50dab 100644 --- a/src/processor/mod.rs +++ b/src/processor/mod.rs @@ -5,6 +5,7 @@ mod delegate_ephemeral_balance; mod init_protocol_fees_vault; mod init_validator_fees_vault; mod protocol_claim_fees; +mod set_fees_receiver; mod top_up_ephemeral_balance; mod utils; mod validator_claim_fees; @@ -19,6 +20,7 @@ pub use delegate_ephemeral_balance::*; pub use init_protocol_fees_vault::*; pub use init_validator_fees_vault::*; pub use protocol_claim_fees::*; +pub use set_fees_receiver::*; pub use top_up_ephemeral_balance::*; pub use validator_claim_fees::*; pub use whitelist_validator_for_program::*; diff --git a/src/processor/protocol_claim_fees.rs b/src/processor/protocol_claim_fees.rs index d3aa17af..8dc3b279 100644 --- a/src/processor/protocol_claim_fees.rs +++ b/src/processor/protocol_claim_fees.rs @@ -1,68 +1,72 @@ -use crate::error::DlpError::Unauthorized; -use crate::processor::utils::loaders::{ - load_initialized_protocol_fees_vault, load_program_upgrade_authority, load_signer, +use solana_program::{ + account_info::AccountInfo, entrypoint::ProgramResult, program_error::ProgramError, + pubkey::Pubkey, rent::Rent, sysvar::Sysvar, }; -use solana_program::msg; -use solana_program::program_error::ProgramError; -use solana_program::rent::Rent; -use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey}; + +use crate::processor::utils::loaders::{load_account, load_initialized_protocol_fees_vault}; +use crate::state::FeesVault; /// Process request to claim fees from the protocol fees vault /// /// Accounts: /// -/// 1. `[signer]` admin account that can claim the fees -/// 2. `[writable]` protocol fees vault PDA +/// 1. `[writable]` protocol fees vault PDA +/// 2. `[writable]` fees receiver /// /// Requirements: /// /// - protocol fees vault is initialized /// - protocol fees vault has enough lamports to claim fees and still be /// rent exempt -/// - admin is the protocol fees vault admin +/// - fees receiver is the correct one /// -/// 1. Transfer lamports from protocol fees_vault PDA to the admin authority +/// 1. Transfer lamports from the protocol fees vault PDA to the configured fees receiver pub fn process_protocol_claim_fees( _program_id: &Pubkey, accounts: &[AccountInfo], _data: &[u8], ) -> ProgramResult { // Load Accounts - let [admin, fees_vault, delegation_program_data] = accounts else { + let [fees_vault_account, fees_receiver] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; - // Check if the admin is signer - load_signer(admin, "admin")?; - load_initialized_protocol_fees_vault(fees_vault, true)?; + // Validate vault PDA and configured receiver + load_initialized_protocol_fees_vault(fees_vault_account, true)?; + + let fees_vault_data = fees_vault_account.try_borrow_data()?; + let fees_vault = FeesVault::try_from_bytes_with_discriminator(&fees_vault_data)?; - // Check if the admin is the correct one - let admin_pubkey = - load_program_upgrade_authority(&crate::ID, delegation_program_data)?.ok_or(Unauthorized)?; - if !admin.key.eq(&admin_pubkey) { - msg!( - "Expected admin pubkey: {} but got {}", - admin_pubkey, - admin.key - ); - return Err(Unauthorized.into()); + load_account( + fees_receiver, + fees_vault.fees_receiver, + true, + "fees receiver", + )?; + + if fees_receiver.key == fees_vault_account.key { + // Nothing to transfer, or ambiguous aliasing – reject explicitly + return Err(ProgramError::InvalidArgument); } // Calculate the amount to transfer - let min_rent = Rent::default().minimum_balance(8); - if fees_vault.lamports() < min_rent { + let min_rent = Rent::get()?.minimum_balance(fees_vault_account.data_len()); + if fees_vault_account.lamports() < min_rent { return Err(ProgramError::InsufficientFunds); } - let amount = fees_vault.lamports() - min_rent; + let amount = fees_vault_account.lamports() - min_rent; + if amount == 0 { + return Ok(()); + } - // Transfer fees to the admin pubkey - **fees_vault.try_borrow_mut_lamports()? = fees_vault - .lamports() + // Transfer fees to the configured fees receiver + let vault_lamports = fees_vault_account.lamports(); + **fees_vault_account.try_borrow_mut_lamports()? = vault_lamports .checked_sub(amount) .ok_or(ProgramError::InsufficientFunds)?; - **admin.try_borrow_mut_lamports()? = admin - .lamports() + let receiver_lamports = fees_receiver.lamports(); + **fees_receiver.try_borrow_mut_lamports()? = receiver_lamports .checked_add(amount) .ok_or(ProgramError::ArithmeticOverflow)?; diff --git a/src/processor/set_fees_receiver.rs b/src/processor/set_fees_receiver.rs new file mode 100644 index 00000000..eb610b76 --- /dev/null +++ b/src/processor/set_fees_receiver.rs @@ -0,0 +1,93 @@ +use borsh::BorshDeserialize; +use solana_program::{ + account_info::AccountInfo, entrypoint::ProgramResult, msg, program_error::ProgramError, + pubkey::Pubkey, +}; + +use crate::{ + args::SetFeesReceiverArgs, + error::DlpError::Unauthorized, + processor::utils::{ + loaders::{ + load_initialized_protocol_fees_vault, load_program, load_program_upgrade_authority, + load_signer, + }, + pda::resize_pda, + }, + state::FeesVault, +}; + +/// Process request to set the fees receiver +/// +/// Accounts: +/// +/// 1. `[signer, writable]` admin account that can set the fees receiver +/// 2. `[writable]` fees vault PDA +/// 3. `[]` delegation program data +/// 4. `[]` system program +/// +/// Requirements: +/// +/// - admin is the program upgrade authority +/// - fees vault is initialized +/// +/// 1. Set the fees receiver in the [FeesVault] account +pub fn process_set_fees_receiver( + _program_id: &Pubkey, + accounts: &[AccountInfo], + data: &[u8], +) -> ProgramResult { + // Load Accounts + let [admin, fees_vault_account, delegation_program_data, system_program] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + // Check if the admin is signer + load_signer(admin, "admin")?; + + // Check if the admin is the correct one + let admin_pubkey = + load_program_upgrade_authority(&crate::ID, delegation_program_data)?.ok_or(Unauthorized)?; + if !admin.key.eq(&admin_pubkey) { + msg!( + "Expected admin pubkey: {} but got {}", + admin_pubkey, + admin.key + ); + return Err(Unauthorized.into()); + } + + // Check if the fees vault is initialized + load_initialized_protocol_fees_vault(fees_vault_account, true)?; + + // Migrate to the new fees vault structure + let (mut fees_vault, migrated) = { + let data = fees_vault_account.try_borrow_data()?; + match FeesVault::try_from_bytes_with_discriminator(&data) { + Ok(fv) => (fv, false), + Err(_) => (FeesVault::default(), true), + } + }; + + if migrated { + load_program( + system_program, + solana_program::system_program::ID, + "system program", + )?; + resize_pda( + admin, + fees_vault_account, + system_program, + fees_vault.size_with_discriminator(), + )?; + } + + let args = SetFeesReceiverArgs::try_from_slice(data)?; + fees_vault.fees_receiver = args.fees_receiver; + + let mut fees_vault_data = fees_vault_account.try_borrow_mut_data()?; + fees_vault.to_bytes_with_discriminator(&mut fees_vault_data.as_mut())?; + + Ok(()) +} diff --git a/src/processor/utils/pda.rs b/src/processor/utils/pda.rs index c1b470de..6c5c3bc0 100644 --- a/src/processor/utils/pda.rs +++ b/src/processor/utils/pda.rs @@ -88,7 +88,7 @@ pub(crate) fn resize_pda<'a, 'info>( system_program: &'a AccountInfo<'info>, new_size: usize, ) -> Result<(), ProgramError> { - let new_minimum_balance = Rent::default().minimum_balance(new_size); + let new_minimum_balance = Rent::get()?.minimum_balance(new_size); let lamports_diff = new_minimum_balance.saturating_sub(pda.lamports()); invoke( &system_instruction::transfer(payer.key, pda.key, lamports_diff), diff --git a/src/state/fees_vault.rs b/src/state/fees_vault.rs new file mode 100644 index 00000000..c7e670c2 --- /dev/null +++ b/src/state/fees_vault.rs @@ -0,0 +1,26 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use solana_program::pubkey::Pubkey; + +use crate::{impl_to_bytes_with_discriminator_borsh, impl_try_from_bytes_with_discriminator_borsh}; + +use super::discriminator::{AccountDiscriminator, AccountWithDiscriminator}; + +#[derive(BorshSerialize, BorshDeserialize, Default, Debug)] +pub struct FeesVault { + pub fees_receiver: Pubkey, +} + +impl AccountWithDiscriminator for FeesVault { + fn discriminator() -> AccountDiscriminator { + AccountDiscriminator::FeesVault + } +} + +impl FeesVault { + pub fn size_with_discriminator(&self) -> usize { + 8 + 32 + } +} + +impl_to_bytes_with_discriminator_borsh!(FeesVault); +impl_try_from_bytes_with_discriminator_borsh!(FeesVault); diff --git a/src/state/mod.rs b/src/state/mod.rs index 59f89366..c86e3b2f 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -1,11 +1,13 @@ mod commit_record; mod delegation_metadata; mod delegation_record; +mod fees_vault; mod program_config; mod utils; pub use commit_record::*; pub use delegation_metadata::*; pub use delegation_record::*; +pub use fees_vault::*; pub use program_config::*; pub use utils::*; diff --git a/src/state/utils/discriminator.rs b/src/state/utils/discriminator.rs index 39989b02..44dc9449 100644 --- a/src/state/utils/discriminator.rs +++ b/src/state/utils/discriminator.rs @@ -7,6 +7,7 @@ pub enum AccountDiscriminator { DelegationMetadata = 102, CommitRecord = 101, ProgramConfig = 103, + FeesVault = 104, } impl AccountDiscriminator { diff --git a/tests/integration/package.json b/tests/integration/package.json index 4a58c8ce..ca002506 100644 --- a/tests/integration/package.json +++ b/tests/integration/package.json @@ -20,4 +20,4 @@ "typescript": "^4.3.5" }, "packageManager": "yarn@1.22.19+sha1.4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447" -} +} \ No newline at end of file diff --git a/tests/integration/programs/test-delegation/Cargo.toml b/tests/integration/programs/test-delegation/Cargo.toml index ad93c701..1681d000 100644 --- a/tests/integration/programs/test-delegation/Cargo.toml +++ b/tests/integration/programs/test-delegation/Cargo.toml @@ -15,6 +15,9 @@ no-log-ix-name = [] cpi = ["no-entrypoint"] default = [] idl-build = ["anchor-lang/idl-build"] +anchor-debug = [] +custom-panic = [] +custom-heap = [] [dependencies] anchor-lang = "0.31.1" diff --git a/tests/integration/programs/test-delegation/src/lib.rs b/tests/integration/programs/test-delegation/src/lib.rs index 4fd33cc1..d9621f10 100644 --- a/tests/integration/programs/test-delegation/src/lib.rs +++ b/tests/integration/programs/test-delegation/src/lib.rs @@ -36,7 +36,10 @@ pub mod test_delegation { ctx.accounts.delegate_pda( &ctx.accounts.payer, &[TEST_PDA_SEED], - DelegateConfig::default(), + DelegateConfig { + commit_frequency_ms: u32::MAX, + validator: Some(ctx.accounts.validator.key()), + }, )?; Ok(()) } @@ -46,12 +49,18 @@ pub mod test_delegation { ctx.accounts.delegate_pda( &ctx.accounts.payer, &[TEST_PDA_SEED], - DelegateConfig::default(), + DelegateConfig { + commit_frequency_ms: u32::MAX, + validator: Some(ctx.accounts.validator.key()), + }, )?; ctx.accounts.delegate_pda_other( &ctx.accounts.payer, &[TEST_PDA_SEED_OTHER], - DelegateConfig::default(), + DelegateConfig { + commit_frequency_ms: u32::MAX, + validator: Some(ctx.accounts.validator.key()), + }, )?; msg!( "Delegated {:?}, owner {:?}", @@ -134,6 +143,8 @@ pub fn transfer_from_undelegated( #[derive(Accounts)] pub struct DelegateInput<'info> { pub payer: Signer<'info>, + /// CHECK: The validator account + pub validator: AccountInfo<'info>, /// CHECK: The pda to delegate #[account(mut, del, seeds = [TEST_PDA_SEED], bump)] pub pda: AccountInfo<'info>, @@ -143,6 +154,8 @@ pub struct DelegateInput<'info> { #[derive(Accounts)] pub struct DelegateInputTwo<'info> { pub payer: Signer<'info>, + /// CHECK: The validator account + pub validator: AccountInfo<'info>, /// CHECK: The pda to delegate #[account(mut, del, seeds = [TEST_PDA_SEED], bump)] pub pda: AccountInfo<'info>, diff --git a/tests/integration/tests/test-delegation.ts b/tests/integration/tests/test-delegation.ts index c27f1bfd..303e63e0 100644 --- a/tests/integration/tests/test-delegation.ts +++ b/tests/integration/tests/test-delegation.ts @@ -9,6 +9,7 @@ import { DELEGATION_PROGRAM_ID, } from "@magicblock-labs/ephemeral-rollups-sdk"; import { ON_CURVE_ACCOUNT } from "./fixtures/consts"; +import { SYSTEM_PROGRAM_ID } from "@coral-xyz/anchor/dist/cjs/native/system"; import { assert } from "chai"; const SEED_TEST_PDA = "test-pda"; @@ -64,12 +65,6 @@ describe("TestDelegation", () => { console.log("Claim validator fee vault tx:", txId); }); - it("Claim protocol fees", async () => { - const ix = createClaimProtocolFeesVaultInstruction(admin); - const txId = await processInstruction(ix); - console.log("Claim protocol fee vault tx:", txId); - }); - it("Initializes the counter", async () => { // Check if the counter is initialized const counterAccountInfo = await provider.connection.getAccountInfo(pda); @@ -165,6 +160,7 @@ describe("TestDelegation", () => { .delegateTwo() .accounts({ payer: provider.wallet.publicKey, + validator, }) .rpc({ skipPreflight: true }); console.log("Your transaction signature", tx); @@ -216,7 +212,7 @@ describe("TestDelegation", () => { slot: new anchor.BN(1), lamports: new anchor.BN(1000000000), allow_undelegation: false, - data: new_data, + data: Uint8Array.from(new_data), }; const ix = createCommitAccountInstruction( validator, @@ -268,7 +264,7 @@ describe("TestDelegation", () => { slot: new anchor.BN(2), lamports: new anchor.BN(1000000000), allow_undelegation: true, - data: new_data, + data: Uint8Array.from(new_data), }; const ix = createCommitAccountInstruction( validator, @@ -321,6 +317,18 @@ describe("TestDelegation", () => { console.log("Whitelist a validator for a program:", txId); }); + it("Set fees receiver", async () => { + const ix = createSetFeesReceiverInstruction(admin, admin); + const txId = await processInstruction(ix); + console.log("Set fees receiver tx:", txId); + }); + + it("Claim protocol fees", async () => { + const ix = createClaimProtocolFeesVaultInstruction(admin); + const txId = await processInstruction(ix); + console.log("Claim protocol fees tx:", txId); + }); + async function processInstruction(ix: web3.TransactionInstruction) { const tx = new web3.Transaction().add(ix); tx.recentBlockhash = ( @@ -482,9 +490,14 @@ describe("TestDelegation", () => { /// Instruction to initialize protocol fees vault function createInitFeesVaultInstruction(payer: web3.PublicKey) { const feesVault = feesVaultPda(); + const delegationProgramData = web3.PublicKey.findProgramAddressSync( + [DELEGATION_PROGRAM_ID.toBuffer()], + BPF_LOADER + )[0]; const keys = [ { pubkey: payer, isSigner: true, isWritable: true }, { pubkey: feesVault, isSigner: false, isWritable: true }, + { pubkey: delegationProgramData, isSigner: false, isWritable: false }, { pubkey: web3.SystemProgram.programId, isSigner: false, @@ -550,8 +563,11 @@ describe("TestDelegation", () => { return ix; } - /// Instruction to claim fees from the protocol vault - function createClaimProtocolFeesVaultInstruction(admin: web3.PublicKey) { + /// Instruction to set fees receiver + function createSetFeesReceiverInstruction( + admin: web3.PublicKey, + feesReceiver: web3.PublicKey + ) { const feesVault = feesVaultPda(); const delegationProgramData = web3.PublicKey.findProgramAddressSync( [DELEGATION_PROGRAM_ID.toBuffer()], @@ -560,7 +576,28 @@ describe("TestDelegation", () => { const keys = [ { pubkey: admin, isSigner: true, isWritable: true }, { pubkey: feesVault, isSigner: false, isWritable: true }, - { pubkey: delegationProgramData, isSigner: false, isWritable: true }, + { pubkey: delegationProgramData, isSigner: false, isWritable: false }, + { pubkey: SYSTEM_PROGRAM_ID, isSigner: false, isWritable: false }, + ]; + const data = Buffer.from( + [16, 0, 0, 0, 0, 0, 0, 0].concat([...feesReceiver.toBytes()]) + ); + const ix = new web3.TransactionInstruction({ + programId: new web3.PublicKey(DELEGATION_PROGRAM_ID), + keys, + data, + }); + return ix; + } + + /// Instruction to claim fees from the protocol vault + function createClaimProtocolFeesVaultInstruction( + feesReceiver: web3.PublicKey + ) { + const feesVault = feesVaultPda(); + const keys = [ + { pubkey: feesVault, isSigner: false, isWritable: true }, + { pubkey: feesReceiver, isSigner: false, isWritable: true }, ]; const data = Buffer.from([12, 0, 0, 0, 0, 0, 0, 0, 0]); const ix = new web3.TransactionInstruction({ diff --git a/tests/test_protocol_claim_fees.rs b/tests/test_protocol_claim_fees.rs index a750703f..235762aa 100644 --- a/tests/test_protocol_claim_fees.rs +++ b/tests/test_protocol_claim_fees.rs @@ -1,10 +1,14 @@ -use crate::fixtures::TEST_AUTHORITY; -use dlp::pda::fees_vault_pda; +use crate::fixtures::{create_program_config_data, TEST_AUTHORITY}; +use dlp::pda::{fees_vault_pda, program_config_from_program_id}; +use dlp::state::FeesVault; use solana_program::rent::Rent; use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, system_program}; -use solana_program_test::{BanksClient, ProgramTest}; +use solana_program_test::{BanksClient, BanksClientError, ProgramTest}; +use solana_sdk::instruction::InstructionError; +use solana_sdk::transaction::TransactionError; use solana_sdk::{ account::Account, + pubkey::Pubkey, signature::{Keypair, Signer}, transaction::Transaction, }; @@ -20,28 +24,223 @@ async fn test_protocol_claim_fees() { // Submit the claim fees tx let ix = dlp::instruction_builder::protocol_claim_fees(admin.pubkey()); - let tx = Transaction::new_signed_with_payer( - &[ix], - Some(&payer.pubkey()), - &[&payer, &admin], - blockhash, - ); + let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash); let res = banks.process_transaction(tx).await; assert!(res.is_ok()); // Assert that fees vault now only have the rent exemption amount + let min_rent = Rent::default().minimum_balance(FeesVault::default().size_with_discriminator()); let fees_vault_account = banks.get_account(fees_vault_pda).await.unwrap(); assert!(fees_vault_account.is_some()); - assert_eq!( - fees_vault_account.unwrap().lamports, - Rent::default().minimum_balance(8) - ); + assert_eq!(fees_vault_account.unwrap().lamports, min_rent); // Assert that the admin account now has the fees let admin_account = banks.get_account(admin.pubkey()).await.unwrap(); assert_eq!( admin_account.unwrap().lamports, - LAMPORTS_PER_SOL * 2 - Rent::default().minimum_balance(8) + LAMPORTS_PER_SOL * 2 - min_rent + ); + + // Verify FeesVault still stores the admin as receiver + let data = banks + .get_account(fees_vault_pda) + .await + .unwrap() + .unwrap() + .data; + let vault = FeesVault::try_from_bytes_with_discriminator(&data).unwrap(); + assert_eq!(vault.fees_receiver, admin.pubkey()); +} + +#[tokio::test] +async fn test_protocol_claim_fees_wrong_receiver() { + // Setup + let (banks, payer, admin, blockhash) = setup_program_test_env().await; + + // Submit the claim fees tx with wrong receiver + let wrong_receiver = Pubkey::new_unique(); + let ix = dlp::instruction_builder::protocol_claim_fees(wrong_receiver); + let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash); + let res = banks.process_transaction(tx).await; + + // Assert that the transaction fails because fees_receiver doesn't match stored value + assert!( + matches!( + res, + Err(BanksClientError::TransactionError( + TransactionError::InstructionError(0, InstructionError::InvalidAccountData) + )) + ), + "Expected InvalidAccountData error, got {res:?}", + ); + + // Assert that the fees vault still has the initial lamports + let fees_vault_pda = fees_vault_pda(); + let vault_after = banks.get_account(fees_vault_pda).await.unwrap().unwrap(); + assert_eq!(vault_after.lamports, LAMPORTS_PER_SOL); + + // Assert that the admin account still has the initial lamports + let admin_account = banks.get_account(admin.pubkey()).await.unwrap().unwrap(); + assert_eq!(admin_account.lamports, LAMPORTS_PER_SOL); + + // Assert that the fees receiver account still has the initial lamports + let fees_receiver_account = banks.get_account(wrong_receiver).await.unwrap(); + assert_eq!(fees_receiver_account, None); +} + +#[tokio::test] +async fn test_protocol_claim_fees_self() { + // Setup + let (banks, payer, admin, blockhash) = setup_program_test_env().await; + + // Set fees receiver to fees vault + let fees_receiver = fees_vault_pda(); + let ix = dlp::instruction_builder::set_fees_receiver(admin.pubkey(), fees_receiver); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer, &admin], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); + + let ix = dlp::instruction_builder::protocol_claim_fees(fees_receiver); + let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash); + let res = banks.process_transaction(tx).await; + + // Assert that the transaction fails because fees_receiver is the same as the fees vault + assert!( + matches!( + res, + Err(BanksClientError::TransactionError( + TransactionError::InstructionError(0, InstructionError::InvalidArgument) + )) + ), + "Expected InvalidArgument error, got {res:?}", + ); + + // Assert that the admin account still has the initial lamports + let admin_account = banks.get_account(admin.pubkey()).await.unwrap().unwrap(); + assert_eq!(admin_account.lamports, LAMPORTS_PER_SOL); + + // Assert that the fees vault still has the initial lamports + let fees_vault_pda = fees_vault_pda(); + let fees_vault_account = banks.get_account(fees_vault_pda).await.unwrap().unwrap(); + assert_eq!(fees_vault_account.lamports, LAMPORTS_PER_SOL); +} + +#[tokio::test] +async fn test_protocol_claim_fees_noop() { + let (banks, payer, admin, blockhash) = setup_program_test_env().await; + let fees_vault = fees_vault_pda(); + + // First claim: drain to min rent + let ix = dlp::instruction_builder::protocol_claim_fees(admin.pubkey()); + let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash); + banks.process_transaction(tx).await.unwrap(); + + // Snapshot balances + let vault_before = banks + .get_account(fees_vault) + .await + .unwrap() + .unwrap() + .lamports; + let admin_before = banks + .get_account(admin.pubkey()) + .await + .unwrap() + .unwrap() + .lamports; + + // Second claim: should be a no-op and return Ok(()) + let ix = dlp::instruction_builder::protocol_claim_fees(admin.pubkey()); + let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); + + let vault_after = banks + .get_account(fees_vault) + .await + .unwrap() + .unwrap() + .lamports; + let admin_after = banks + .get_account(admin.pubkey()) + .await + .unwrap() + .unwrap() + .lamports; + assert_eq!(vault_after, vault_before); + assert_eq!(admin_after, admin_before); +} + +#[tokio::test] +async fn test_protocol_claim_fees_insufficient_funds() { + // Custom env: vault funded below min rent + use solana_program_test::ProgramTest; + use solana_sdk::{account::Account, system_program}; + let mut program_test = ProgramTest::new("dlp", dlp::ID, None); + program_test.prefer_bpf(true); + + let admin = Keypair::from_bytes(&TEST_AUTHORITY).unwrap(); + program_test.add_account( + admin.pubkey(), + Account { + lamports: LAMPORTS_PER_SOL, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Prepare FeesVault with correct data but underfunded + let mut buf = vec![]; + FeesVault { + fees_receiver: admin.pubkey(), + } + .to_bytes_with_discriminator(&mut buf) + .unwrap(); + let min_rent = Rent::default().minimum_balance(buf.len()); + let underfunded = min_rent.saturating_sub(1); + program_test.add_account( + fees_vault_pda(), + Account { + lamports: underfunded, + data: buf, + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Program config (not used by processor here but commonly present) + program_test.add_account( + program_config_from_program_id(&dlp::ID), + Account { + lamports: LAMPORTS_PER_SOL, + data: create_program_config_data(Pubkey::new_unique()), + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let (banks, payer, blockhash) = program_test.start().await; + + let ix = dlp::instruction_builder::protocol_claim_fees(admin.pubkey()); + let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash); + let res = banks.process_transaction(tx).await; + assert!( + matches!( + res, + Err(BanksClientError::TransactionError( + TransactionError::InstructionError(0, InstructionError::InsufficientFunds) + )) + ), + "Expected InsufficientFunds error, got {res:?}", ); } @@ -63,11 +262,29 @@ async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { ); // Setup the fees vault account + let mut buffer = vec![]; + FeesVault { + fees_receiver: admin_keypair.pubkey(), + } + .to_bytes_with_discriminator(&mut buffer) + .unwrap(); program_test.add_account( fees_vault_pda(), Account { lamports: LAMPORTS_PER_SOL, - data: vec![], + data: buffer, + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Setup the fees program config account + program_test.add_account( + program_config_from_program_id(&dlp::ID), + Account { + lamports: LAMPORTS_PER_SOL, + data: create_program_config_data(Pubkey::new_unique()), owner: dlp::id(), executable: false, rent_epoch: 0, diff --git a/tests/test_set_fees_receiver.rs b/tests/test_set_fees_receiver.rs new file mode 100644 index 00000000..23fdfd77 --- /dev/null +++ b/tests/test_set_fees_receiver.rs @@ -0,0 +1,266 @@ +use std::collections::BTreeSet; +use std::vec; + +use crate::fixtures::{create_program_config_data, TEST_AUTHORITY}; +use borsh::{BorshDeserialize, BorshSerialize}; +use dlp::error::DlpError; +use dlp::pda::{fees_vault_pda, program_config_from_program_id}; +use dlp::state::discriminator::{AccountDiscriminator, AccountWithDiscriminator}; +use dlp::state::FeesVault; +use dlp::{impl_to_bytes_with_discriminator_borsh, impl_try_from_bytes_with_discriminator_borsh}; +use solana_program::rent::Rent; +use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, system_program}; +use solana_program_test::{BanksClient, BanksClientError, ProgramTest}; +use solana_sdk::instruction::InstructionError; +use solana_sdk::pubkey::Pubkey; +use solana_sdk::transaction::TransactionError; +use solana_sdk::{ + account::Account, + signature::{Keypair, Signer}, + transaction::Transaction, +}; + +mod fixtures; + +#[derive(BorshSerialize, BorshDeserialize)] +struct OldProgramConfig { + approved_validators: BTreeSet, +} + +impl AccountWithDiscriminator for OldProgramConfig { + fn discriminator() -> AccountDiscriminator { + AccountDiscriminator::ProgramConfig + } +} + +impl_to_bytes_with_discriminator_borsh!(OldProgramConfig); +impl_try_from_bytes_with_discriminator_borsh!(OldProgramConfig); + +#[tokio::test] +async fn test_set_fees_receiver() { + // Setup + let (banks, payer, admin, blockhash) = setup_program_test_env(false).await; + + helper_test_set_fees_receiver(&banks, &payer, &admin, blockhash).await; +} + +#[tokio::test] +async fn test_set_fees_receiver_unauthorized() { + // Setup + let (banks, payer, _admin, blockhash) = setup_program_test_env(false).await; + + let fees_receiver: Pubkey = Pubkey::new_unique(); + let unauthorized_admin = Keypair::new(); + + // Set the fees receiver to a new account + let ix = + dlp::instruction_builder::set_fees_receiver(unauthorized_admin.pubkey(), fees_receiver); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer, &unauthorized_admin], + blockhash, + ); + let res = banks.process_transaction(tx).await; + let expected_code = DlpError::Unauthorized as u32; + assert!( + matches!( + res, + Err(BanksClientError::TransactionError( + TransactionError::InstructionError( + 0, + InstructionError::Custom(code), + ) + )) if code == expected_code, + ), + "Expected Unauthorized error, got {res:?}", + ); +} + +#[tokio::test] +async fn test_set_fees_receiver_migration() { + // Setup + let (banks, payer, admin, blockhash) = setup_program_test_env_old_fees_vault(true).await; + + helper_test_set_fees_receiver(&banks, &payer, &admin, blockhash).await; +} + +async fn setup_program_test_env(migrate: bool) -> (BanksClient, Keypair, Keypair, Hash) { + // Setup the fees vault account + let mut buffer = vec![]; + FeesVault { + fees_receiver: Pubkey::new_unique(), + } + .to_bytes_with_discriminator(&mut buffer) + .unwrap(); + base_setup_program_test_env(migrate, buffer).await +} + +async fn setup_program_test_env_old_fees_vault( + migrate: bool, +) -> (BanksClient, Keypair, Keypair, Hash) { + base_setup_program_test_env(migrate, vec![]).await +} + +async fn base_setup_program_test_env( + migrate: bool, + fees_vault_data: Vec, +) -> (BanksClient, Keypair, Keypair, Hash) { + let mut program_test = ProgramTest::new("dlp", dlp::ID, None); + program_test.prefer_bpf(true); + + let admin_keypair = Keypair::from_bytes(&TEST_AUTHORITY).unwrap(); + + program_test.add_account( + admin_keypair.pubkey(), + Account { + lamports: LAMPORTS_PER_SOL, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Setup the fees vault account + program_test.add_account( + fees_vault_pda(), + Account { + lamports: LAMPORTS_PER_SOL, + data: fees_vault_data, + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Setup the fees program config account + let data = if migrate { + let mut program_config = OldProgramConfig { + approved_validators: BTreeSet::new(), + }; + program_config + .approved_validators + .insert(Pubkey::new_unique()); + let mut bytes = vec![]; + program_config + .to_bytes_with_discriminator(&mut bytes) + .unwrap(); + bytes + } else { + create_program_config_data(Pubkey::new_unique()) + }; + program_test.add_account( + program_config_from_program_id(&dlp::ID), + Account { + lamports: LAMPORTS_PER_SOL, + data, + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let (banks, payer, blockhash) = program_test.start().await; + (banks, payer, admin_keypair, blockhash) +} + +async fn helper_test_set_fees_receiver( + banks: &BanksClient, + payer: &Keypair, + admin: &Keypair, + blockhash: Hash, +) { + let fees_vault_pda = fees_vault_pda(); + + let fees_receiver = Pubkey::new_unique(); + + // Set the fees receiver to a new account + let ix = dlp::instruction_builder::set_fees_receiver(admin.pubkey(), fees_receiver); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer, &admin], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); + + let vault_before = banks + .get_account(fees_vault_pda) + .await + .unwrap() + .unwrap() + .lamports; + let admin_before = banks + .get_account(admin.pubkey()) + .await + .unwrap() + .unwrap() + .lamports; + + // Try claiming to the wrong fees receiver + let ix = dlp::instruction_builder::protocol_claim_fees(admin.pubkey()); + let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash); + let res = banks.process_transaction(tx).await; + assert!( + matches!( + res, + Err(BanksClientError::TransactionError( + TransactionError::InstructionError(0, InstructionError::InvalidAccountData) + )) + ), + "Expected InvalidAccountData error, got {res:?}", + ); + + // State unchanged + let vault_after = banks + .get_account(fees_vault_pda) + .await + .unwrap() + .unwrap() + .lamports; + let admin_after = banks + .get_account(admin.pubkey()) + .await + .unwrap() + .unwrap() + .lamports; + assert_eq!( + vault_after, vault_before, + "vault lamports changed on failure" + ); + assert_eq!( + admin_after, admin_before, + "admin lamports changed on failure" + ); + + // Claim to the correct fees receiver + let ix = dlp::instruction_builder::protocol_claim_fees(fees_receiver); + let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); + + // Assert that fees vault now only have the rent exemption amount + let min_rent = Rent::default().minimum_balance(FeesVault::default().size_with_discriminator()); + let fees_vault_account = banks.get_account(fees_vault_pda).await.unwrap(); + assert!(fees_vault_account.is_some()); + assert_eq!(fees_vault_account.unwrap().lamports, min_rent); + + // Assert that the fees receiver account now has the fees + let fees_receiver_account = banks.get_account(fees_receiver).await.unwrap(); + assert_eq!( + fees_receiver_account.unwrap().lamports, + LAMPORTS_PER_SOL - min_rent + ); + + // Assert that FeesVault deserializes correctly and stores the right fees_receiver + let data = banks + .get_account(fees_vault_pda) + .await + .unwrap() + .unwrap() + .data; + let vault = FeesVault::try_from_bytes_with_discriminator(&data).unwrap(); + assert_eq!(Pubkey::from(vault.fees_receiver), fees_receiver); +}