Skip to content

Fix: handle lamports properly in CommitFinalize#148

Open
snawaz wants to merge 2 commits intomainfrom
snawaz/commit-finalize-lamports
Open

Fix: handle lamports properly in CommitFinalize#148
snawaz wants to merge 2 commits intomainfrom
snawaz/commit-finalize-lamports

Conversation

@snawaz
Copy link
Contributor

@snawaz snawaz commented Mar 9, 2026

#131 did not settle lamports. This PR does it properly.

CodeRabbit comment on #131:

image

Summary by CodeRabbit

  • New Features

    • Added lamport settlement logic in commit finalization to adjust account balances based on delegation requirements
    • Introduced lamport operation utilities for account balance management
  • Bug Fixes

    • Enhanced validation for rent-exempt accounts with new error handling
  • Tests

    • Expanded test coverage including lamport increase/decrease scenarios and insufficient rent error cases

Copy link
Contributor Author

snawaz commented Mar 9, 2026

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

@snawaz snawaz force-pushed the snawaz/commit-finalize-lamports branch 3 times, most recently from 124f94d to 17a016f Compare March 9, 2026 20:41
@snawaz snawaz requested a review from GabrielePicco March 9, 2026 21:30
@snawaz snawaz marked this pull request as ready for review March 9, 2026 21:30
@snawaz snawaz force-pushed the snawaz/commit-finalize-lamports branch from 17a016f to 625998f Compare March 9, 2026 21:31
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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/error.rs`:
- Around line 152-155: The error message on the InsufficientRent enum variant
uses incorrect verb agreement; update the #[error(...)] string for the
InsufficientRent variant so it reads "The account lamports are too small to make
the account rent-exempt" -> change "is" to "are" (i.e., "The account lamports
are too small to make the account rent-exempt") by editing the #[error(...)]
attribute above the InsufficientRent variant.

In `@src/processor/fast/internal/commit_finalize_internal.rs`:
- Around line 118-144: The delegation record is set from the account balance
after settling, which leaves excess lamports when the account initially had more
than delegation_record; update the assignment so delegation_record.lamports is
explicitly set to the target args.commit_lamports (instead of
args.delegated_account.lamports()), i.e., change the final reconciliation to
assign delegation_record.lamports = args.commit_lamports so the commit reaches
the intended value after the Transfer/increment/decrement logic in this function
handling args.commit_lamports, delegation_record, and args.delegated_account.

In `@tests/test_commit_finalize.rs`:
- Around line 319-378: Add a failing boundary test to ensure commit_finalize
rejects underfunded accounts: in tests/test_commit_finalize.rs (near
test_commit_finalize_lamports_decrease) add a case that computes the minimum
rent (Rent::minimum_balance) for the delegation record / delegated account data
length and sets commit_lamports to a value strictly less than that, build the
same commit_finalize instruction (using
dlp::instruction_builder::commit_finalize and CommitFinalizeArgs), send the
transaction with Transaction::new_signed_with_payer and call
banks.process_transaction(tx).await and assert it returns an error (use
unwrap_err() or is_err()), and optionally assert that neither the delegated PDA
nor the delegation_record PDA were created/changed (get_account returns None or
unchanged lamports); this ensures the rent guard prevents underfunded commits.
- Around line 302-317: The test currently asserts the destination balances
(delegated_account and DelegationRecord) but doesn't ensure the extra lamports
didn't come from the validator fees vault; capture the vault balance before the
operation and assert it is unchanged after the commit by fetching the vault
account (e.g., validator_fees_vault or the constant/ID used for the fees vault),
calling banks.get_account(VAULT_ID).await.unwrap().unwrap(), storing its
lamports, and then after the existing assertions
assert_eq!(vault_account.lamports, original_vault_lamports) to pin the increase
path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: af3d8ac0-1a91-4da0-927f-318ae7cb22aa

📥 Commits

Reviewing files that changed from the base of the PR and between 95ca88d and 17a016f.

📒 Files selected for processing (9)
  • src/error.rs
  • src/instruction_builder/commit_finalize.rs
  • src/instruction_builder/commit_finalize_from_buffer.rs
  • src/processor/fast/commit_finalize.rs
  • src/processor/fast/commit_finalize_from_buffer.rs
  • src/processor/fast/internal/commit_finalize_internal.rs
  • src/processor/fast/utils/mod.rs
  • src/processor/fast/utils/requires.rs
  • tests/test_commit_finalize.rs

Comment on lines +118 to +144
match args.commit_lamports.cmp(&delegation_record.lamports) {
std::cmp::Ordering::Greater => {
system::Transfer {
from: args.validator,
to: args.delegated_account,
lamports: args.commit_lamports - delegation_record.lamports,
}
.invoke()?;
}
std::cmp::Ordering::Less => {
let amount = delegation_record.lamports - args.commit_lamports;

args.delegated_account.lamports_decrement_by(amount)?;
args.validator_fees_vault.lamports_increment_by(amount)?;

require_ge!(
args.delegated_account.lamports(),
Rent::get()?
.try_minimum_balance(args.delegated_account.data_len())?,
DlpError::InsufficientRent
);
}
std::cmp::Ordering::Equal => {}
}

// Update the delegation record lamports after settling.
delegation_record.lamports = args.delegated_account.lamports();
Copy link
Contributor

@coderabbitai coderabbitai bot Mar 9, 2026

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:

#!/bin/bash
# Search for tests that exercise the excess lamports scenario
rg -n "delegation_record.*lamports|commit_lamports" --type rust -g "*test*" -C 3

# Check if there's documentation about expected behavior
rg -n "excess.*lamport|lamport.*excess" --type rust -C 2

Repository: magicblock-labs/delegation-program

Length of output: 4212


🏁 Script executed:

#!/bin/bash
# Find the setup function definition
rg -n "fn setup_program_test_env_with_record_lamports" --type rust -A 30

# Verify the exact implementation at line 144
sed -n '138,148p' src/processor/fast/internal/commit_finalize_internal.rs

Repository: magicblock-labs/delegation-program

Length of output: 2387


🏁 Script executed:

#!/bin/bash
# Find the require_ge check that allows account to have more lamports than record
sed -n '105,120p' src/processor/fast/internal/commit_finalize_internal.rs

# Also check what happens before the match statement - what's the state of the account?
sed -n '80,145p' src/processor/fast/internal/commit_finalize_internal.rs

Repository: magicblock-labs/delegation-program

Length of output: 2935


The lamport reconciliation logic has a bug when the account holds excess lamports.

The implementation sets delegation_record.lamports = args.delegated_account.lamports() at line 144, but this fails to achieve the target commit_lamports when delegated_account.lamports() > delegation_record.lamports initially (which is allowed by the require_ge! check at line 110).

Example (Less branch):

  • Initial: delegated_account.lamports() = 1000, delegation_record.lamports = 800, commit_lamports = 700
  • After decrement: delegated_account.lamports() = 900
  • Result: delegation_record.lamports = 900 (expected: 700)

Example (Greater branch):

  • Initial: delegated_account.lamports() = 1000, delegation_record.lamports = 800, commit_lamports = 900
  • After transfer: delegated_account.lamports() = 1100
  • Result: delegation_record.lamports = 1100 (expected: 900)

The amount calculations use delegation_record.lamports instead of delegated_account.lamports(), causing the reconciliation to leave excess lamports in the account. Change line 144 to delegation_record.lamports = args.commit_lamports to settle to the target value.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/processor/fast/internal/commit_finalize_internal.rs` around lines 118 -
144, The delegation record is set from the account balance after settling, which
leaves excess lamports when the account initially had more than
delegation_record; update the assignment so delegation_record.lamports is
explicitly set to the target args.commit_lamports (instead of
args.delegated_account.lamports()), i.e., change the final reconciliation to
assign delegation_record.lamports = args.commit_lamports so the commit reaches
the intended value after the Transfer/increment/decrement logic in this function
handling args.commit_lamports, delegation_record, and args.delegated_account.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No. Your reasoning is wrong. commit_lamports is not the target one, it is the current value on ER node but in the meanwhile, on the base chain, the delegated_account might have received lamports from others (but it cannot send it, because the account has been delegated to the delegation-program), so the only possibility is: delegated_account.lamports() can be equal or greater than the lamports which is delegated to the ER.

