Skip to content

Conversation

@GabrielePicco
Copy link
Contributor

@GabrielePicco GabrielePicco commented Dec 3, 2025

Description

  • Add an instruction which allows to undelegate confined accounts
  • Confined accounts can only be undelegated from the admin and don't commit state changes, to preserve safety guarantees

Summary by CodeRabbit

  • New Features

    • Added admin-only undelegation instruction for confined accounts, allowing authorized administrators to restore accounts to their original owner program while preserving account data.
    • Includes proper cleanup of delegation records and associated metadata.
  • Tests

    • Added integration tests verifying undelegation behavior, account ownership restoration, and cleanup of delegation records and metadata.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 3, 2025

Walkthrough

This PR introduces support for a new undelegate instruction for confined accounts. It adds a new discriminator variant, an instruction builder function, processor logic to validate admin authority and manage PDAs, and an integration test. The changes integrate the new instruction into the dispatch pipeline and processor chain.

Changes

Cohort / File(s) Summary
Discriminator variant
src/discriminator.rs
Added new public enum variant UndelegateConfinedAccount = 18 to DlpDiscriminator with documentation reference to processor handler
Instruction builder module
src/instruction_builder/mod.rs
Added module declaration and public re-export for undelegate_confined_account submodule
Instruction builder logic
src/instruction_builder/undelegate_confined_account.rs
New public function undelegate_confined_account that constructs an admin-only Instruction for undelegating confined accounts, deriving PDAs and including relevant account references
Dispatch integration
src/lib.rs
Added new match case in fast_process_instruction to route UndelegateConfinedAccount discriminator to processor::fast::process_undelegate_confined_account
Processor module setup
src/processor/fast/mod.rs
Added module declaration and public re-export for undelegate_confined_account submodule
Processor refactoring
src/processor/fast/undelegate.rs
Changed visibility of process_undelegation_with_cpi from private to pub(crate) to enable reuse; removed no-op comment
Processor implementation
src/processor/fast/undelegate_confined_account.rs
New public function process_undelegate_confined_account implementing admin-only confined account undelegation with authority validation (conditional compilation for unit tests vs. runtime), PDA management, state validation, and cleanup
Integration test
tests/test_undelegate_confined_account.rs
New test verifying undelegation of confined accounts: confirms PDAs are closed, delegated account ownership is restored, and state is preserved

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Authority validation logic in src/processor/fast/undelegate_confined_account.rs, particularly the conditional compilation branches (unit test vs. runtime UpgradeableLoaderState decoding)
  • PDA derivation and bump seed calculations across instruction builder and processor to verify consistency
  • Account ownership and delegation state validation sequence
  • Memory management and cleanup paths in the processor, especially with borrowed data handles
  • Integration test setup and fixtures for delegated PDA and related PDAs

Possibly related PRs

Suggested reviewers

  • snawaz
  • thlorenz

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Description check ❓ Inconclusive The description is minimal and lacks most required template sections (Problem, Solution, Deploy Notes, etc.), though it does convey the core intent. Expand description to follow the template structure: explain the problem being solved, detail the solution implementation, and note any deployment considerations.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding an instruction for undelegating confined accounts.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/add-option-to-undelegate-confined-account

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c891abb and ef64a21.

