Skip to content

solana: reclaim rent for old accounts#4746

Open
kcsongor wants to merge 5 commits intowormhole-foundation:mainfrom
wormholelabs-xyz:solana/reclaim-rent
Open

solana: reclaim rent for old accounts#4746
kcsongor wants to merge 5 commits intowormhole-foundation:mainfrom
wormholelabs-xyz:solana/reclaim-rent

Conversation

@kcsongor
Copy link
Copy Markdown
Contributor

No description provided.

@johnsaigle
Copy link
Copy Markdown
Contributor

@claude

(I think this is broken unfortunately, but giving it a whirl)

@github-actions
Copy link
Copy Markdown

Claude Code is working…

I'll analyze this and get back to you.

View job run

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds rent-reclamation capabilities to the Solana bridge program by introducing close instructions for old message/VAA-related accounts (with a 30-day retention policy), and extends the solitaire! macro to optionally support Anchor-style event CPI dispatch guarding (enabled for the bridge program).

Changes:

  • Add optional Anchor event CPI dispatch guard to solitaire! and enable it for the bridge program.
  • Introduce close_posted_message and close_signature_set_and_posted_vaa instructions to reclaim rent, plus supporting error variants and instruction builders.
  • Populate submission_time for PostedVAA and expand integration tests/helpers to validate retention boundaries and rent reclamation flows.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
solana/solitaire/program/src/macros.rs Adds @event_cpi mode and a dispatch guard for Anchor event CPI selector.
solana/solitaire/program/src/lib.rs Defines event CPI tag/seed constants used by the guard and bridge program.
solana/bridge/program/src/lib.rs Enables event CPI support and wires new close instructions into dispatch.
solana/bridge/program/src/api.rs Exposes the new close APIs.
solana/bridge/program/src/api/close_posted_message.rs Implements posted message closure with retention checks and event emission.
solana/bridge/program/src/api/close_signature_set_and_posted_vaa.rs Implements signature set + posted VAA closure logic with retention/guardian-set checks.
solana/bridge/program/src/api/post_vaa.rs Sets submission_time when posting VAAs; exposes check_active for reuse.
solana/bridge/program/src/instructions.rs Adds client-side instruction constructors for the new close instructions.
solana/bridge/program/src/error.rs Adds new program error variants for close/retention/event validation.
solana/bridge/program/tests/common.rs Switches to ProgramTestContext and adds helpers for close instructions + patching submission_time.
solana/bridge/program/tests/integration.rs Refactors test harness and adds extensive tests for event CPI guard and rent reclamation behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

data: ix_data,
};

invoke_signed(&ix, ctx.accounts, &[&[EVENT_AUTHORITY_SEED, &[bump]]])?;
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

invoke_signed is called with ctx.accounts, but the CPI instruction declares only one account (expected_authority). The AccountInfos passed to invoke_signed must be in the same order/shape as ix.accounts, otherwise the CPI will fail due to account mismatch. Pass only the event authority AccountInfo (or update ix.accounts to match the provided slice) so the first account is the PDA signer expected by the event guard.

Suggested change
invoke_signed(&ix, ctx.accounts, &[&[EVENT_AUTHORITY_SEED, &[bump]]])?;
invoke_signed(
&ix,
&[event_authority.clone()],
&[&[EVENT_AUTHORITY_SEED, &[bump]]],
)?;

Copilot uses AI. Check for mistakes.
Comment on lines +84 to +90
let message_data = MessageData::try_from_slice(&data[3..])
.map_err(|_| SolitaireError::ProgramError(ProgramError::InvalidAccountData))?;

// Check retention: submission_time must be at or before (now - 30 days).
if (message_data.submission_time as i64) > accs.clock.unix_timestamp - RETENTION_PERIOD {
return Err(MessageWithinRetentionWindow.into());
}
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

The retention check treats submission_time == 0 as "very old" and will allow immediate closure. If 0 is used by legacy accounts created before submission_time was populated, this bypasses the intended 30-day grace period (note the VAA close path handles this via DEFAULT_SUBMISSION_TIME). Consider applying the same default/grace logic here.

Copilot uses AI. Check for mistakes.
Comment on lines +47 to +51
if $a.is_empty() || !$a[0].is_signer {
return Err(SolitaireError::InvalidSigner(
solana_program::pubkey::Pubkey::default(),
));
}
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

When the account list is non-empty but the first account is not a signer, this returns InvalidSigner(Pubkey::default()), which loses the offending key and makes debugging harder. Consider returning InvalidSigner(*a[0].key) when available, and only use default when the slice is empty.

Suggested change
if $a.is_empty() || !$a[0].is_signer {
return Err(SolitaireError::InvalidSigner(
solana_program::pubkey::Pubkey::default(),
));
}
if $a.is_empty() {
return Err(SolitaireError::InvalidSigner(
solana_program::pubkey::Pubkey::default(),
));
}
if !$a[0].is_signer {
return Err(SolitaireError::InvalidSigner(*$a[0].key));
}

Copilot uses AI. Check for mistakes.
let message_lamports =
common::get_account_balance(&mut test_ctx.banks_client, message_key).await;

// Set submission_time to 0 (epoch) so it passes the 30-day retention check.
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

Comment mismatch: this sets submission_time to old_submission_time() (now - 31 days), not 0/epoch. Please update the comment to reflect what the test is doing.

Suggested change
// Set submission_time to 0 (epoch) so it passes the 30-day retention check.
// Set submission_time to an old value (via old_submission_time(), e.g. now - 31 days)
// so it passes the 30-day retention check.

Copilot uses AI. Check for mistakes.
Comment on lines +539 to +540
.unwrap()
.unwrap();
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

account_data.data[41..45] will panic with an out-of-bounds slice if the account is missing or the layout changes. Adding a length check (and ideally validating the 3-byte prefix) would make failures clearer and avoid hard-to-diagnose panics in tests.

Suggested change
.unwrap()
.unwrap();
.expect("failed to fetch account for submission_time patch")
.expect("cannot patch submission_time: account does not exist");
assert!(
account_data.data.len() >= 45,
"cannot patch submission_time: account data too short (expected at least 45 bytes, got {})",
account_data.data.len()
);
assert!(
matches!(&account_data.data[..3], b"msg" | b"msu" | b"vaa"),
"cannot patch submission_time: unexpected account prefix {:?}",
&account_data.data[..3]
);

Copilot uses AI. Check for mistakes.
Comment on lines +131 to +135
// Close PostedVAA: transfer lamports to fee_collector.
let vaa_lamports = accs.posted_vaa.lamports();
**accs.fee_collector.lamports.borrow_mut() += vaa_lamports;
**accs.posted_vaa.lamports.borrow_mut() = 0;
accs.posted_vaa.data.borrow_mut().fill(0);
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

Lamports are accumulated with += without overflow checking. Elsewhere the program uses checked arithmetic (e.g., MathOverflow) for lamport math; consider using checked_add here as well to avoid potential wraparound.

Copilot uses AI. Check for mistakes.
Comment on lines +151 to +155
// 6. Close signature_set: transfer lamports to fee_collector.
let sig_lamports = accs.signature_set.lamports();
**accs.fee_collector.lamports.borrow_mut() += sig_lamports;
**accs.signature_set.lamports.borrow_mut() = 0;
accs.signature_set.data.borrow_mut().fill(0);
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

Lamports are accumulated with += without overflow checking. Elsewhere the program uses checked arithmetic (e.g., MathOverflow) for lamport math; consider using checked_add here as well to avoid potential wraparound.

Copilot uses AI. Check for mistakes.
) -> Result<()> {
// Verify self_program is actually our program.
if self_program.key != ctx.program_id {
return Err(InvalidEventAuthority.into());
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

This branch checks self_program.key != ctx.program_id but returns InvalidEventAuthority, which is misleading (the program account is what's invalid here, not the event authority). Consider returning a more accurate error variant for easier diagnosis.

Suggested change
return Err(InvalidEventAuthority.into());
return Err(InvalidProgramOwner.into());

Copilot uses AI. Check for mistakes.
Comment thread solana/bridge/program/src/api/close_signature_set_and_posted_vaa.rs
Comment thread solana/bridge/program/src/api/close_posted_message.rs
Comment thread solana/bridge/program/src/api/close_posted_message.rs
@kcsongor kcsongor force-pushed the solana/reclaim-rent branch 2 times, most recently from 21aeea0 to 40562e5 Compare April 13, 2026 15:06
@kcsongor kcsongor force-pushed the solana/reclaim-rent branch from 40562e5 to e3bb812 Compare April 13, 2026 16:08
Comment thread solana/bridge/program/src/api/close_posted_message.rs Outdated
@evan-gray evan-gray dismissed their stale review April 14, 2026 15:11

changes made

The main challenge in doing this is figuring out when we're on testnet,
since the solana contract only indirectly encodes this by taking the
contract address as an env variable.

The approach we take is minimally invasive in that it doesn't require
introducing a new flag. Instead, we just check that the contract address
is the testnet address.
@aleksiko17
Copy link
Copy Markdown

I staked my W coins a few months ago on the official website https://w.wormhole.com in the Solana network. Everything was displaying correctly before, but now my staked coins are no longer visible in the dashboard. What could be the reason?

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.

5 participants