In the Less branch example: delegation_record.lamports = 900 is correct. (expected: 700) is wrong. Likewise, delegation_record.lamports = 1100 is correct.

and then delegation_record.lamports is set to the value on the base chain, not the ER.

Let me know if that makes sense now.

Copy link
Contributor

Choose a reason for hiding this comment

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

@snawaz, yes, that makes complete sense now — thank you for the detailed explanation. My earlier reasoning was flawed.

To summarize the correct model:

  • commit_lamports = the lamport balance as observed on the ER node (not a target).
  • delegation_record.lamports = the last recorded base-chain balance.
  • delegated_account.lamports() = the actual current base-chain balance, which may exceed delegation_record.lamports due to external transfers into the account (sends are blocked since the account is delegated to the program).

The match computes the delta relative to delegation_record.lamports, applies the transfer/decrement accordingly, and then delegation_record.lamports = args.delegated_account.lamports() correctly captures the true post-settlement base-chain balance — excess externally received lamports included. The logic is sound. I apologize for the noise.


✏️ Learnings added
Learnt from: snawaz
Repo: magicblock-labs/delegation-program PR: 148
File: src/processor/fast/internal/commit_finalize_internal.rs:118-144
Timestamp: 2026-03-10T08:59:47.915Z
Learning: In `delegation-program` (`src/processor/fast/internal/commit_finalize_internal.rs`), `commit_lamports` represents the lamport balance observed on the Ephemeral Rollup (ER) node, NOT a target base-chain balance. The match block computes a delta relative to `delegation_record.lamports` (last recorded base-chain value) and applies a transfer or decrement. After settling, `delegation_record.lamports = args.delegated_account.lamports()` is intentionally set to the actual post-settlement base-chain balance, which can exceed `commit_lamports` because `delegated_account` can receive lamports from external parties on the base chain (but cannot send, since it is delegated to the delegation program). The invariant `delegated_account.lamports() >= delegation_record.lamports` is enforced by the `require_ge!` check.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: snawaz
Repo: magicblock-labs/delegation-program PR: 148
File: src/error.rs:152-155
Timestamp: 2026-03-10T08:21:51.883Z
Learning: In the `delegation-program` repository (`src/error.rs`), the `InsufficientRent` error message uses "The account lamports is too small to make the account rent-exempt". The team intentionally uses "is" (not "are") because `lamports` is treated as a single field name (a `u64` value), not as a plural noun. Do not flag this as a grammar issue.

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: 1

♻️ Duplicate comments (2)
tests/test_commit_finalize.rs (2)

268-317: ⚠️ Potential issue | 🟠 Major

Add the below-rent rejection case.

The new lamport tests still only cover successful grow/shrink flows. Please add a case where CommitFinalizeArgs.lamports is Rent::default().minimum_balance(new_state.len()) - 1, assert process_transaction fails, and assert the delegated PDA plus DelegationRecord.lamports stay unchanged so the rent guard cannot regress silently. Based on learnings, in Solana tests using solana_program_test, use Rent::default() instead of Rent::get() because the rent sysvar is unavailable in the test context.

Also applies to: 319-378

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_commit_finalize.rs` around lines 268 - 317, Add a new negative
test case in the same test module (e.g., alongside
test_commit_finalize_lamports_increase) that builds a CommitFinalizeArgs where
lamports = Rent::default().minimum_balance(new_state_len) - 1 (use
Rent::default() not Rent::get()), call the instruction via
Transaction::new_signed_with_payer and assert
banks.process_transaction(tx).await.unwrap_err() (i.e., the transaction fails),
then reload the DELEGATED_PDA_ID account and pdas.delegation_record account and
assert both their lamports values remain unchanged (use
DelegationRecord::try_from_bytes_with_discriminator to read
delegation_record.lamports) to ensure the rent guard prevented regression.

302-317: ⚠️ Potential issue | 🟠 Major

Assert that the grow path does not drain validator_fees_vault.

This only proves the destination balances. It still passes if the extra lamports are sourced from validator_fees_vault instead of the validator, so please snapshot the vault before commit_finalize and assert it is unchanged afterwards.

Suggested test update
     let (ix, pdas) = dlp::instruction_builder::commit_finalize(
         authority.pubkey(),
         DELEGATED_PDA_ID,
         &mut CommitFinalizeArgs {
             commit_id: 1,
             allow_undelegation: false.into(),
             data_is_diff: false.into(),
             lamports: commit_lamports,
             bumps: Default::default(),
             reserved_padding: Default::default(),
         },
         &vec![1; 8],
     );
+    let initial_fees_vault_lamports = banks
+        .get_account(pdas.validator_fees_vault)
+        .await
+        .unwrap()
+        .unwrap()
+        .lamports;
 
     let tx = Transaction::new_signed_with_payer(
         &[ix],
         Some(&authority.pubkey()),
         &[&authority],
@@
     let delegation_record =
         DelegationRecord::try_from_bytes_with_discriminator(
             &delegation_record_account.data,
         )
         .unwrap();
     assert_eq!(delegation_record.lamports, commit_lamports);
+
+    let fees_vault = banks
+        .get_account(pdas.validator_fees_vault)
+        .await
+        .unwrap()
+        .unwrap();
+    assert_eq!(fees_vault.lamports, initial_fees_vault_lamports);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_commit_finalize.rs` around lines 302 - 317, Before invoking
commit_finalize in this test, snapshot the lamports of the validator_fees_vault
account by calling
banks.get_account(validator_fees_vault).await.unwrap().unwrap().lamports and
store it (e.g., pre_vault_lamports), then after the existing assertions
(delegated_account and delegation_record checks) fetch the vault account again
and assert its lamports equal pre_vault_lamports; reference the
validator_fees_vault identifier and the commit_finalize test flow so the
snapshot is taken immediately before the operation and the equality check is
done immediately after.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/processor/fast/internal/commit_finalize_internal.rs`:
- Around line 118-125: The top-up branch that creates and invokes
system::Transfer (using args.validator -> args.delegated_account with lamports =
commit_lamports - delegation_record.lamports) requires the funding account
(args.validator) to be [WRITE, SIGNER]; currently the validator is emitted
readonly causing CPIs to fail. Fix by ensuring the account metas passed into
this instruction (the account at index 0 / the validator account) are marked
writable and signer before the branch executes (or change the builder that
constructs the instruction metas so args.validator is emitted as writable), so
the system::Transfer::invoke() can succeed.

---

Duplicate comments:
In `@tests/test_commit_finalize.rs`:
- Around line 268-317: Add a new negative test case in the same test module
(e.g., alongside test_commit_finalize_lamports_increase) that builds a
CommitFinalizeArgs where lamports =
Rent::default().minimum_balance(new_state_len) - 1 (use Rent::default() not
Rent::get()), call the instruction via Transaction::new_signed_with_payer and
assert banks.process_transaction(tx).await.unwrap_err() (i.e., the transaction
fails), then reload the DELEGATED_PDA_ID account and pdas.delegation_record
account and assert both their lamports values remain unchanged (use
DelegationRecord::try_from_bytes_with_discriminator to read
delegation_record.lamports) to ensure the rent guard prevented regression.
- Around line 302-317: Before invoking commit_finalize in this test, snapshot
the lamports of the validator_fees_vault account by calling
banks.get_account(validator_fees_vault).await.unwrap().unwrap().lamports and
store it (e.g., pre_vault_lamports), then after the existing assertions
(delegated_account and delegation_record checks) fetch the vault account again
and assert its lamports equal pre_vault_lamports; reference the
validator_fees_vault identifier and the commit_finalize test flow so the
snapshot is taken immediately before the operation and the equality check is
done immediately after.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: eccdb8b5-9a3a-47ce-9cf0-c0b911686d1e

📥 Commits

Reviewing files that changed from the base of the PR and between 17a016f and 625998f.

📒 Files selected for processing (9)
  • src/error.rs
  • src/instruction_builder/commit_finalize.rs
  • src/instruction_builder/commit_finalize_from_buffer.rs
  • src/processor/fast/commit_finalize.rs
  • src/processor/fast/commit_finalize_from_buffer.rs
  • src/processor/fast/internal/commit_finalize_internal.rs
  • src/processor/fast/utils/mod.rs
  • src/processor/fast/utils/requires.rs
  • tests/test_commit_finalize.rs

@magicblock-labs magicblock-labs deleted a comment from coderabbitai bot Mar 10, 2026
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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/test_commit_finalize.rs`:
- Around line 102-103: Remove the leftover commented debug line
"//tokio::time::sleep(Duration::from_secs(10)).await;" from the
test_commit_finalize test in tests/test_commit_finalize.rs; simply delete that
commented sleep so the test file contains no commented-out debug code.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9984f2a0-3247-4a1b-87b0-6e156d2f35b8

