From 681bf71fd2b3c748c25857a66e4e120ca82396d1 Mon Sep 17 00:00:00 2001 From: JKrishnaD Date: Mon, 29 Dec 2025 19:40:02 +0530 Subject: [PATCH] fix: prevent undelegation of closed delegated accounts --- src/error.rs | 2 + src/processor/fast/undelegate.rs | 7 ++ tests/test_undelegate_close_account.rs | 137 +++++++++++++++++++++++++ 3 files changed, 146 insertions(+) create mode 100644 tests/test_undelegate_close_account.rs diff --git a/src/error.rs b/src/error.rs index 6d43ba5..62c4f18 100644 --- a/src/error.rs +++ b/src/error.rs @@ -87,6 +87,8 @@ pub enum DlpError { InvalidDiscriminator = 39, #[error("Invalid delegation record deserialization")] InvalidDelegationRecordData = 40, + #[error("Delegated account is already closed")] + DelegateAccountClosed = 41, #[error("An infallible error is encountered possibly due to logic error")] InfallibleError = 100, } diff --git a/src/processor/fast/undelegate.rs b/src/processor/fast/undelegate.rs index e054abe..0931fa7 100644 --- a/src/processor/fast/undelegate.rs +++ b/src/processor/fast/undelegate.rs @@ -68,6 +68,7 @@ use super::{ /// /// - Close the delegation metadata /// - Close the delegation record +/// - If delegated account is closed then stop the process /// - If delegated account has no data, assign to prev owner (and stop here) /// - If there's data, create an "undelegate_buffer" and store the data in it /// - Close the original delegated account @@ -87,6 +88,12 @@ pub fn process_undelegate( return Err(ProgramError::NotEnoughAccountKeys); }; + // Reject undelegation of closed account + if delegated_account.lamports() == 0 && delegated_account.data_is_empty() { + log!("Delegated account is already closed"); + return Err(DlpError::DelegateAccountClosed.into()); + } + // Check accounts require_signer(validator, "validator")?; require_owned_pda(delegated_account, &crate::fast::ID, "delegated account")?; diff --git a/tests/test_undelegate_close_account.rs b/tests/test_undelegate_close_account.rs new file mode 100644 index 0000000..868d79a --- /dev/null +++ b/tests/test_undelegate_close_account.rs @@ -0,0 +1,137 @@ +use dlp::pda::{ + delegation_metadata_pda_from_delegated_account, delegation_record_pda_from_delegated_account, + fees_vault_pda, validator_fees_vault_pda_from_validator, +}; +use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, rent::Rent, system_program}; +use solana_program_test::{read_file, BanksClient, ProgramTest}; +use solana_sdk::{ + account::Account, + signature::{Keypair, Signer}, + transaction::Transaction, +}; + +use crate::fixtures::{ + get_delegation_metadata_data, get_delegation_record_data, DELEGATED_PDA_ID, + DELEGATED_PDA_OWNER_ID, TEST_AUTHORITY, +}; + +mod fixtures; + +#[tokio::test] +async fn test_undelegate_close_account() { + // Setup + let (banks, _, authority, blockhash) = setup_program_test_env().await; + + // Create the undelegate tx + let ix_undelegate = dlp::instruction_builder::undelegate( + authority.pubkey(), + DELEGATED_PDA_ID, + DELEGATED_PDA_OWNER_ID, + authority.pubkey(), + ); + + // Submit the transaction + let tx = Transaction::new_signed_with_payer( + &[ix_undelegate], + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_err(), "Delegate account is already closed"); +} + +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 authority = Keypair::from_bytes(&TEST_AUTHORITY).unwrap(); + + program_test.add_account( + authority.pubkey(), + Account { + lamports: LAMPORTS_PER_SOL, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Setup a Closed delegated PDA + program_test.add_account( + DELEGATED_PDA_ID, + Account { + lamports: 0, + data: vec![], + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Setup the delegated 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, + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Setup the delegated 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, + }, + ); + + // Setup program to test undelegation + 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()), + data, + owner: solana_sdk::bpf_loader::id(), + executable: true, + rent_epoch: 0, + }, + ); + + // 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(&authority.pubkey()), + Account { + lamports: LAMPORTS_PER_SOL, + data: vec![], + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let (banks, payer, blockhash) = program_test.start().await; + (banks, payer, authority, blockhash) +}