Skip to content

feat: Impl RequestUndelegation and UndelegateWithRollbackAfterTimeout#183

Open
snawaz wants to merge 12 commits into
mainfrom
snawaz/request-undelegation
Open

feat: Impl RequestUndelegation and UndelegateWithRollbackAfterTimeout#183
snawaz wants to merge 12 commits into
mainfrom
snawaz/request-undelegation

Conversation

@snawaz

@snawaz snawaz commented Jun 21, 2026

Copy link
Copy Markdown
Collaborator
  • Added two instructions:
    • RequestUndelegation to be invoked by owner program through CPI
    • UndelegateWithRollbackAfterTimeout to be invoked by anyone once timeout is expired.
      • It was planned to permissionless but that is unsafe because the instruction discards any unfinalized commits, means data-loss, which is why the instruction has Rollback in it.
      • Please notice the CHECKPOINT comment.
  • Modified Undelegate instruction to optionally take additional accounts to handle request-undelegation.
  • Added tests.

snawaz commented Jun 21, 2026

Copy link
Copy Markdown
Collaborator Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Two new instruction handlers are added to the DLP delegation program: RequestUndelegation (discriminant 26) and UndelegateAfterRequestTimeout (discriminant 27). The first creates an UndelegationRequest PDA on-chain that records the delegated account, owner program, rent payer, creation slot, expiry slot, and delegation nonce; it is idempotent for the same delegated account. The second is a permissionless path that, after the expiry slot passes, performs the actual undelegation, rolls back any pending validator commits without applying them, and closes the undelegation-request, delegation-record, and delegation-metadata PDAs. The existing process_undelegate handler is extended to optionally accept and close a request PDA when supplied. Nine new DlpError variants, a new UndelegationRequest zero-copy account struct with discriminant 104, updated PDA seed helpers, instruction builders with compute-budget sizing, test fixtures, and integration tests are included.

Suggested reviewers

  • GabrielePicco
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch snawaz/request-undelegation

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_request_undelegation.rs`:
- Around line 170-224: The test suite currently only validates the happy-path
case for the undelegate_with_request instruction with optional accounts. Add a
new test function that creates a malformed undelegate_with_request instruction
with exactly one optional account instead of the required zero or two, then
assert that processing the transaction returns an InvalidInstructionData error.
This regression test ensures the strict optional-account validation logic in
src/processor/fast/undelegate.rs (lines 101-111) continues to work correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f31d3f2e-84de-4ecc-99a0-022139c3c9bc

📥 Commits

Reviewing files that changed from the base of the PR and between 3f9239a and bf93ac1.

📒 Files selected for processing (16)
  • dlp-api/src/discriminator.rs
  • dlp-api/src/error.rs
  • dlp-api/src/instruction_builder/mod.rs
  • dlp-api/src/instruction_builder/request_undelegation.rs
  • dlp-api/src/instruction_builder/undelegate.rs
  • dlp-api/src/pda.rs
  • dlp-api/src/requires.rs
  • dlp-api/src/state/mod.rs
  • dlp-api/src/state/undelegation_request.rs
  • dlp-api/src/state/utils/discriminator.rs
  • src/lib.rs
  • src/processor/fast/mod.rs
  • src/processor/fast/request_undelegation.rs
  • src/processor/fast/undelegate.rs
  • tests/fixtures/accounts.rs
  • tests/test_request_undelegation.rs

Comment thread tests/test_request_undelegation.rs
@snawaz snawaz force-pushed the snawaz/request-undelegation branch from a2ffa59 to 4d45bec Compare June 22, 2026 13:22

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/processor/fast/carry_over_requested_undelegation.rs`:
- Around line 252-259: The code checks if commit_state_account and
commit_record_account are uninitialized and returns early without verifying
these are the expected PDA addresses, allowing an attacker to pass arbitrary
empty accounts. Before the uninitialized check on commit_state_account and
commit_record_account, add validation to ensure both accounts are the correct
expected PDA addresses for commit-state and commit-record respectively. Only
after confirming the accounts match the expected PDAs should the code proceed to
check if they are uninitialized and return Ok(()).
- Around line 61-65: The `require_owned_pda` call on `delegated_account`
validates ownership but does not verify the account is writable before it is
later mutated. Add a writability validation check immediately after the
ownership validation to ensure `delegated_account` has the writable flag set
before proceeding with any mutations or unsafe owner changes. This guard should
reject readonly account metadata early in the function.

In `@tests/test_carry_over_requested_undelegation.rs`:
- Around line 162-168: The test currently only checks that the transaction fails
with assert!(res.is_err()) and verifies the request PDA exists, which is
insufficient. Strengthen this test by asserting the specific error variant or
message related to "request not expired" instead of just checking is_err, and
add additional assertions to verify that the delegation record metadata and
delegated account state remain unchanged after the failed transaction to ensure
no partial mutations occurred.

In `@tests/test_request_undelegation.rs`:
- Around line 159-160: The test at the process_transaction call is using a
generic is_err() assertion that will pass for any transaction error, not just
the specific timeout validation error being tested. Replace this broad check
with a specific assertion that extracts the error result and verifies it matches
the exact custom program error for "timeout below minimum" validation. This will
ensure the test fails only for the intended error condition and not for
unrelated transaction failures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c60d13ee-2ecb-450c-a11f-c695b6f5eb80

📥 Commits

Reviewing files that changed from the base of the PR and between bf93ac1 and a2ffa59.

📒 Files selected for processing (14)
  • dlp-api/src/consts.rs
  • dlp-api/src/discriminator.rs
  • dlp-api/src/error.rs
  • dlp-api/src/instruction_builder/carry_over_requested_undelegation.rs
  • dlp-api/src/instruction_builder/mod.rs
  • dlp-api/src/instruction_builder/request_undelegation.rs
  • dlp-api/src/state/undelegation_request.rs
  • src/lib.rs
  • src/processor/fast/carry_over_requested_undelegation.rs
  • src/processor/fast/mod.rs
  • src/processor/fast/request_undelegation.rs
  • tests/fixtures/accounts.rs
  • tests/test_carry_over_requested_undelegation.rs
  • tests/test_request_undelegation.rs

Comment thread src/processor/fast/undelegate_with_rollback_after_timeout.rs
Comment thread src/processor/fast/undelegate_with_rollback_after_timeout.rs
Comment thread tests/test_carry_over_requested_undelegation.rs Outdated
Comment thread tests/test_request_undelegation.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/processor/fast/request_undelegation.rs (1)

103-130: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Use the shared undelegation-request seed helper.

Line 103 manually reconstructs the PDA seeds even though dlp-api/src/pda.rs defines undelegation_request_seeds_from_delegated_account!; using the shared helper keeps processor and client/builder PDA contracts from drifting.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/processor/fast/request_undelegation.rs` around lines 103 - 130, The
undelegation request PDA seeds are being rebuilt manually in
request_undelegation.rs instead of using the shared helper, which risks drifting
from the client-side PDA contract. Update the undelegation flow to use
undelegation_request_seeds_from_delegated_account! from the shared pda module
wherever request_seeds is constructed, including the require_uninitialized_pda
and create_pda paths, so the processor and builder stay aligned.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/processor/fast/carry_over_requested_undelegation.rs`:
- Around line 97-127: The timeout flow in carry_over_requested_undelegation
currently validates the delegation owner and rent payer but does not bind the
loaded UndelegationRequest to the active delegation instance. Update the logic
in the carry_over_requested_undelegation path to compare
request.delegation_nonce_at_request against the current delegation state from
DelegationMetadata (or its equivalent current nonce/slot snapshot) before
allowing teardown, and return an error when they differ. Add a regression test
around the carry_over_requested_undelegation handler covering the
re-delegated/stale request case to ensure an expired request cannot affect a
newer delegation.

---

Outside diff comments:
In `@src/processor/fast/request_undelegation.rs`:
- Around line 103-130: The undelegation request PDA seeds are being rebuilt
manually in request_undelegation.rs instead of using the shared helper, which
risks drifting from the client-side PDA contract. Update the undelegation flow
to use undelegation_request_seeds_from_delegated_account! from the shared pda
module wherever request_seeds is constructed, including the
require_uninitialized_pda and create_pda paths, so the processor and builder
stay aligned.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ce808865-72db-46be-a7a0-3aeefacb34be

📥 Commits

Reviewing files that changed from the base of the PR and between a2ffa59 and 723ead0.

📒 Files selected for processing (16)
  • dlp-api/src/consts.rs
  • dlp-api/src/discriminator.rs
  • dlp-api/src/error.rs
  • dlp-api/src/instruction_builder/carry_over_requested_undelegation.rs
  • dlp-api/src/instruction_builder/mod.rs
  • dlp-api/src/instruction_builder/request_undelegation.rs
  • dlp-api/src/state/undelegation_request.rs
  • src/lib.rs
  • src/processor/fast/carry_over_requested_undelegation.rs
  • src/processor/fast/mod.rs
  • src/processor/fast/request_undelegation.rs
  • tests/fixtures/accounts.rs
  • tests/test_call_handler_v2.rs
  • tests/test_carry_over_requested_undelegation.rs
  • tests/test_lamports_settlement.rs
  • tests/test_request_undelegation.rs
💤 Files with no reviewable changes (1)
  • tests/test_lamports_settlement.rs

Comment thread src/processor/fast/undelegate_after_request_timeout.rs Outdated
@snawaz snawaz force-pushed the snawaz/request-undelegation branch from d2555d9 to 3cd51cc Compare June 24, 2026 14:47
@snawaz snawaz force-pushed the snawaz/request-undelegation branch from 803cc35 to 2f367f4 Compare June 24, 2026 15:09

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
dlp-api/src/error.rs (1)

158-183: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve the existing custom error code for InvalidPendingCommitState.

Line 182 moves an existing DlpError variant to 52 by inserting the new request errors ahead of it. That is a wire-level breaking change: Solana surfaces these as InstructionError::Custom(code), and the provided tests already compare DlpError as u32. Please keep InvalidPendingCommitState on its previous discriminant and assign the new undelegation-request errors after it instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dlp-api/src/error.rs` around lines 158 - 183, Preserve the existing custom
error code for InvalidPendingCommitState in DlpError and do not change its
discriminant when adding the new undelegation-request variants. Move the new
request-related errors so they are assigned after InvalidPendingCommitState,
keeping the existing variant’s numeric value stable for InstructionError::Custom
and tests that cast DlpError as u32. Locate the enum in error.rs by the
InvalidPendingCommitState variant and adjust the surrounding discriminants
accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@dlp-api/src/error.rs`:
- Around line 158-183: Preserve the existing custom error code for
InvalidPendingCommitState in DlpError and do not change its discriminant when
adding the new undelegation-request variants. Move the new request-related
errors so they are assigned after InvalidPendingCommitState, keeping the
existing variant’s numeric value stable for InstructionError::Custom and tests
that cast DlpError as u32. Locate the enum in error.rs by the
InvalidPendingCommitState variant and adjust the surrounding discriminants
accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e917a490-9e60-4cad-b1c3-f990e2d9bb36

📥 Commits

Reviewing files that changed from the base of the PR and between d2555d9 and 7cdfee4.

📒 Files selected for processing (12)
  • .github/workflows/run-tests.yml
  • dlp-api/src/consts.rs
  • dlp-api/src/discriminator.rs
  • dlp-api/src/error.rs
  • dlp-api/src/instruction_builder/mod.rs
  • dlp-api/src/instruction_builder/undelegate_after_request_timeout.rs
  • dlp-api/src/state/undelegation_request.rs
  • src/lib.rs
  • src/processor/fast/mod.rs
  • src/processor/fast/undelegate_after_request_timeout.rs
  • tests/test_request_undelegation.rs
  • tests/test_undelegate_after_request_timeout.rs

@snawaz snawaz marked this pull request as ready for review June 25, 2026 07:09
@snawaz snawaz requested a review from GabrielePicco June 25, 2026 07:09
@snawaz snawaz changed the title feat: Impl RequestUndelegation feat: Impl RequestUndelegation and UndelegateAfterRequestTimeout instructions Jun 25, 2026
…meout and enforce program-authorization, removing permissionless
@snawaz snawaz changed the title feat: Impl RequestUndelegation and UndelegateAfterRequestTimeout instructions feat: Impl RequestUndelegation and UndelegateWithRollbackAfterTimeout instructions Jun 26, 2026
@snawaz snawaz changed the title feat: Impl RequestUndelegation and UndelegateWithRollbackAfterTimeout instructions feat: Impl RequestUndelegation and UndelegateWithRollbackAfterTimeout Jun 26, 2026
@snawaz snawaz force-pushed the snawaz/request-undelegation branch from ccbdd76 to 28bc3c1 Compare June 26, 2026 17:16

@GabrielePicco GabrielePicco left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Overall LGTM, but left some questions and comments


let request_accounts = match optional_accounts.len() {
0 => None,
2 => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

When an active request exists but the validator uses the existing 12-account undelegate path, this branch lets undelegation succeed without closing the request PDA. After that the delegated account is no longer DLP-owned and the delegation PDAs are closed, so neither re-request nor timeout rollback can validate and close the request, leaving request rent stranded and scanners with a stale pending request.

Comment thread dlp-api/Cargo.toml
serde = { version = "1.0.228", default-features = false, features = ["derive"] }

solana-pubkey-compat = { package = "solana-pubkey", version = "2.4", features = ["borsh", "bytemuck", "curve25519"] }
wheels = { git = "https://github.com/magicblock-labs/magicblock-wheels.git", rev = "5b83b56" }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We need to publish wheels otherwise the API crate won't be publishable anymore

solana --version
solana-keygen new --silent --no-bip39-passphrase

- name: log disk space

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks unintentional


#[variable_offset_layout(buffer_offset = 0)]
pub struct RequestUndelegationArgs {
pub timeout_slots: Option<u16>, // number of slots as timeout

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this shouldn't be configurable as it's otherwise an attack vector, should be removed and defined as a protocol constant


let timeout_slots = RequestUndelegationArgs::decode(data)?
.timeout_slots()
.unwrap_or(DEFAULT_UNDELEGATION_REQUEST_TIMEOUT_SLOTS as u16)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This should not be configurable trough args

DlpError::UndelegationRequestTimeoutTooShort
);

if is_uninitialized_account(undelegation_request_account) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We should return Ok in the else branch if we want this to be idempotent, or otherwise fail the transaction.

If there is already a pending request, it should either be honored or closed. Updating it adds unnecessary extra complexity, in my opinion, and potentially introduces an attack vector where someone can keep updating it, bumping the nonce and/or the timeout slot.

Comment on lines +44 to +46
/// request/re-request path for the delegated account. The timed-out rollback is
/// then restricted to the request rent payer so unrelated callers cannot race to
/// execute it. For non-empty accounts, the owner program must also accept the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Q: "The timed-out rollback is then restricted to the request rent payer so unrelated callers cannot race to execute it"

Racing is a desirable property to undelegate after the timeout, what's the advantage of this restriction?

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