📥 Commits

Reviewing files that changed from the base of the PR and between 95ca88d and b864cb4.

📒 Files selected for processing (10)
  • src/error.rs
  • src/instruction_builder/commit_finalize.rs
  • src/instruction_builder/commit_finalize_from_buffer.rs
  • src/processor/fast/commit_finalize.rs
  • src/processor/fast/commit_finalize_from_buffer.rs
  • src/processor/fast/internal/commit_finalize_internal.rs
  • src/processor/fast/utils/mod.rs
  • src/processor/fast/utils/requires.rs
  • tests/test_commit_finalize.rs
  • tests/test_commit_finalize_from_buffer.rs

@magicblock-labs magicblock-labs deleted a comment from coderabbitai bot Mar 10, 2026
@snawaz snawaz force-pushed the snawaz/commit-finalize-lamports branch from b864cb4 to 1755f38 Compare March 10, 2026 12:51
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: 3

Caution

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

⚠️ Outside diff range comments (3)
src/instruction_builder/commit_finalize_from_buffer.rs (1)

78-91: ⚠️ Potential issue | 🟡 Minor

Size budget includes account not in instruction.

The commit_finalize_from_buffer_size_budget function includes program_config_pda (line 89) in the account size calculation, but this account is not part of the instruction's account list (lines 50-58 show only 7 accounts, none of which is program_config).