📒 Files selected for processing (8)
  • src/discriminator.rs (1 hunks)
  • src/instruction_builder/mod.rs (2 hunks)
  • src/instruction_builder/undelegate_confined_account.rs (1 hunks)
  • src/lib.rs (1 hunks)
  • src/processor/fast/mod.rs (2 hunks)
  • src/processor/fast/undelegate.rs (1 hunks)
  • src/processor/fast/undelegate_confined_account.rs (1 hunks)
  • tests/test_undelegate_confined_account.rs (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-15T11:45:25.802Z
Learnt from: snawaz
Repo: magicblock-labs/delegation-program PR: 107
File: src/entrypoint.rs:141-144
Timestamp: 2025-10-15T11:45:25.802Z
Learning: In the delegation-program codebase, prefer using `log!` (from pinocchio_log) over `msg!` for error and panic scenarios in the entrypoint code, as per maintainer preference.

Applied to files:

  • src/processor/fast/mod.rs
🧬 Code graph analysis (5)
src/lib.rs (1)
src/processor/fast/undelegate_confined_account.rs (1)
  • process_undelegate_confined_account (32-171)
src/instruction_builder/mod.rs (1)
src/instruction_builder/undelegate_confined_account.rs (1)
  • undelegate_confined_account (14-40)
src/processor/fast/mod.rs (1)
src/instruction_builder/undelegate_confined_account.rs (1)
  • undelegate_confined_account (14-40)
tests/test_undelegate_confined_account.rs (3)
src/pda.rs (2)
  • delegation_metadata_pda_from_delegated_account (106-112)
  • delegation_record_pda_from_delegated_account (98-104)
tests/fixtures/accounts.rs (2)
  • create_delegation_record_data (67-84)
  • get_delegation_metadata_data (99-105)
src/instruction_builder/undelegate_confined_account.rs (1)
  • undelegate_confined_account (14-40)
src/instruction_builder/undelegate_confined_account.rs (1)
src/pda.rs (3)
  • delegation_metadata_pda_from_delegated_account (106-112)
  • delegation_record_pda_from_delegated_account (98-104)
  • undelegate_buffer_pda_from_delegated_account (141-147)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: lint
🔇 Additional comments (10)
src/processor/fast/mod.rs (1)

8-8: LGTM!

The new module declaration and re-export follow the established pattern for processor modules.

Also applies to: 18-18

src/lib.rs (1)

120-122: LGTM!

The new discriminator case correctly routes to the fast processor handler, consistent with other fast-path instructions.

src/instruction_builder/mod.rs (1)

16-16: LGTM!

Module declaration and re-export follow the established pattern and maintain alphabetical ordering.

Also applies to: 35-35

src/discriminator.rs (1)

42-43: LGTM!

The new discriminator variant follows the established pattern with unique value and consistent documentation style.

src/processor/fast/undelegate.rs (1)

232-232: LGTM!

Appropriate visibility change to pub(crate) enables code reuse by the new process_undelegate_confined_account handler while keeping the function internal to the crate.

src/instruction_builder/undelegate_confined_account.rs (1)

14-39: LGTM!

The instruction builder correctly:

  • Derives all required PDAs from the delegated account
  • Sets appropriate mutability flags (writable for accounts being modified/closed)
  • Maintains correct account ordering matching the processor expectation
  • Uses the new discriminator for serialization
src/processor/fast/undelegate_confined_account.rs (4)

22-41: LGTM!

The function signature and account unpacking follow standard patterns. Documentation clearly describes the required accounts and their roles.


43-44: LGTM!

Admin signer validation is correctly implemented.


82-109: LGTM!

The validation logic correctly enforces that:

  1. The delegated account is a valid PDA owned by the delegation program
  2. Delegation record and metadata are initialized
  3. The account is confined (authority == system program)
  4. The owner matches the stored owner in the delegation record

146-170: LGTM!

The CPI processing and cleanup logic is correctly implemented:

  1. Reuses the existing process_undelegation_with_cpi helper for consistency
  2. Properly drops borrowed data references before closing PDAs to avoid memory safety issues
  3. Closes all PDAs in the correct order, returning lamports to the admin

The error handling is safe due to Solana's transaction atomicity - if any step fails, all state changes are reverted.

Comment on lines +46 to +80
// Verify admin is the program upgrade authority.
// In unit tests, accept DEFAULT_VALIDATOR_IDENTITY without requiring ProgramData.
#[cfg(feature = "unit_test_config")]
{
if !pubkey_eq(
&crate::consts::DEFAULT_VALIDATOR_IDENTITY.to_bytes(),
admin.key(),
) {
return Err(DlpError::Unauthorized.into());
}
}

#[cfg(not(feature = "unit_test_config"))]
{
// Deserialize UpgradeableLoaderState::ProgramData to get upgrade_authority_address
let data = delegation_program_data.try_borrow_data()?;
// Use the same structure as solana_program to decode ProgramData
use solana_program::bpf_loader_upgradeable::UpgradeableLoaderState as SolUpgradeableLoaderState;
let state: SolUpgradeableLoaderState =
bincode::deserialize(&data).map_err(|_| ProgramError::InvalidAccountData)?;
if let SolUpgradeableLoaderState::ProgramData {
upgrade_authority_address,
..
} = state
{
let Some(auth) = upgrade_authority_address else {
return Err(DlpError::Unauthorized.into());
};
if !pubkey_eq(&auth.to_bytes(), admin.key()) {
return Err(DlpError::Unauthorized.into());
}
} else {
return Err(ProgramError::InvalidAccountData);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cat -n src/processor/fast/undelegate_confined_account.rs | head -120

Repository: magicblock-labs/delegation-program

Length of output: 5608


🏁 Script executed:

rg -n "ProgramData|find_program_address" --type=rust src/ | head -50

Repository: magicblock-labs/delegation-program

Length of output: 3780


🏁 Script executed:

sed -n '190,230p' src/processor/utils/loaders.rs

Repository: magicblock-labs/delegation-program

Length of output: 1488


🏁 Script executed:

cat -n src/instruction_builder/undelegate_confined_account.rs

Repository: magicblock-labs/delegation-program

Length of output: 2084


Add validation that delegation_program_data matches this program's actual ProgramData account.

The code deserializes the UpgradeableLoaderState and checks the upgrade authority, but does not validate that delegation_program_data (account 7) is actually the ProgramData account for this program. An attacker could pass a different program's ProgramData account with a different upgrade authority to bypass the authorization check.

A validation pattern for this check already exists in src/processor/utils/loaders.rs (line 209-213). Add the following check before deserializing:

let program_data_address =
    Pubkey::find_program_address(&[_program_id.as_ref()], &solana_program::bpf_loader_upgradeable::id()).0;
if !pubkey_eq(&program_data_address.to_bytes(), delegation_program_data.key()) {
    return Err(ProgramError::InvalidAccountData);
}
🤖 Prompt for AI Agents
In src/processor/fast/undelegate_confined_account.rs around lines 46 to 80, the
code deserializes delegation_program_data and checks only the upgrade authority
but does not ensure delegation_program_data is actually this program's
ProgramData account; before deserializing, compute the expected program_data PDA
via Pubkey::find_program_address with this program_id and
solana_program::bpf_loader_upgradeable::id(), compare its bytes to
delegation_program_data.key(), and if they differ return
ProgramError::InvalidAccountData; then proceed to deserialize and validate the
upgrade_authority as currently implemented.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants