From cc8f6a8d1215a983419f151e8c7158175b34dc54 Mon Sep 17 00:00:00 2001 From: taco-paco Date: Tue, 6 May 2025 13:43:16 +0900 Subject: [PATCH 1/7] fix: fixed undelegation of ephemeral balance. Now undelegated to system program. Added new version of close_ephemeral_balance to support it. kept old one for backward-compatability --- src/discriminator.rs | 9 +- .../close_ephemeral_balance.rs | 8 +- src/instruction_builder/mod.rs | 2 + .../undelegate_ephemeral_balance.rs | 18 +++ src/lib.rs | 6 + src/processor/close_ephemeral_balance_v1.rs | 72 +++++++++++ src/processor/mod.rs | 4 + src/processor/top_up_ephemeral_balance.rs | 2 +- src/processor/undelegate_ephemeral_balance.rs | 74 +++++++++++ tests/test_top_up.rs | 120 ++++++++++++------ 10 files changed, 274 insertions(+), 41 deletions(-) create mode 100644 src/instruction_builder/undelegate_ephemeral_balance.rs create mode 100644 src/processor/close_ephemeral_balance_v1.rs create mode 100644 src/processor/undelegate_ephemeral_balance.rs diff --git a/src/discriminator.rs b/src/discriminator.rs index e896fa2..197a1b9 100644 --- a/src/discriminator.rs +++ b/src/discriminator.rs @@ -33,6 +33,10 @@ pub enum DlpDiscriminator { CommitStateFromBuffer = 13, /// See [crate::processor::process_close_validator_fees_vault] for docs. CloseValidatorFeesVault = 14, + /// See [crate::processor::process_undelegate_ephemeral_balance] for docs. + UndelegateEphemeralBalance = 15, + /// See [crate::processor::process_close_ephemeral_balance_v1] for docs. + CloseEphemeralBalanceV1 = 16 } impl DlpDiscriminator { @@ -45,7 +49,8 @@ impl DlpDiscriminator { impl TryFrom<[u8; 8]> for DlpDiscriminator { type Error = ProgramError; fn try_from(bytes: [u8; 8]) -> Result { - match bytes[0] { + let discriminator = u64::from_le_bytes(bytes); + match discriminator { 0x0 => Ok(DlpDiscriminator::Delegate), 0x1 => Ok(DlpDiscriminator::CommitState), 0x2 => Ok(DlpDiscriminator::Finalize), @@ -60,6 +65,8 @@ impl TryFrom<[u8; 8]> for DlpDiscriminator { 0xc => Ok(DlpDiscriminator::ProtocolClaimFees), 0xd => Ok(DlpDiscriminator::CommitStateFromBuffer), 0xe => Ok(DlpDiscriminator::CloseValidatorFeesVault), + 0xf => Ok(DlpDiscriminator::UndelegateEphemeralBalance), + 0x10 => Ok(DlpDiscriminator::CloseEphemeralBalanceV1), _ => Err(ProgramError::InvalidInstructionData), } } diff --git a/src/instruction_builder/close_ephemeral_balance.rs b/src/instruction_builder/close_ephemeral_balance.rs index dece09b..153fdfa 100644 --- a/src/instruction_builder/close_ephemeral_balance.rs +++ b/src/instruction_builder/close_ephemeral_balance.rs @@ -1,11 +1,12 @@ 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; /// Creates instruction to close an ephemeral balance account -/// See [crate::processor::process_close_ephemeral_balance] for docs. +/// See [crate::processor::process_close_ephemeral_balance_v1] for docs. +/// [crate::processor::process_close_ephemeral_balance] now deprecated pub fn close_ephemeral_balance(payer: Pubkey, index: u8) -> Instruction { let ephemeral_balance_pda = ephemeral_balance_pda_from_payer(&payer, index); Instruction { @@ -13,9 +14,10 @@ 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(), + DlpDiscriminator::CloseEphemeralBalanceV1.to_vec(), vec![index], ] .concat(), diff --git a/src/instruction_builder/mod.rs b/src/instruction_builder/mod.rs index e0cae79..d174d6d 100644 --- a/src/instruction_builder/mod.rs +++ b/src/instruction_builder/mod.rs @@ -11,6 +11,7 @@ mod init_validator_fees_vault; mod protocol_claim_fees; mod top_up_ephemeral_balance; mod undelegate; +mod undelegate_ephemeral_balance; mod validator_claim_fees; mod whitelist_validator_for_program; @@ -26,5 +27,6 @@ pub use init_validator_fees_vault::*; pub use protocol_claim_fees::*; pub use top_up_ephemeral_balance::*; pub use undelegate::*; +pub use undelegate_ephemeral_balance::*; pub use validator_claim_fees::*; pub use whitelist_validator_for_program::*; diff --git a/src/instruction_builder/undelegate_ephemeral_balance.rs b/src/instruction_builder/undelegate_ephemeral_balance.rs new file mode 100644 index 0000000..fec70f1 --- /dev/null +++ b/src/instruction_builder/undelegate_ephemeral_balance.rs @@ -0,0 +1,18 @@ +use crate::discriminator::DlpDiscriminator; +use crate::instruction_builder::undelegate; +use solana_program::instruction::Instruction; +use solana_program::pubkey::Pubkey; + +/// Builds an undelegate instruction. +/// See [crate::processor::process_undelegate] for docs. +#[allow(clippy::too_many_arguments)] +pub fn undelegate_ephemeral_balance( + validator: Pubkey, + delegated_account: Pubkey, + rent_reimbursement: Pubkey, +) -> Instruction { + let mut ix = undelegate(validator, delegated_account, crate::ID, rent_reimbursement); + ix.data = DlpDiscriminator::UndelegateEphemeralBalance.to_vec(); + + ix +} diff --git a/src/lib.rs b/src/lib.rs index 4b1eca8..a8fc29a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -93,6 +93,12 @@ pub fn process_instruction( discriminator::DlpDiscriminator::CloseValidatorFeesVault => { processor::process_close_validator_fees_vault(program_id, accounts, data)? } + discriminator::DlpDiscriminator::UndelegateEphemeralBalance => { + processor::process_undelegate_ephemeral_balance(program_id, accounts, data)? + } + discriminator::DlpDiscriminator::CloseEphemeralBalanceV1 => { + processor::process_close_ephemeral_balance_v1(program_id, accounts, data)? + } } Ok(()) } diff --git a/src/processor/close_ephemeral_balance_v1.rs b/src/processor/close_ephemeral_balance_v1.rs new file mode 100644 index 0000000..3e371c3 --- /dev/null +++ b/src/processor/close_ephemeral_balance_v1.rs @@ -0,0 +1,72 @@ +use crate::ephemeral_balance_seeds_from_payer; +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::system_instruction::transfer; +use solana_program::{ + account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey, system_program, +}; + +/// Process the closing of an ephemeral balance account +/// +/// Accounts: +/// +/// 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: +/// +/// - ephemeral balance account is initialized +/// +/// Steps: +/// +/// 1. Closes the ephemeral balance account and refunds the payer with the +/// escrowed lamports +pub fn process_close_ephemeral_balance_v1( + _program_id: &Pubkey, + accounts: &[AccountInfo], + data: &[u8], +) -> ProgramResult { + let index = *data.first().ok_or(ProgramError::InvalidInstructionData)?; + + // Load Accounts + let [payer, ephemeral_balance_account, system_program] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + load_signer(payer, "payer")?; + + 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, + &crate::id(), + true, + "ephemeral balance", + )?; + if ephemeral_balance_account.owner != &system_program::id() { + msg!( + "ephemeral balance expected to be owned by system program: {}", + system_program::id() + ); + return Err(ProgramError::InvalidAccountOwner); + } + + let amount = ephemeral_balance_account.lamports(); + 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/mod.rs b/src/processor/mod.rs index e9b25db..43eda89 100644 --- a/src/processor/mod.rs +++ b/src/processor/mod.rs @@ -1,4 +1,5 @@ mod close_ephemeral_balance; +mod close_ephemeral_balance_v1; mod close_validator_fees_vault; mod commit_state; mod commit_state_from_buffer; @@ -10,11 +11,13 @@ mod init_validator_fees_vault; mod protocol_claim_fees; mod top_up_ephemeral_balance; mod undelegate; +mod undelegate_ephemeral_balance; mod utils; mod validator_claim_fees; mod whitelist_validator_for_program; pub use close_ephemeral_balance::*; +pub use close_ephemeral_balance_v1::*; pub use close_validator_fees_vault::*; pub use commit_state::*; pub use commit_state_from_buffer::*; @@ -26,5 +29,6 @@ pub use init_validator_fees_vault::*; pub use protocol_claim_fees::*; pub use top_up_ephemeral_balance::*; pub use undelegate::*; +pub use undelegate_ephemeral_balance::*; pub use validator_claim_fees::*; pub use whitelist_validator_for_program::*; 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/processor/undelegate_ephemeral_balance.rs b/src/processor/undelegate_ephemeral_balance.rs new file mode 100644 index 0000000..7179b77 --- /dev/null +++ b/src/processor/undelegate_ephemeral_balance.rs @@ -0,0 +1,74 @@ +use crate::instruction_builder::undelegate; +use solana_program::msg; +use solana_program::program::invoke; +use solana_program::program_error::ProgramError; +use solana_program::{ + account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey, system_program, +}; + +/// Undelegate ephemeral balance +/// +/// Accounts: +/// +/// 0: `[signer]` the validator account +/// 1: `[writable]` the delegated account +/// 2: `[]` the owner program of the delegated account +/// 3: `[writable]` the undelegate buffer PDA we use to store the data temporarily +/// 4: `[]` the commit state PDA +/// 5: `[]` the commit record PDA +/// 6: `[writable]` the delegation record PDA +/// 7: `[writable]` the delegation metadata PDA +/// 8: `[]` the rent reimbursement account +/// 9: `[writable]` the protocol fees vault account +/// 10: `[writable]` the validator fees vault account +/// 11: `[]` the system program +/// +/// Requirements: +/// +/// - delegated account is owned by delegation program +/// - delegation record is initialized +/// - delegation metadata is initialized +/// - protocol fees vault is initialized +/// - validator fees vault is initialized +/// - commit state is uninitialized +/// - commit record is uninitialized +/// - delegated account is NOT undelegatable +/// - owner program account matches the owner in the delegation record +/// - rent reimbursement account matches the rent payer in the delegation metadata +/// +/// Steps: +/// +/// - Undelegate using CPI into [`crate::processor::undelegate`] +/// - Assigns ownership back to system program +pub fn process_undelegate_ephemeral_balance( + _program_id: &Pubkey, + accounts: &[AccountInfo], + _data: &[u8], +) -> ProgramResult { + let [validator, delegated_account, owner_program, _, _, _, _, _, rent_reimbursement, _, _, _] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + if owner_program.key != &crate::ID { + msg!( + "Unexpected owner program. expected dlp, got: {}", + owner_program.key + ); + return Err(ProgramError::IncorrectProgramId); + } + + // Propagate to undelegate which also runs all necessary checks. + let undelegate_ix = undelegate( + *validator.key, + *delegated_account.key, + *owner_program.key, + *rent_reimbursement.key, + ); + invoke(&undelegate_ix, accounts)?; + + // Assign ownership back to system_program + delegated_account.assign(&system_program::ID); + Ok(()) +} diff --git a/tests/test_top_up.rs b/tests/test_top_up.rs index e880bd6..e29525a 100644 --- a/tests/test_top_up.rs +++ b/tests/test_top_up.rs @@ -11,7 +11,7 @@ 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, }; @@ -125,6 +125,47 @@ async fn test_top_up_ephemeral_balance_and_delegate_for_pubkey() { assert!(res.is_ok()); } +#[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_ephemeral_balance( + validator.pubkey(), + ephemeral_balance_pda, + 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 is closed + 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(); + let expected_owner = system_program::id(); + assert_eq!(actual_owner, expected_owner); +} + #[tokio::test] async fn test_undelegate_and_close() { // Setup @@ -149,10 +190,9 @@ async fn test_undelegate_and_close() { .lamports; // Undelegate ephemeral balance Ix - let ix = dlp::instruction_builder::undelegate( + let ix = dlp::instruction_builder::undelegate_ephemeral_balance( validator.pubkey(), ephemeral_balance_pda, - dlp::id(), validator.pubkey(), ); @@ -201,87 +241,95 @@ 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(), dlp::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) } From f7e71b41cc7f4a1593847a11de58705d912f070e Mon Sep 17 00:00:00 2001 From: taco-paco Date: Tue, 6 May 2025 13:54:46 +0900 Subject: [PATCH 2/7] refactor: skip transfer if amount is 0 --- src/processor/close_ephemeral_balance_v1.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/processor/close_ephemeral_balance_v1.rs b/src/processor/close_ephemeral_balance_v1.rs index 3e371c3..b2b1162 100644 --- a/src/processor/close_ephemeral_balance_v1.rs +++ b/src/processor/close_ephemeral_balance_v1.rs @@ -55,6 +55,10 @@ pub fn process_close_ephemeral_balance_v1( } 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(); From d781c42b4325ea67450d4dfec3ca8161e91f84e0 Mon Sep 17 00:00:00 2001 From: taco-paco Date: Tue, 6 May 2025 14:06:02 +0900 Subject: [PATCH 3/7] refactor: addressed greptile comments --- src/instruction_builder/undelegate_ephemeral_balance.rs | 5 ++--- tests/test_top_up.rs | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/instruction_builder/undelegate_ephemeral_balance.rs b/src/instruction_builder/undelegate_ephemeral_balance.rs index fec70f1..c924fee 100644 --- a/src/instruction_builder/undelegate_ephemeral_balance.rs +++ b/src/instruction_builder/undelegate_ephemeral_balance.rs @@ -3,9 +3,8 @@ use crate::instruction_builder::undelegate; use solana_program::instruction::Instruction; use solana_program::pubkey::Pubkey; -/// Builds an undelegate instruction. -/// See [crate::processor::process_undelegate] for docs. -#[allow(clippy::too_many_arguments)] +/// Builds an undelegate instruction for ephemeral balance. +/// See [crate::processor::process_undelegate_ephemeral_balance] for docs. pub fn undelegate_ephemeral_balance( validator: Pubkey, delegated_account: Pubkey, diff --git a/tests/test_top_up.rs b/tests/test_top_up.rs index e29525a..6712e0a 100644 --- a/tests/test_top_up.rs +++ b/tests/test_top_up.rs @@ -157,7 +157,7 @@ async fn test_undelegate() { let res = banks.process_transaction(tx).await; assert!(res.is_ok()); - // Assert that the ephemeral balance account is closed + // 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()); From b03eecc10a7669b8f18b536a24cd5986e2d6f1db Mon Sep 17 00:00:00 2001 From: taco-paco Date: Thu, 8 May 2025 18:22:25 +0900 Subject: [PATCH 4/7] feat: undelegating escrow adding external_undelegate support to dlp --- src/discriminator.rs | 12 +++- src/lib.rs | 3 + src/processor/delegate_ephemeral_balance.rs | 6 ++ src/processor/external_undelegate.rs | 76 +++++++++++++++++++++ src/processor/mod.rs | 2 + src/processor/top_up_ephemeral_balance.rs | 2 +- src/state/ephemeral_balance.rs | 15 ++++ src/state/mod.rs | 2 + src/state/utils/discriminator.rs | 3 +- src/state/utils/to_bytes.rs | 6 +- tests/test_top_up.rs | 7 +- 11 files changed, 124 insertions(+), 10 deletions(-) create mode 100644 src/processor/external_undelegate.rs create mode 100644 src/state/ephemeral_balance.rs diff --git a/src/discriminator.rs b/src/discriminator.rs index 197a1b9..d6bcabb 100644 --- a/src/discriminator.rs +++ b/src/discriminator.rs @@ -1,3 +1,4 @@ +use crate::consts::EXTERNAL_UNDELEGATE_DISCRIMINATOR; use num_enum::TryFromPrimitive; use solana_program::program_error::ProgramError; @@ -36,7 +37,9 @@ pub enum DlpDiscriminator { /// See [crate::processor::process_undelegate_ephemeral_balance] for docs. UndelegateEphemeralBalance = 15, /// See [crate::processor::process_close_ephemeral_balance_v1] for docs. - CloseEphemeralBalanceV1 = 16 + CloseEphemeralBalanceV1 = 16, + /// see [crate::processor::process_external_undelegate] for docs. + ExternalUndelegate = 17 } impl DlpDiscriminator { @@ -49,8 +52,11 @@ impl DlpDiscriminator { impl TryFrom<[u8; 8]> for DlpDiscriminator { type Error = ProgramError; fn try_from(bytes: [u8; 8]) -> Result { - let discriminator = u64::from_le_bytes(bytes); - match discriminator { + if bytes == EXTERNAL_UNDELEGATE_DISCRIMINATOR { + return Ok(DlpDiscriminator::ExternalUndelegate); + } + + match bytes[0] { 0x0 => Ok(DlpDiscriminator::Delegate), 0x1 => Ok(DlpDiscriminator::CommitState), 0x2 => Ok(DlpDiscriminator::Finalize), diff --git a/src/lib.rs b/src/lib.rs index a8fc29a..37df423 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -99,6 +99,9 @@ pub fn process_instruction( discriminator::DlpDiscriminator::CloseEphemeralBalanceV1 => { processor::process_close_ephemeral_balance_v1(program_id, accounts, data)? } + discriminator::DlpDiscriminator::ExternalUndelegate => { + processor::process_external_undelegate(program_id, accounts, data)? + } } Ok(()) } diff --git a/src/processor/delegate_ephemeral_balance.rs b/src/processor/delegate_ephemeral_balance.rs index cd467bc..1780a6b 100644 --- a/src/processor/delegate_ephemeral_balance.rs +++ b/src/processor/delegate_ephemeral_balance.rs @@ -1,6 +1,7 @@ use crate::args::DelegateEphemeralBalanceArgs; use crate::ephemeral_balance_seeds_from_payer; use crate::processor::utils::loaders::{load_program, load_signer}; +use crate::state::EphemeralBalance; use borsh::BorshDeserialize; use solana_program::program::invoke_signed; use solana_program::program_error::ProgramError; @@ -71,6 +72,11 @@ pub fn process_delegate_ephemeral_balance( &[ephemeral_balance_account.clone(), system_program.clone()], &[&ephemeral_balance_signer_seeds], )?; + // Set discriminator + { + let mut data = ephemeral_balance_account.try_borrow_mut_data()?; + EphemeralBalance.to_bytes_with_discriminator(&mut data.as_mut())?; + } // Create the delegation ix let ix = crate::instruction_builder::delegate( diff --git a/src/processor/external_undelegate.rs b/src/processor/external_undelegate.rs new file mode 100644 index 0000000..db722bd --- /dev/null +++ b/src/processor/external_undelegate.rs @@ -0,0 +1,76 @@ +use crate::processor::utils::loaders::load_uninitialized_pda; +use crate::processor::utils::pda::create_pda; +use crate::state::discriminator::AccountDiscriminator; +use borsh::BorshDeserialize; +use solana_program::account_info::AccountInfo; +use solana_program::entrypoint::ProgramResult; +use solana_program::program_error::ProgramError; +use solana_program::pubkey::Pubkey; +use solana_program::{msg, system_program}; + +pub fn process_external_undelegate( + _program_id: &Pubkey, + accounts: &[AccountInfo], + data: &[u8], +) -> ProgramResult { + let [delegated_account, undelegate_buffer_account, payer, system_program] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + // Verify that buffer owned by dlp + if undelegate_buffer_account.owner != &crate::ID { + msg!( + "dlp program must be an owner of buffer account. actual owner: {}", + undelegate_buffer_account.owner + ); + return Err(ProgramError::InvalidAccountOwner); + } + // Verify that only dlp could be initiator of this call + // buffer derived from dlp::ID, hence only dlp could be signer + if !undelegate_buffer_account.is_signer { + msg!("buffer account must be a signer!"); + return Err(ProgramError::MissingRequiredSignature); + }; + + // Check that delegated account is uninitialized and derived from delegation program + let delegated_account_seeds: Vec> = Vec::>::try_from_slice(data)?; + let delegated_account_seeds: Vec<&[u8]> = delegated_account_seeds + .iter() + .map(|v| v.as_slice()) + .collect(); + let delegated_account_bump = load_uninitialized_pda( + delegated_account, + &delegated_account_seeds, + &crate::id(), + true, + "undelegate buffer", + )?; + + // Re-create the original PDA + msg!( + "ndelegate_buffer_account.data_len(): {}", + undelegate_buffer_account.data_len() + ); + let discriminator: [u8; 8] = undelegate_buffer_account + .try_borrow_data()? + .as_ref() + .try_into() + .map_err(|_| ProgramError::InvalidAccountData)?; + + if discriminator == AccountDiscriminator::EphemeralBalance.to_bytes() { + // zero data. Needed because of check in undelegate.rs:255 that checks data consistency + // for system transfer account can't contain any data, hence length set to 0 + undelegate_buffer_account.realloc(0, false)?; + create_pda( + delegated_account, + &system_program::ID, + 0, + &delegated_account_seeds, + delegated_account_bump, + system_program, + payer, + ) + } else { + Err(ProgramError::InvalidAccountData) + } +} diff --git a/src/processor/mod.rs b/src/processor/mod.rs index 43eda89..6df6212 100644 --- a/src/processor/mod.rs +++ b/src/processor/mod.rs @@ -5,6 +5,7 @@ mod commit_state; mod commit_state_from_buffer; mod delegate; mod delegate_ephemeral_balance; +mod external_undelegate; mod finalize; mod init_protocol_fees_vault; mod init_validator_fees_vault; @@ -23,6 +24,7 @@ pub use commit_state::*; pub use commit_state_from_buffer::*; pub use delegate::*; pub use delegate_ephemeral_balance::*; +pub use external_undelegate::*; pub use finalize::*; pub use init_protocol_fees_vault::*; pub use init_validator_fees_vault::*; diff --git a/src/processor/top_up_ephemeral_balance.rs b/src/processor/top_up_ephemeral_balance.rs index ccd0fc6..cca1e38 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(), - 0, + 8, ephemeral_balance_seeds_from_payer!(pubkey.key, args.index), bump_ephemeral_balance, system_program, diff --git a/src/state/ephemeral_balance.rs b/src/state/ephemeral_balance.rs new file mode 100644 index 0000000..b4c9044 --- /dev/null +++ b/src/state/ephemeral_balance.rs @@ -0,0 +1,15 @@ +use crate::state::discriminator::{AccountDiscriminator, AccountWithDiscriminator}; +use crate::{impl_to_bytes_with_discriminator_borsh, impl_try_from_bytes_with_discriminator_borsh}; +use borsh::{BorshDeserialize, BorshSerialize}; + +#[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq)] +pub struct EphemeralBalance; + +impl AccountWithDiscriminator for EphemeralBalance { + fn discriminator() -> AccountDiscriminator { + AccountDiscriminator::EphemeralBalance + } +} + +impl_to_bytes_with_discriminator_borsh!(EphemeralBalance); +impl_try_from_bytes_with_discriminator_borsh!(EphemeralBalance); diff --git a/src/state/mod.rs b/src/state/mod.rs index 59f8936..e86bd14 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 ephemeral_balance; mod program_config; mod utils; pub use commit_record::*; pub use delegation_metadata::*; pub use delegation_record::*; +pub use ephemeral_balance::*; pub use program_config::*; pub use utils::*; diff --git a/src/state/utils/discriminator.rs b/src/state/utils/discriminator.rs index 7d283ec..832d1d3 100644 --- a/src/state/utils/discriminator.rs +++ b/src/state/utils/discriminator.rs @@ -7,10 +7,11 @@ pub enum AccountDiscriminator { DelegationMetadata = 102, CommitRecord = 101, ProgramConfig = 103, + EphemeralBalance = 104, } 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 6712e0a..6aed80a 100644 --- a/tests/test_top_up.rs +++ b/tests/test_top_up.rs @@ -2,11 +2,13 @@ use crate::fixtures::{ create_delegation_metadata_data, create_delegation_record_data, TEST_AUTHORITY, }; use dlp::args::DelegateEphemeralBalanceArgs; +use dlp::consts::DELEGATION_PROGRAM_ID; use dlp::ephemeral_balance_seeds_from_payer; 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::discriminator::AccountDiscriminator; use solana_program::rent::Rent; use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, system_program}; use solana_program_test::{processor, BanksClient, ProgramTest}; @@ -142,9 +144,10 @@ async fn test_undelegate() { assert_eq!(ephemeral_balance_owner, dlp::id()); // Undelegate ephemeral balance Ix - let ix = dlp::instruction_builder::undelegate_ephemeral_balance( + let ix = dlp::instruction_builder::undelegate( validator.pubkey(), ephemeral_balance_pda, + DELEGATION_PROGRAM_ID, validator.pubkey(), ); @@ -295,7 +298,7 @@ async fn setup_ephemeral_balance( ephemeral_balance_pda, Account { lamports: LAMPORTS_PER_SOL, - data: vec![], + data: AccountDiscriminator::EphemeralBalance.to_bytes().to_vec(), owner: dlp::id(), executable: false, rent_epoch: 0, From 0ee1fe08a1d1daaa8267a2d682c11fa3bbfc8749 Mon Sep 17 00:00:00 2001 From: taco-paco Date: Thu, 8 May 2025 18:27:11 +0900 Subject: [PATCH 5/7] refactor: remove initial implementation --- src/discriminator.rs | 8 +- .../close_ephemeral_balance.rs | 5 +- src/instruction_builder/mod.rs | 2 - .../undelegate_ephemeral_balance.rs | 17 ----- src/lib.rs | 6 -- src/processor/close_ephemeral_balance.rs | 43 +++++++++-- src/processor/close_ephemeral_balance_v1.rs | 76 ------------------- src/processor/mod.rs | 4 - src/processor/undelegate_ephemeral_balance.rs | 74 ------------------ tests/test_top_up.rs | 3 +- 10 files changed, 41 insertions(+), 197 deletions(-) delete mode 100644 src/instruction_builder/undelegate_ephemeral_balance.rs delete mode 100644 src/processor/close_ephemeral_balance_v1.rs delete mode 100644 src/processor/undelegate_ephemeral_balance.rs diff --git a/src/discriminator.rs b/src/discriminator.rs index d6bcabb..db37d94 100644 --- a/src/discriminator.rs +++ b/src/discriminator.rs @@ -34,12 +34,8 @@ pub enum DlpDiscriminator { CommitStateFromBuffer = 13, /// See [crate::processor::process_close_validator_fees_vault] for docs. CloseValidatorFeesVault = 14, - /// See [crate::processor::process_undelegate_ephemeral_balance] for docs. - UndelegateEphemeralBalance = 15, - /// See [crate::processor::process_close_ephemeral_balance_v1] for docs. - CloseEphemeralBalanceV1 = 16, /// see [crate::processor::process_external_undelegate] for docs. - ExternalUndelegate = 17 + ExternalUndelegate = 15 } impl DlpDiscriminator { @@ -71,8 +67,6 @@ impl TryFrom<[u8; 8]> for DlpDiscriminator { 0xc => Ok(DlpDiscriminator::ProtocolClaimFees), 0xd => Ok(DlpDiscriminator::CommitStateFromBuffer), 0xe => Ok(DlpDiscriminator::CloseValidatorFeesVault), - 0xf => Ok(DlpDiscriminator::UndelegateEphemeralBalance), - 0x10 => Ok(DlpDiscriminator::CloseEphemeralBalanceV1), _ => Err(ProgramError::InvalidInstructionData), } } diff --git a/src/instruction_builder/close_ephemeral_balance.rs b/src/instruction_builder/close_ephemeral_balance.rs index 153fdfa..ddf501b 100644 --- a/src/instruction_builder/close_ephemeral_balance.rs +++ b/src/instruction_builder/close_ephemeral_balance.rs @@ -5,8 +5,7 @@ use crate::discriminator::DlpDiscriminator; use crate::pda::ephemeral_balance_pda_from_payer; /// Creates instruction to close an ephemeral balance account -/// See [crate::processor::process_close_ephemeral_balance_v1] for docs. -/// [crate::processor::process_close_ephemeral_balance] now deprecated +/// See [crate::processor::process_close_ephemeral_balance_] for docs. pub fn close_ephemeral_balance(payer: Pubkey, index: u8) -> Instruction { let ephemeral_balance_pda = ephemeral_balance_pda_from_payer(&payer, index); Instruction { @@ -17,7 +16,7 @@ pub fn close_ephemeral_balance(payer: Pubkey, index: u8) -> Instruction { AccountMeta::new_readonly(system_program::id(), false), ], data: [ - DlpDiscriminator::CloseEphemeralBalanceV1.to_vec(), + DlpDiscriminator::CloseEphemeralBalance.to_vec(), vec![index], ] .concat(), diff --git a/src/instruction_builder/mod.rs b/src/instruction_builder/mod.rs index d174d6d..e0cae79 100644 --- a/src/instruction_builder/mod.rs +++ b/src/instruction_builder/mod.rs @@ -11,7 +11,6 @@ mod init_validator_fees_vault; mod protocol_claim_fees; mod top_up_ephemeral_balance; mod undelegate; -mod undelegate_ephemeral_balance; mod validator_claim_fees; mod whitelist_validator_for_program; @@ -27,6 +26,5 @@ pub use init_validator_fees_vault::*; pub use protocol_claim_fees::*; pub use top_up_ephemeral_balance::*; pub use undelegate::*; -pub use undelegate_ephemeral_balance::*; pub use validator_claim_fees::*; pub use whitelist_validator_for_program::*; diff --git a/src/instruction_builder/undelegate_ephemeral_balance.rs b/src/instruction_builder/undelegate_ephemeral_balance.rs deleted file mode 100644 index c924fee..0000000 --- a/src/instruction_builder/undelegate_ephemeral_balance.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::discriminator::DlpDiscriminator; -use crate::instruction_builder::undelegate; -use solana_program::instruction::Instruction; -use solana_program::pubkey::Pubkey; - -/// Builds an undelegate instruction for ephemeral balance. -/// See [crate::processor::process_undelegate_ephemeral_balance] for docs. -pub fn undelegate_ephemeral_balance( - validator: Pubkey, - delegated_account: Pubkey, - rent_reimbursement: Pubkey, -) -> Instruction { - let mut ix = undelegate(validator, delegated_account, crate::ID, rent_reimbursement); - ix.data = DlpDiscriminator::UndelegateEphemeralBalance.to_vec(); - - ix -} diff --git a/src/lib.rs b/src/lib.rs index 37df423..66a3381 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -93,12 +93,6 @@ pub fn process_instruction( discriminator::DlpDiscriminator::CloseValidatorFeesVault => { processor::process_close_validator_fees_vault(program_id, accounts, data)? } - discriminator::DlpDiscriminator::UndelegateEphemeralBalance => { - processor::process_undelegate_ephemeral_balance(program_id, accounts, data)? - } - discriminator::DlpDiscriminator::CloseEphemeralBalanceV1 => { - processor::process_close_ephemeral_balance_v1(program_id, accounts, data)? - } discriminator::DlpDiscriminator::ExternalUndelegate => { processor::process_external_undelegate(program_id, accounts, data)? } diff --git a/src/processor/close_ephemeral_balance.rs b/src/processor/close_ephemeral_balance.rs index ac9d784..04ec4e6 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: {}", + system_program::id() + ); + 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/close_ephemeral_balance_v1.rs b/src/processor/close_ephemeral_balance_v1.rs deleted file mode 100644 index b2b1162..0000000 --- a/src/processor/close_ephemeral_balance_v1.rs +++ /dev/null @@ -1,76 +0,0 @@ -use crate::ephemeral_balance_seeds_from_payer; -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::system_instruction::transfer; -use solana_program::{ - account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey, system_program, -}; - -/// Process the closing of an ephemeral balance account -/// -/// Accounts: -/// -/// 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: -/// -/// - ephemeral balance account is initialized -/// -/// Steps: -/// -/// 1. Closes the ephemeral balance account and refunds the payer with the -/// escrowed lamports -pub fn process_close_ephemeral_balance_v1( - _program_id: &Pubkey, - accounts: &[AccountInfo], - data: &[u8], -) -> ProgramResult { - let index = *data.first().ok_or(ProgramError::InvalidInstructionData)?; - - // Load Accounts - let [payer, ephemeral_balance_account, system_program] = accounts else { - return Err(ProgramError::NotEnoughAccountKeys); - }; - - load_signer(payer, "payer")?; - - 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, - &crate::id(), - true, - "ephemeral balance", - )?; - if ephemeral_balance_account.owner != &system_program::id() { - msg!( - "ephemeral balance expected to be owned by system program: {}", - system_program::id() - ); - return Err(ProgramError::InvalidAccountOwner); - } - - 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/mod.rs b/src/processor/mod.rs index 6df6212..365b67e 100644 --- a/src/processor/mod.rs +++ b/src/processor/mod.rs @@ -1,5 +1,4 @@ mod close_ephemeral_balance; -mod close_ephemeral_balance_v1; mod close_validator_fees_vault; mod commit_state; mod commit_state_from_buffer; @@ -12,13 +11,11 @@ mod init_validator_fees_vault; mod protocol_claim_fees; mod top_up_ephemeral_balance; mod undelegate; -mod undelegate_ephemeral_balance; mod utils; mod validator_claim_fees; mod whitelist_validator_for_program; pub use close_ephemeral_balance::*; -pub use close_ephemeral_balance_v1::*; pub use close_validator_fees_vault::*; pub use commit_state::*; pub use commit_state_from_buffer::*; @@ -31,6 +28,5 @@ pub use init_validator_fees_vault::*; pub use protocol_claim_fees::*; pub use top_up_ephemeral_balance::*; pub use undelegate::*; -pub use undelegate_ephemeral_balance::*; pub use validator_claim_fees::*; pub use whitelist_validator_for_program::*; diff --git a/src/processor/undelegate_ephemeral_balance.rs b/src/processor/undelegate_ephemeral_balance.rs deleted file mode 100644 index 7179b77..0000000 --- a/src/processor/undelegate_ephemeral_balance.rs +++ /dev/null @@ -1,74 +0,0 @@ -use crate::instruction_builder::undelegate; -use solana_program::msg; -use solana_program::program::invoke; -use solana_program::program_error::ProgramError; -use solana_program::{ - account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey, system_program, -}; - -/// Undelegate ephemeral balance -/// -/// Accounts: -/// -/// 0: `[signer]` the validator account -/// 1: `[writable]` the delegated account -/// 2: `[]` the owner program of the delegated account -/// 3: `[writable]` the undelegate buffer PDA we use to store the data temporarily -/// 4: `[]` the commit state PDA -/// 5: `[]` the commit record PDA -/// 6: `[writable]` the delegation record PDA -/// 7: `[writable]` the delegation metadata PDA -/// 8: `[]` the rent reimbursement account -/// 9: `[writable]` the protocol fees vault account -/// 10: `[writable]` the validator fees vault account -/// 11: `[]` the system program -/// -/// Requirements: -/// -/// - delegated account is owned by delegation program -/// - delegation record is initialized -/// - delegation metadata is initialized -/// - protocol fees vault is initialized -/// - validator fees vault is initialized -/// - commit state is uninitialized -/// - commit record is uninitialized -/// - delegated account is NOT undelegatable -/// - owner program account matches the owner in the delegation record -/// - rent reimbursement account matches the rent payer in the delegation metadata -/// -/// Steps: -/// -/// - Undelegate using CPI into [`crate::processor::undelegate`] -/// - Assigns ownership back to system program -pub fn process_undelegate_ephemeral_balance( - _program_id: &Pubkey, - accounts: &[AccountInfo], - _data: &[u8], -) -> ProgramResult { - let [validator, delegated_account, owner_program, _, _, _, _, _, rent_reimbursement, _, _, _] = - accounts - else { - return Err(ProgramError::NotEnoughAccountKeys); - }; - - if owner_program.key != &crate::ID { - msg!( - "Unexpected owner program. expected dlp, got: {}", - owner_program.key - ); - return Err(ProgramError::IncorrectProgramId); - } - - // Propagate to undelegate which also runs all necessary checks. - let undelegate_ix = undelegate( - *validator.key, - *delegated_account.key, - *owner_program.key, - *rent_reimbursement.key, - ); - invoke(&undelegate_ix, accounts)?; - - // Assign ownership back to system_program - delegated_account.assign(&system_program::ID); - Ok(()) -} diff --git a/tests/test_top_up.rs b/tests/test_top_up.rs index 6aed80a..13daaa6 100644 --- a/tests/test_top_up.rs +++ b/tests/test_top_up.rs @@ -193,9 +193,10 @@ async fn test_undelegate_and_close() { .lamports; // Undelegate ephemeral balance Ix - let ix = dlp::instruction_builder::undelegate_ephemeral_balance( + let ix = dlp::instruction_builder::undelegate( validator.pubkey(), ephemeral_balance_pda, + DELEGATION_PROGRAM_ID, validator.pubkey(), ); From e2ac45474d6f93115b7cf705e5a6cfc8df09d31c Mon Sep 17 00:00:00 2001 From: taco-paco Date: Thu, 8 May 2025 18:45:48 +0900 Subject: [PATCH 6/7] refactor: added docs + some refactor --- .../close_ephemeral_balance.rs | 2 +- src/processor/close_ephemeral_balance.rs | 4 ++-- src/processor/external_undelegate.rs | 21 +++++++++++++++++++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/instruction_builder/close_ephemeral_balance.rs b/src/instruction_builder/close_ephemeral_balance.rs index ddf501b..cae2470 100644 --- a/src/instruction_builder/close_ephemeral_balance.rs +++ b/src/instruction_builder/close_ephemeral_balance.rs @@ -5,7 +5,7 @@ use crate::discriminator::DlpDiscriminator; use crate::pda::ephemeral_balance_pda_from_payer; /// Creates instruction to close an ephemeral balance account -/// See [crate::processor::process_close_ephemeral_balance_] for docs. +/// See [crate::processor::process_close_ephemeral_balance] for docs. pub fn close_ephemeral_balance(payer: Pubkey, index: u8) -> Instruction { let ephemeral_balance_pda = ephemeral_balance_pda_from_payer(&payer, index); Instruction { diff --git a/src/processor/close_ephemeral_balance.rs b/src/processor/close_ephemeral_balance.rs index 04ec4e6..61e41a4 100644 --- a/src/processor/close_ephemeral_balance.rs +++ b/src/processor/close_ephemeral_balance.rs @@ -48,8 +48,8 @@ pub fn process_close_ephemeral_balance( )?; if ephemeral_balance_account.owner != &system_program::id() { msg!( - "ephemeral balance expected to be owned by system program: {}", - system_program::id() + "ephemeral balance expected to be owned by system program. got: {}", + ephemeral_balance_account.owner ); return Err(ProgramError::InvalidAccountOwner); } diff --git a/src/processor/external_undelegate.rs b/src/processor/external_undelegate.rs index db722bd..404e676 100644 --- a/src/processor/external_undelegate.rs +++ b/src/processor/external_undelegate.rs @@ -8,6 +8,27 @@ use solana_program::program_error::ProgramError; use solana_program::pubkey::Pubkey; use solana_program::{msg, system_program}; +/// External undelegate implementation for accounts owned by dlp +/// +/// Accounts: +/// +/// 0: `[writable]` the delegated account +/// 1: `[writable]` the undelegated buffer account +/// 2: `[signer]` the payer account +/// 3: `[]` the system program +/// +/// Requirements: +/// +/// - delegated account is uninitialized +/// - undelegated buffer account is signer and owned by dlp +/// - payer account is validator +/// Steps: +/// +/// - Check if dlp initiated call +/// - Check if delegated account is uninitialized +/// - Extract account discriminator +/// - Run discriminator specific actions +/// - For ephemeral balance transfer ownership to system program, zero buffer account. pub fn process_external_undelegate( _program_id: &Pubkey, accounts: &[AccountInfo], From 73afc2b322bc0c8555f824f81f386dbb7ec80366 Mon Sep 17 00:00:00 2001 From: Gabriele Picco Date: Mon, 12 May 2025 07:43:10 -0400 Subject: [PATCH 7/7] Simplify escrow undelegation fix (#78) * fix: remove discriminator * chore: simplify fix * fix: delegation record with system program as owner & add tests --- src/discriminator.rs | 7 -- .../delegate_ephemeral_balance.rs | 2 +- src/lib.rs | 3 - src/processor/delegate.rs | 12 ++- src/processor/delegate_ephemeral_balance.rs | 8 +- src/processor/external_undelegate.rs | 97 ------------------- src/processor/mod.rs | 2 - src/processor/top_up_ephemeral_balance.rs | 2 +- src/state/ephemeral_balance.rs | 15 --- src/state/mod.rs | 2 - src/state/utils/discriminator.rs | 1 - tests/test_top_up.rs | 86 +++++++++++----- 12 files changed, 75 insertions(+), 162 deletions(-) delete mode 100644 src/processor/external_undelegate.rs delete mode 100644 src/state/ephemeral_balance.rs diff --git a/src/discriminator.rs b/src/discriminator.rs index db37d94..e896fa2 100644 --- a/src/discriminator.rs +++ b/src/discriminator.rs @@ -1,4 +1,3 @@ -use crate::consts::EXTERNAL_UNDELEGATE_DISCRIMINATOR; use num_enum::TryFromPrimitive; use solana_program::program_error::ProgramError; @@ -34,8 +33,6 @@ pub enum DlpDiscriminator { CommitStateFromBuffer = 13, /// See [crate::processor::process_close_validator_fees_vault] for docs. CloseValidatorFeesVault = 14, - /// see [crate::processor::process_external_undelegate] for docs. - ExternalUndelegate = 15 } impl DlpDiscriminator { @@ -48,10 +45,6 @@ impl DlpDiscriminator { impl TryFrom<[u8; 8]> for DlpDiscriminator { type Error = ProgramError; fn try_from(bytes: [u8; 8]) -> Result { - if bytes == EXTERNAL_UNDELEGATE_DISCRIMINATOR { - return Ok(DlpDiscriminator::ExternalUndelegate); - } - match bytes[0] { 0x0 => Ok(DlpDiscriminator::Delegate), 0x1 => Ok(DlpDiscriminator::CommitState), 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/lib.rs b/src/lib.rs index 66a3381..4b1eca8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -93,9 +93,6 @@ pub fn process_instruction( discriminator::DlpDiscriminator::CloseValidatorFeesVault => { processor::process_close_validator_fees_vault(program_id, accounts, data)? } - discriminator::DlpDiscriminator::ExternalUndelegate => { - processor::process_external_undelegate(program_id, accounts, data)? - } } 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 1780a6b..f98bf15 100644 --- a/src/processor/delegate_ephemeral_balance.rs +++ b/src/processor/delegate_ephemeral_balance.rs @@ -1,7 +1,6 @@ use crate::args::DelegateEphemeralBalanceArgs; use crate::ephemeral_balance_seeds_from_payer; use crate::processor::utils::loaders::{load_program, load_signer}; -use crate::state::EphemeralBalance; use borsh::BorshDeserialize; use solana_program::program::invoke_signed; use solana_program::program_error::ProgramError; @@ -72,17 +71,12 @@ pub fn process_delegate_ephemeral_balance( &[ephemeral_balance_account.clone(), system_program.clone()], &[&ephemeral_balance_signer_seeds], )?; - // Set discriminator - { - let mut data = ephemeral_balance_account.try_borrow_mut_data()?; - EphemeralBalance.to_bytes_with_discriminator(&mut data.as_mut())?; - } // Create the delegation ix 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/external_undelegate.rs b/src/processor/external_undelegate.rs deleted file mode 100644 index 404e676..0000000 --- a/src/processor/external_undelegate.rs +++ /dev/null @@ -1,97 +0,0 @@ -use crate::processor::utils::loaders::load_uninitialized_pda; -use crate::processor::utils::pda::create_pda; -use crate::state::discriminator::AccountDiscriminator; -use borsh::BorshDeserialize; -use solana_program::account_info::AccountInfo; -use solana_program::entrypoint::ProgramResult; -use solana_program::program_error::ProgramError; -use solana_program::pubkey::Pubkey; -use solana_program::{msg, system_program}; - -/// External undelegate implementation for accounts owned by dlp -/// -/// Accounts: -/// -/// 0: `[writable]` the delegated account -/// 1: `[writable]` the undelegated buffer account -/// 2: `[signer]` the payer account -/// 3: `[]` the system program -/// -/// Requirements: -/// -/// - delegated account is uninitialized -/// - undelegated buffer account is signer and owned by dlp -/// - payer account is validator -/// Steps: -/// -/// - Check if dlp initiated call -/// - Check if delegated account is uninitialized -/// - Extract account discriminator -/// - Run discriminator specific actions -/// - For ephemeral balance transfer ownership to system program, zero buffer account. -pub fn process_external_undelegate( - _program_id: &Pubkey, - accounts: &[AccountInfo], - data: &[u8], -) -> ProgramResult { - let [delegated_account, undelegate_buffer_account, payer, system_program] = accounts else { - return Err(ProgramError::NotEnoughAccountKeys); - }; - - // Verify that buffer owned by dlp - if undelegate_buffer_account.owner != &crate::ID { - msg!( - "dlp program must be an owner of buffer account. actual owner: {}", - undelegate_buffer_account.owner - ); - return Err(ProgramError::InvalidAccountOwner); - } - // Verify that only dlp could be initiator of this call - // buffer derived from dlp::ID, hence only dlp could be signer - if !undelegate_buffer_account.is_signer { - msg!("buffer account must be a signer!"); - return Err(ProgramError::MissingRequiredSignature); - }; - - // Check that delegated account is uninitialized and derived from delegation program - let delegated_account_seeds: Vec> = Vec::>::try_from_slice(data)?; - let delegated_account_seeds: Vec<&[u8]> = delegated_account_seeds - .iter() - .map(|v| v.as_slice()) - .collect(); - let delegated_account_bump = load_uninitialized_pda( - delegated_account, - &delegated_account_seeds, - &crate::id(), - true, - "undelegate buffer", - )?; - - // Re-create the original PDA - msg!( - "ndelegate_buffer_account.data_len(): {}", - undelegate_buffer_account.data_len() - ); - let discriminator: [u8; 8] = undelegate_buffer_account - .try_borrow_data()? - .as_ref() - .try_into() - .map_err(|_| ProgramError::InvalidAccountData)?; - - if discriminator == AccountDiscriminator::EphemeralBalance.to_bytes() { - // zero data. Needed because of check in undelegate.rs:255 that checks data consistency - // for system transfer account can't contain any data, hence length set to 0 - undelegate_buffer_account.realloc(0, false)?; - create_pda( - delegated_account, - &system_program::ID, - 0, - &delegated_account_seeds, - delegated_account_bump, - system_program, - payer, - ) - } else { - Err(ProgramError::InvalidAccountData) - } -} diff --git a/src/processor/mod.rs b/src/processor/mod.rs index 365b67e..e9b25db 100644 --- a/src/processor/mod.rs +++ b/src/processor/mod.rs @@ -4,7 +4,6 @@ mod commit_state; mod commit_state_from_buffer; mod delegate; mod delegate_ephemeral_balance; -mod external_undelegate; mod finalize; mod init_protocol_fees_vault; mod init_validator_fees_vault; @@ -21,7 +20,6 @@ pub use commit_state::*; pub use commit_state_from_buffer::*; pub use delegate::*; pub use delegate_ephemeral_balance::*; -pub use external_undelegate::*; pub use finalize::*; pub use init_protocol_fees_vault::*; pub use init_validator_fees_vault::*; 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/ephemeral_balance.rs b/src/state/ephemeral_balance.rs deleted file mode 100644 index b4c9044..0000000 --- a/src/state/ephemeral_balance.rs +++ /dev/null @@ -1,15 +0,0 @@ -use crate::state::discriminator::{AccountDiscriminator, AccountWithDiscriminator}; -use crate::{impl_to_bytes_with_discriminator_borsh, impl_try_from_bytes_with_discriminator_borsh}; -use borsh::{BorshDeserialize, BorshSerialize}; - -#[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq)] -pub struct EphemeralBalance; - -impl AccountWithDiscriminator for EphemeralBalance { - fn discriminator() -> AccountDiscriminator { - AccountDiscriminator::EphemeralBalance - } -} - -impl_to_bytes_with_discriminator_borsh!(EphemeralBalance); -impl_try_from_bytes_with_discriminator_borsh!(EphemeralBalance); diff --git a/src/state/mod.rs b/src/state/mod.rs index e86bd14..59f8936 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -1,13 +1,11 @@ mod commit_record; mod delegation_metadata; mod delegation_record; -mod ephemeral_balance; mod program_config; mod utils; pub use commit_record::*; pub use delegation_metadata::*; pub use delegation_record::*; -pub use ephemeral_balance::*; pub use program_config::*; pub use utils::*; diff --git a/src/state/utils/discriminator.rs b/src/state/utils/discriminator.rs index 832d1d3..6f0c832 100644 --- a/src/state/utils/discriminator.rs +++ b/src/state/utils/discriminator.rs @@ -7,7 +7,6 @@ pub enum AccountDiscriminator { DelegationMetadata = 102, CommitRecord = 101, ProgramConfig = 103, - EphemeralBalance = 104, } impl AccountDiscriminator { diff --git a/tests/test_top_up.rs b/tests/test_top_up.rs index 13daaa6..e55daf2 100644 --- a/tests/test_top_up.rs +++ b/tests/test_top_up.rs @@ -2,13 +2,12 @@ use crate::fixtures::{ create_delegation_metadata_data, create_delegation_record_data, TEST_AUTHORITY, }; use dlp::args::DelegateEphemeralBalanceArgs; -use dlp::consts::DELEGATION_PROGRAM_ID; use dlp::ephemeral_balance_seeds_from_payer; 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::discriminator::AccountDiscriminator; +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}; @@ -47,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 @@ -74,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] @@ -125,6 +148,17 @@ 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] @@ -147,7 +181,7 @@ async fn test_undelegate() { let ix = dlp::instruction_builder::undelegate( validator.pubkey(), ephemeral_balance_pda, - DELEGATION_PROGRAM_ID, + system_program::id(), validator.pubkey(), ); @@ -165,8 +199,7 @@ async fn test_undelegate() { assert!(ephemeral_balance_account.is_some()); let actual_owner = *ephemeral_balance_account.unwrap().owner(); - let expected_owner = system_program::id(); - assert_eq!(actual_owner, expected_owner); + assert_eq!(actual_owner, system_program::id()); } #[tokio::test] @@ -196,7 +229,7 @@ async fn test_undelegate_and_close() { let ix = dlp::instruction_builder::undelegate( validator.pubkey(), ephemeral_balance_pda, - DELEGATION_PROGRAM_ID, + system_program::id(), validator.pubkey(), ); @@ -299,7 +332,7 @@ async fn setup_ephemeral_balance( ephemeral_balance_pda, Account { lamports: LAMPORTS_PER_SOL, - data: AccountDiscriminator::EphemeralBalance.to_bytes().to_vec(), + data: vec![], owner: dlp::id(), executable: false, rent_epoch: 0, @@ -307,8 +340,11 @@ async fn setup_ephemeral_balance( ); // Setup the delegated record PDA - let delegation_record_data = - create_delegation_record_data(validator.pubkey(), dlp::id(), Some(LAMPORTS_PER_SOL)); + 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 {