This will over-estimate the data size budget, which is wasteful but not incorrect. However, it may indicate stale code from a previous version.

🔧 Proposed fix
 pub fn commit_finalize_from_buffer_size_budget(
     delegated_account: AccountSizeClass,
 ) -> u32 {
     total_size_budget(&[
         DLP_PROGRAM_DATA_SIZE_CLASS,
         AccountSizeClass::Tiny, // validator
         delegated_account,      // delegated_account
         AccountSizeClass::Tiny, // delegation_record_pda
         AccountSizeClass::Tiny, // delegation_metadata_pda
         delegated_account,      // data_buffer
         AccountSizeClass::Tiny, // validator_fees_vault_pda
-        AccountSizeClass::Tiny, // program_config_pda
         AccountSizeClass::Tiny, // system_program
     ])
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/instruction_builder/commit_finalize_from_buffer.rs` around lines 78 - 91,
The size budget function commit_finalize_from_buffer_size_budget is including an
AccountSizeClass::Tiny entry annotated as program_config_pda that is not
actually part of the instruction's account list; remove that program_config_pda
entry from the array passed to total_size_budget so the returned budget matches
the real accounts (keep the other entries: DLP_PROGRAM_DATA_SIZE_CLASS,
validator, delegated_account, delegation_record_pda, delegation_metadata_pda,
data_buffer, validator_fees_vault_pda, and system_program). Also scan for any
stale comments/annotations around program_config_pda in
commit_finalize_from_buffer_size_budget and delete them so the function reflects
the current instruction accounts.
tests/test_commit_finalize.rs (1)

189-214: 🧹 Nitpick | 🔵 Trivial

Add a regression case where the base-chain account already has extra lamports.

The new fixture still forces delegated_account.lamports == delegation_record.lamports, so this suite won’t catch a future refactor that incorrectly persists args.commit_lamports instead of the actual post-settlement base-chain balance. Let the helper take separate account and record lamports, then add one increase/decrease case with delegated_account.lamports() > delegation_record.lamports.

Based on learnings: commit_lamports is the ER-observed balance, while delegated_account.lamports() may already be higher on the base chain and delegation_record.lamports is intentionally updated to that actual post-settlement balance.

Also applies to: 269-403

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_commit_finalize.rs` around lines 189 - 214, The test helper
setup_program_test_env_with_record_lamports currently forces the delegated
account and delegation_record to have identical lamports; change it to accept
separate "delegated_account_lamports" and "delegation_record_lamports" (keep the
function name setup_program_test_env_with_record_lamports but update its
parameters and usage) and initialize the ProgramTest added account
(DELEGATED_PDA_ID) with delegated_account_lamports while populating the stored
delegation record with delegation_record_lamports; then add a regression test
case where delegated_account_lamports > delegation_record_lamports to ensure the
code uses the actual base-chain balance (delegated_account.lamports()) rather
than args.commit_lamports when persisting the final delegation_record.lamports
(apply the same change pattern to all affected tests around the other ranges
mentioned).
src/processor/fast/internal/commit_finalize_internal.rs (1)

68-78: ⚠️ Potential issue | 🟠 Major

Don’t require validator_fees_vault writable unless the commit shrinks lamports.

Only std::cmp::Ordering::Less mutates the vault, but this precheck now forces a write lock on every finalize. Because the vault PDA is shared per validator (src/pda.rs:163-169), that serializes otherwise unrelated commits for the same validator and rejects callers that still pass the legacy readonly meta even when the vault is never touched.

🔧 Suggested change
     require_initialized_pda_fast!(
         args.validator_fees_vault,
         &[
             pda::VALIDATOR_FEES_VAULT_TAG,
             args.validator.address().as_ref(),
             &[args.bumps.validator_fees_vault],
             crate::fast::ID.as_ref(),
             PDA_MARKER
         ],
-        true
+        false
     );
...
         std::cmp::Ordering::Less => {
+            require!(args.validator_fees_vault.is_writable(), ProgramError::Immutable);
             let amount = delegation_record.lamports - args.commit_lamports;
 
             args.delegated_account.lamports_decrement_by(amount)?;
             args.validator_fees_vault.lamports_increment_by(amount)?;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/processor/fast/internal/commit_finalize_internal.rs` around lines 68 -
78, The require_initialized_pda_fast! call currently forces the
validator_fees_vault account to be writable unconditionally; change it to only
require writable when the finalize actually decreases the vault lamports (i.e.,
when the operation is mutating). Detect that by comparing the before/after
lamports (or using the existing delta/ordering value) and pass writable = true
only when the comparison yields std::cmp::Ordering::Less (otherwise pass
writable = false). Update the require_initialized_pda_fast! invocation that
references args.validator_fees_vault and args.bumps.validator_fees_vault so the
writable flag is conditional on the lamports-decrease check.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/processor/fast/commit_finalize_from_buffer.rs`:
- Around line 20-26: The top doc comment listing accounts in
commit_finalize_from_buffer.rs incorrectly marks the validator only as
`[signer]`; update that first entry to `[signer, writable]` to match the
instruction builder and the same fix in commit_finalize.rs so the validator
account documentation is consistent with the actual account flags (update the
comment line that currently reads "0: `[signer]`   the validator requesting the
commit" to include `[writable]`).

In `@src/processor/fast/commit_finalize.rs`:
- Around line 21-26: Update the doc comment in commit_finalize.rs to mark the
validator account as writable to match the instruction builder; change the first
entry from "`[signer]`   the validator requesting the commit" to include
"`[writable]`" (e.g., "`[signer, writable]`" or similar) so it reflects
AccountMeta::new(validator, true) used when building the instruction and keeps
the comments consistent with the actual account metas.

In `@src/processor/fast/internal/commit_finalize_internal.rs`:
- Around line 116-146: After the match that adjusts lamports (the block using
args.commit_lamports.cmp(&delegation_record.lamports)), add a single
rent-exemption check after the match to ensure the delegated account remains
rent-exempt regardless of branch; call Rent::get()? and use
try_minimum_balance(args.delegated_account.data_len()) to compute the minimum
and require_ge! that args.delegated_account.lamports() meets that minimum,
returning DlpError::InsufficientRent on failure — this uses the existing
args.delegated_account, Rent::get(), try_minimum_balance, and
DlpError::InsufficientRent symbols and should be placed immediately after the
match and before updating delegation_record.lamports.

---

Outside diff comments:
In `@src/instruction_builder/commit_finalize_from_buffer.rs`:
- Around line 78-91: The size budget function
commit_finalize_from_buffer_size_budget is including an AccountSizeClass::Tiny
entry annotated as program_config_pda that is not actually part of the
instruction's account list; remove that program_config_pda entry from the array
passed to total_size_budget so the returned budget matches the real accounts
(keep the other entries: DLP_PROGRAM_DATA_SIZE_CLASS, validator,
delegated_account, delegation_record_pda, delegation_metadata_pda, data_buffer,
validator_fees_vault_pda, and system_program). Also scan for any stale
comments/annotations around program_config_pda in
commit_finalize_from_buffer_size_budget and delete them so the function reflects
the current instruction accounts.

In `@src/processor/fast/internal/commit_finalize_internal.rs`:
- Around line 68-78: The require_initialized_pda_fast! call currently forces the
validator_fees_vault account to be writable unconditionally; change it to only
require writable when the finalize actually decreases the vault lamports (i.e.,
when the operation is mutating). Detect that by comparing the before/after
lamports (or using the existing delta/ordering value) and pass writable = true
only when the comparison yields std::cmp::Ordering::Less (otherwise pass
writable = false). Update the require_initialized_pda_fast! invocation that
references args.validator_fees_vault and args.bumps.validator_fees_vault so the
writable flag is conditional on the lamports-decrease check.

In `@tests/test_commit_finalize.rs`:
- Around line 189-214: The test helper
setup_program_test_env_with_record_lamports currently forces the delegated
account and delegation_record to have identical lamports; change it to accept
separate "delegated_account_lamports" and "delegation_record_lamports" (keep the
function name setup_program_test_env_with_record_lamports but update its
parameters and usage) and initialize the ProgramTest added account
(DELEGATED_PDA_ID) with delegated_account_lamports while populating the stored
delegation record with delegation_record_lamports; then add a regression test
case where delegated_account_lamports > delegation_record_lamports to ensure the
code uses the actual base-chain balance (delegated_account.lamports()) rather
than args.commit_lamports when persisting the final delegation_record.lamports
(apply the same change pattern to all affected tests around the other ranges
mentioned).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7db01719-0a91-4593-a605-e574cefc2ea3

📥 Commits

Reviewing files that changed from the base of the PR and between 95ca88d and 1755f38.

📒 Files selected for processing (10)
  • src/error.rs
  • src/instruction_builder/commit_finalize.rs
  • src/instruction_builder/commit_finalize_from_buffer.rs
  • src/processor/fast/commit_finalize.rs
  • src/processor/fast/commit_finalize_from_buffer.rs
  • src/processor/fast/internal/commit_finalize_internal.rs
  • src/processor/fast/utils/mod.rs
  • src/processor/fast/utils/requires.rs
  • tests/test_commit_finalize.rs
  • tests/test_commit_finalize_from_buffer.rs

Comment on lines 20 to +26
/// 0: `[signer]` the validator requesting the commit
/// 1: `[]` the delegated account
/// 2: `[writable]` the PDA storing the new state
/// 3: `[writable]` the PDA storing the commit record
/// 4: `[]` the delegation record
/// 5: `[writable]` the delegation metadata
/// 6: `[]` the validator fees vault
/// 1: `[writable]` the delegated account
/// 2: `[writable]` the delegation record
/// 3: `[writable]` the delegation metadata
/// 4: `[]` the data buffer
/// 5: `[writable]` the validator fees vault
/// 6: `[]` system program
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 | 🟡 Minor

Doc comment missing [writable] marker for validator account.

Same as in commit_finalize.rs: line 20 shows the validator as [signer] only, but the instruction builder marks it as writable. The doc comment should be updated for consistency.

📝 Proposed fix
 /// Accounts:
 ///
-/// 0: `[signer]`   the validator requesting the commit
+/// 0: `[signer, writable]` the validator requesting the commit
 /// 1: `[writable]` the delegated account
 /// 2: `[writable]` the delegation record
 /// 3: `[writable]` the delegation metadata
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/processor/fast/commit_finalize_from_buffer.rs` around lines 20 - 26, The
top doc comment listing accounts in commit_finalize_from_buffer.rs incorrectly
marks the validator only as `[signer]`; update that first entry to `[signer,
writable]` to match the instruction builder and the same fix in
commit_finalize.rs so the validator account documentation is consistent with the
actual account flags (update the comment line that currently reads "0:
`[signer]`   the validator requesting the commit" to include `[writable]`).

Comment on lines 21 to 26
/// 0: `[signer]` the validator requesting the commit
/// 1: `[]` the delegated account
/// 2: `[]` the delegation record
/// 1: `[writable]` the delegated account
/// 2: `[writable]` the delegation record
/// 3: `[writable]` the delegation metadata
/// 4: `[]` the validator fees vault
/// 4: `[writable]` the validator fees vault
/// 5: `[]` system program
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 | 🟡 Minor

Doc comment missing [writable] marker for validator account.

Line 21 shows the validator as [signer] only, but the instruction builder marks it as writable (AccountMeta::new(validator, true)). The doc comment should be updated for consistency.

📝 Proposed fix
 /// Accounts:
 ///
-/// 0: `[signer]`   the validator requesting the commit
+/// 0: `[signer, writable]` the validator requesting the commit
 /// 1: `[writable]` the delegated account
 /// 2: `[writable]` the delegation record
 /// 3: `[writable]` the delegation metadata
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/processor/fast/commit_finalize.rs` around lines 21 - 26, Update the doc
comment in commit_finalize.rs to mark the validator account as writable to match
the instruction builder; change the first entry from "`[signer]`   the validator
requesting the commit" to include "`[writable]`" (e.g., "`[signer, writable]`"
or similar) so it reflects AccountMeta::new(validator, true) used when building
the instruction and keeps the comments consistent with the actual account metas.

Comment on lines 116 to +146
args.delegated_account.resize(args.new_state.data_len())?;

match args.commit_lamports.cmp(&delegation_record.lamports) {
std::cmp::Ordering::Greater => {
require!(args.validator.is_writable(), ProgramError::Immutable);
system::Transfer {
from: args.validator,
to: args.delegated_account,
lamports: args.commit_lamports - delegation_record.lamports,
}
.invoke()?;
}
std::cmp::Ordering::Less => {
let amount = delegation_record.lamports - args.commit_lamports;

args.delegated_account.lamports_decrement_by(amount)?;
args.validator_fees_vault.lamports_increment_by(amount)?;

// require the account is still rent-exempted even after decrementing lamports
require_ge!(
args.delegated_account.lamports(),
Rent::get()?
.try_minimum_balance(args.delegated_account.data_len())?,
DlpError::InsufficientRent
);
}
std::cmp::Ordering::Equal => {}
}

// Update the delegation record lamports after settling.
delegation_record.lamports = args.delegated_account.lamports();
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 | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In pinocchio 0.10.1 / Solana realloc semantics, does AccountView::resize()enforce rent exemption when increasing account data length, or should programs explicitly verifylamports >= Rent::minimum_balance(data_len) after resizing?

💡 Result:

AccountView::resize() in the solana-account-view v1.x that Pinocchio 0.10.1 depends on does not enforce rent exemption. It only checks borrow state, updates data_len/resize_delta, enforces the max growth limit, and zero-fills any newly-added bytes—there is no lamports/rent check anywhere in the implementation. [1]

So if your program increases an account’s data length, you should explicitly ensure it remains rent-exempt, i.e. fund/check lamports >= Rent::minimum_balance(new_data_len) (typically fund before resizing so you can fail early). This is also the approach reflected by Pinocchio’s system helper, which computes the rent-exempt minimum, transfers missing lamports, and only then calls resize_unchecked. [2] Solana guidance likewise notes you should ensure an account remains rent-exempt when resizing. [3]

Answer: AccountView::resize() does not enforce rent exemption; programs should explicitly verify/fund to meet Rent::minimum_balance(data_len) when growing account data. [1][2][3]

References:
[1] solana-account-view v1.0.0 AccountView::resize / resize_unchecked source (no lamports/rent checks) (docs.rs)
[2] pinocchio-system create_account_with_minimum_balance_signed (compute minimum balance + transfer, then resize) (docs.rs)
[3] Solana course note: “Always ensure your account remains rent-exempt before resizing.” (solana.com)

Citations:


Add a post-match rent-exemption check for the delegated account.

AccountView::resize() does not enforce rent-exemption when growing account data. The explicit InsufficientRent guard only runs in the Less branch. If resizing increases the data length, the Greater and Equal branches could leave the account below the rent minimum despite transferring or moving lamports. Add a single check after the match block to verify rent-exemption holds regardless of which branch executed:

require_ge!(
    args.delegated_account.lamports(),
    Rent::get()?
        .try_minimum_balance(args.delegated_account.data_len())?,
    DlpError::InsufficientRent
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/processor/fast/internal/commit_finalize_internal.rs` around lines 116 -
146, After the match that adjusts lamports (the block using
args.commit_lamports.cmp(&delegation_record.lamports)), add a single
rent-exemption check after the match to ensure the delegated account remains
rent-exempt regardless of branch; call Rent::get()? and use
try_minimum_balance(args.delegated_account.data_len()) to compute the minimum
and require_ge! that args.delegated_account.lamports() meets that minimum,
returning DlpError::InsufficientRent on failure — this uses the existing
args.delegated_account, Rent::get(), try_minimum_balance, and
DlpError::InsufficientRent symbols and should be placed immediately after the
match and before updating delegation_record.lamports.

@magicblock-labs magicblock-labs deleted a comment from coderabbitai bot Mar 10, 2026
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.

1 participant