Skip to content

feat(m5): GraveVault salvage_pool execution path#19

Open
graveyieldprotocol wants to merge 5 commits into
mainfrom
feat/m5-grave-vault-salvage-execution-path
Open

feat(m5): GraveVault salvage_pool execution path#19
graveyieldprotocol wants to merge 5 commits into
mainfrom
feat/m5-grave-vault-salvage-execution-path

Conversation

@graveyieldprotocol

@graveyieldprotocol graveyieldprotocol commented May 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

Wires the full Raydium V4 SOL/X salvage flow end-to-end. salvage_pool now actually executes a salvage instead of just running pre-flight gates:

  1. Pre-flight (m3, unchanged)
  2. Lazy-init system PDAs (lp_holder_pool_vault, vault_sol_holding_account)
  3. Snapshot validation: assert params.lp_total_supply_at_snapshot == lp_mint.supply (InvalidSnapshotData 7018 otherwise)
  4. Atomic salvor→vault LP transfer (salvor signs; SPL Transfer CPI)
  5. AMM remove_liquidity dispatch by pool.owner — Raydium V4 real, CLMM/Orca/PumpSwap revert AmmCpiUnimplemented 7017 (honest-stub pattern)
  6. Jupiter v6 swap (memecoin → WSOL) if memecoin balance ≥ dust threshold; skipped with a log otherwise
  7. Slippage assertion on the Jupiter swap leg (post-swap vault_base.amount - removal.base_received ≥ min_quote_output_lamports)
  8. WSOL → native SOL unwrap via spl_token::close_account(dest=vault_sol_holding_account)
  9. 40/40/20 distribution via three system_program::transfer calls (vault_authority PDA-signs); u128 math; rounding remainder → protocol
  10. Populate PoolRegistry + SalvageReceipt; emit PoolSalvaged + SalvageCompleted

PR scope (Option B, code-path split)

This PR is the salvage_pool side. The matching claim_lp_proceeds Merkle verification + ClaimRecord double-claim defense + SOL transfer from lp_holder_pool_vault ships in a follow-up PR (m6). No code overlap between the two PRs — they touch different handler files.

Locked design (from prior thread design lock)

  • Source of LP: atomic salvor→vault SPL transfer inside salvage_pool. vault_authority PDA-signs source_lp_owner for the Raydium V4 withdraw via invoke_signed. Canon doc + m3 TODO comment both confirm this is the intended shape; the other two design options (salvor signs withdraw directly / separate deposit ix) were ruled out.
  • Merkle root location: PoolRegistry.lp_snapshot_merkle_root (not SalvageReceipt). m6's claim path will verify against PoolRegistry.
  • Error codes: append at 7015–7019 in lock-step with docs/error_codes.md (sync convention I memorized as a failure pattern).
  • Base token scope: WSOL only for v1.0. USDC/USDT base reverts UnsupportedBaseToken 7019 (v1.1 needs a token-account variant of lp_holder_pool_vault + protocol_treasury).

What's in this PR

New files (programs/grave-vault/src/cpi/):

  • mod.rs — dispatcher by pool.owner
  • raydium_v4.rs — real Raydium V4 withdraw CPI (18-account list per processor.rs::process_withdraw, 9-byte data [tag=4][amount_le], amm_authority constant validation against 5Q544…)
  • jupiter.rs — Jupiter v6 swap CPI (forwards salvor's pre-computed route data + accounts; vault_authority signs)
  • raydium_clmm.rs, orca_whirlpool.rs, pump_swap.rs — honest stubs (revert AmmCpiUnimplemented)

Modified:

  • lib.rs+ pub mod cpi;; salvage_pool signature now threads 'info explicitly per Anchor 0.31+ lifetime invariance (failure-pattern memory)
  • errors.rs — 5 new codes 7015–7019 (explicit discriminants matching the on-main pattern)
  • constants.rs — new PDA seeds (VAULT_AUTHORITY_SEED, VAULT_SOL_HOLDING_SEED), all AMM + Jupiter program IDs, Raydium V4 layout constants, BPS_DENOMINATOR, HARD_MAX_SLIPPAGE_BPS
  • instructions/salvage_pool.rs — full handler rewrite + extended Accounts struct (11 new accounts: vault_authority, vault_sol_holding_account, salvor_lp_token_account, vault_lp_token_account, vault_base_token_account, vault_memecoin_token_account, lp_mint, memecoin_mint, wsol_mint pinned to So111…112, token_program, associated_token_program)
  • docs/error_codes.md — append rows 7015–7019 + header range bump 7000-7014 → 7000-7019 + sync-convention note (per the failure-pattern memory on spec-doc drift)
  • docs/PRE_MAINNET_CHECKLIST.md — 4 new rows: CPI-006/007/008 (stub adapters), CPI-009 (Raydium V4 account-ordering verification, marked 🟥 blocking)
  • CHANGELOG.md — m5 entry with full added/changed/unverified breakdown

Test plan

Host-side (cargo check / clippy / fmt) is the local verification ceiling — deferring BPF + integration to CI per the validated parallel-track cadence.

  • cargo fmt --check green (local verification deferred; CI will catch)
  • cargo clippy --all-targets -- -D warnings green
  • anchor build green (per the recent PR docs: add error_codes.md aligned with on-main errors.rs #17 evidence, cargo-binstall is working; if it intermittently fails we'll iterate)
  • terminology-lint green (pre-checked locally: PASS, no forbidden vocabulary)
  • pnpm typecheck green (untouched; should remain green)
  • CodeRabbit review (mandatory pre-merge)
  • Host unit tests — deferred to a follow-up commit on this PR if CodeRabbit or you ask for them; the architectural surface is the priority for this initial push

Unverified (transparent)

  • Live Raydium V4 fork test of the exact 18-account ordering. The amm_authority constant check (account [0] of remaining_accounts must equal 5Q544…) catches an obviously-wrong layout but not subtle swaps like open_orders ↔ target_orders. Tracked as PRE_MAINNET_CHECKLIST.md row CPI-009 (🟥 blocking).
  • Real Jupiter v6 route execution. The CPI helper forwards salvor's pre-computed route_data + accounts verbatim; verification is localnet smoke post-merge.
  • Pool base-side orientation: base_is_coin_side is currently hardcoded true (assumes WSOL is the pool's coin side). A SOL/X pool where WSOL is the PC side will need the salvor's bot to swap submission ordering; runtime pool-data parsing is a PRE-MAINNET-TODO(CPI) in salvage_pool.rs.
  • Host BPF compile (anchor build) has not been run locally for this branch — CI will surface any BPF-target-specific compile failures.

Honest disclaimers

Following the ship-now-verify-after cadence with explicit unverified flags. Anything CodeRabbit or CI flags will be addressed as fix-up commits on this PR rather than blocking the initial push. The CHANGELOG's "Unverified" section enumerates what's not yet proven.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Complete pool salvage flow: LP transfer, AMM-specific liquidity removal dispatch, optional token swap to native SOL, automatic SOL conversion, and 40/40/20 proceeds distribution with on-chain receipts/events; adds Jupiter v6 swap and Raydium V4 withdraw integrations.
  • Documentation

    • Updated changelog for milestone m5.
    • Expanded pre-mainnet checklist with CPI/adapter verification items.
    • Extended error-code documentation to cover new 7015–7019 entries.
  • Chores

    • CI/tooling update for build compatibility.

Review Change Stack

Wires the full Raydium V4 SOL/X salvage flow end-to-end: salvor→vault
LP transfer, Raydium V4 withdraw CPI, Jupiter v6 swap (or dust skip),
WSOL→SOL unwrap, 40/40/20 distribution, SalvageReceipt population,
PoolSalvaged + SalvageCompleted events.

See CHANGELOG.md "Unreleased — m5" for the full added/changed/unverified
breakdown.

PR scope per Option B (2 PRs by code path): this PR is the salvage_pool
side. claim_lp_proceeds Merkle verification ships in a follow-up PR.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 16, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@graveyieldprotocol has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 52 minutes and 58 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7131a00c-8d76-4e2e-9108-19b5637c767e

📥 Commits

Reviewing files that changed from the base of the PR and between 5a7e9db and 445b458.

📒 Files selected for processing (1)
  • .github/workflows/ci.yml
📝 Walkthrough

Walkthrough

This PR implements the m5 milestone execution path for salvage_pool, extending the instruction with a complete flow: LP redemption from AMM-specific remove_liquidity CPIs, optional Jupiter memecoin→WSOL swapping, WSOL unwrap, and 40/40/20 distribution of recovered proceeds to the LP-holder, salvor, and protocol, with on-chain recording and event emission.

Changes

m5: Salvage Pool Execution Path with AMM CPI Dispatch and Distribution

Layer / File(s) Summary
Constants, Error Codes, and Lint Configuration
programs/grave-vault/src/constants.rs, programs/grave-vault/src/errors.rs, programs/grave-vault/src/lib.rs, programs/grave-vault/Cargo.toml
New constants for slippage bounds (HARD_MAX_SLIPPAGE_BPS), vault PDA seeds (VAULT_AUTHORITY_SEED, VAULT_SOL_HOLDING_SEED), cross-program seeds, external program Pubkey constants (WSOL, Raydium V4/CLMM, Orca, PumpSwap, Jupiter v6), and Raydium V4 withdraw CPI layout. Five new error variants (7015–7019). Crate-level Anchor lint allowances and anchor-lang feature init-if-needed added.
CPI Module Foundation and Dispatcher Architecture
programs/grave-vault/src/cpi/mod.rs
Defines RemoveLiquidityInput and RemoveLiquidityOutput types and dispatch_remove_liquidity router that selects AMM-specific adapters based on pool owner program ID. Exports five CPI submodules (jupiter, raydium_v4, raydium_clmm, orca_whirlpool, pump_swap).
Raydium V4 Withdraw CPI Adapter
programs/grave-vault/src/cpi/raydium_v4.rs
Full implementation of remove_liquidity for Raydium V4: validates remaining_accounts length and pool ownership, determines base vs memecoin account assignment, snapshots pre-CPI balances, builds 18-account withdraw instruction with vault_authority as signer, invokes Raydium via invoke_signed, snapshots post-CPI balances, and computes received deltas with underflow checks and zero-base rejection.
Jupiter v6 Swap CPI Helper
programs/grave-vault/src/cpi/jupiter.rs
Implements swap(...) for Jupiter v6 routing: snapshots pre-swap destination token balance, constructs CPI instruction from pre-ordered route_accounts (marking vault_authority as signer), invokes Jupiter via invoke_signed, snapshots post-swap balance, and returns net output_amount via checked subtraction.
Honest-Stub AMM Adapters
programs/grave-vault/src/cpi/orca_whirlpool.rs, programs/grave-vault/src/cpi/pump_swap.rs, programs/grave-vault/src/cpi/raydium_clmm.rs
Three stub modules for unimplemented AMM remove_liquidity adapters: each immediately returns AmmCpiUnimplemented, preventing silent success and signaling pre-mainnet status.
Salvage Pool Handler: LP Transfer, CPI Dispatch, Jupiter Swap, Distribution
programs/grave-vault/src/instructions/salvage_pool.rs
Rewritten salvage_pool handler executing m5 flow: pool consistency and snapshot validation, lazy-initialized vault PDAs, salvor LP token transfer, dispatched remove_liquidity CPI, conditional Jupiter memecoin→WSOL swap with slippage enforcement, WSOL account close, 40/40/20 distribution using u128 math, lamport transfers to recipients, PoolRegistry and SalvageReceipt updates with recorded shares, and emitted events including computed amounts. Adds lazy_init_system_pda and transfer_from_vault_sol_holding helper functions.
Public Instruction Signature and Anchor Context Lifetime Threading
programs/grave-vault/src/lib.rs
Updates grave_vault::salvage_pool instruction signature to explicit Anchor Context<'_, '_, '_, 'info, SalvagePool<'info>> lifetime parameters for proper reference scoping in the expanded handler.
Changelog, Pre-Mainnet Checklist, Error Code Documentation, CI
CHANGELOG.md, docs/PRE_MAINNET_CHECKLIST.md, docs/error_codes.md, .github/workflows/ci.yml
Documents m5 milestone with CPI helpers, handler wiring, new errors (7015–7019), PDA seeds, and constants; adds four CPI verification checklist rows (CPI-006–CPI-009); extends error_codes.md to cover 7015–7019 and updates mirror/footer note; pins Solana platform-tools to v1.54 in CI.

Sequence Diagram: high-level salvage flow

sequenceDiagram
  participant Client
  participant SalvageHandler as SalvagePool::handler
  participant CPI_Dispatch as dispatch_remove_liquidity
  participant RaydiumV4
  participant Jupiter
  participant System as SystemProgram
  Client->>SalvageHandler: salvage_pool(params, remaining_accounts)
  SalvageHandler->>CPI_Dispatch: RemoveLiquidityInput(pool,...)
  CPI_Dispatch->>RaydiumV4: remove_liquidity(...) (invoke_signed)
  RaydiumV4-->>CPI_Dispatch: base + memecoin amounts
  CPI_Dispatch-->>SalvageHandler: RemoveLiquidityOutput
  SalvageHandler->>Jupiter: jupiter::swap(...) (conditional)
  Jupiter-->>SalvageHandler: swap output amount
  SalvageHandler->>System: close WSOL token account -> vault SOL holding
  SalvageHandler->>System: transfer lamports to salvor, lp-holder, protocol (invoke_signed)
  SalvageHandler-->>Client: emit PoolSalvaged, SalvageCompleted
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

"🐰 Luna the rabbit hops through the code with cheer,
Swapping memecoin, closing WSOL—events appear,
Forty, forty, twenty split with careful math,
CPIs dispatched, balances read, no silent path,
m5 marches on with receipts and a thumping heart!"

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main change: implementing the salvage_pool execution path for milestone 5 in GraveVault.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/m5-grave-vault-salvage-execution-path

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🤖 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 `@CHANGELOG.md`:
- Around line 22-27: The CHANGELOG notes a hardcoded pool orientation
(base_is_coin_side in salvage_pool.rs) marked with PRE-MAINNET-TODO(CPI) but
there is no corresponding entry in PRE_MAINNET_CHECKLIST.md; add a new checklist
row (e.g., CPI-010) that references the PRE-MAINNET-TODO marker, gives a status
(🟥/🟧) and concrete verification criteria (runtime detection of pool
orientation or a documented inversion procedure), or update the CHANGELOG text
to point to an existing CPI entry if this is already tracked; ensure the
checklist row ID matches the TODO marker and the description mentions
salvage_pool.rs and base_is_coin_side so reviewers can locate the code.

In `@programs/grave-vault/src/constants.rs`:
- Around line 84-118: The Pubkey constant declarations (e.g., WSOL_MINT,
RAYDIUM_V4_PROGRAM_ID, RAYDIUM_V4_AMM_AUTHORITY, RAYDIUM_CLMM_PROGRAM_ID,
ORCA_WHIRLPOOL_PROGRAM_ID, PUMP_SWAP_PROGRAM_ID, JUPITER_V6_PROGRAM_ID) are
misformatted per rustfmt; run rustfmt to fix wrapping and alignment and commit
the changes. Specifically, run cargo fmt --all in the workspace root (or rustfmt
the programs/grave-vault/src/constants.rs file), verify the
Pubkey::anchor_lang::solana_program::pubkey!(...) blocks are wrapped/formatted
by rustfmt, and stage/commit the resulting updated file so the CI fmt check
passes.
- Around line 84-118: The qualified procedural-macro invocations like
anchor_lang::solana_program::pubkey! used for WSOL_MINT, RAYDIUM_V4_PROGRAM_ID,
RAYDIUM_V4_AMM_AUTHORITY, RAYDIUM_CLMM_PROGRAM_ID, ORCA_WHIRLPOOL_PROGRAM_ID,
PUMP_SWAP_PROGRAM_ID and JUPITER_V6_PROGRAM_ID fail to compile; import the
pubkey macro into scope (add a use/import for pubkey! after the other imports)
and replace each qualified call (anchor_lang::solana_program::pubkey!(...)) with
the bare macro pubkey!(...) for those symbols so the procedural macro is invoked
correctly.

In `@programs/grave-vault/src/cpi/jupiter.rs`:
- Around line 89-101: The code formatting for the invoke_signed block is failing
CI; run rustfmt (cargo fmt --all) to reformat the block around account_infos,
the bump/signer_seeds declaration, and the invoke_signed call (symbols:
account_infos, input.route_accounts, bump, signer_seeds, VAULT_AUTHORITY_SEED,
invoke_signed) so the vector construction, seed array, and invoke_signed
invocation match project style and pass cargo fmt.
- Around line 63-81: The code currently sets meta.is_signer when meta.pubkey ==
*input.vault_authority.key but never verifies the vault_authority was present;
update the logic to track whether vault_authority was found while building metas
(e.g., set a boolean when meta.pubkey == *input.vault_authority.key), and after
the loop if not found either return an explicit error or append a new
AccountMeta for *input.vault_authority.key with is_signer = true (and
appropriate is_writable) and also append the corresponding AccountInfo to the
account_infos vector so the signer account is included before calling
invoke_signed; ensure you update the same variables used later (metas,
account_infos) and preserve the invoke_signed seeds behavior.

In `@programs/grave-vault/src/cpi/mod.rs`:
- Around line 21-24: The import block for crate::constants (the use statement
referencing ORCA_WHIRLPOOL_PROGRAM_ID, PUMP_SWAP_PROGRAM_ID,
RAYDIUM_CLMM_PROGRAM_ID, RAYDIUM_V4_PROGRAM_ID) is misformatted; run rustfmt to
fix it by applying cargo fmt --all (or rustfmt) so the use
crate::constants::{...} line is wrapped/indented according to project style,
ensuring the listed constants remain in the same use declaration.

In `@programs/grave-vault/src/instructions/salvage_pool.rs`:
- Around line 363-365: The preflight check uses a >= comparison allowing
pre-existing LP; change the invariant to require exact deposit-and-burn by
replacing the condition in the require! call so it asserts
ctx.accounts.vault_lp_token_account.amount == params.salvor_lp_amount and still
returns GraveVaultError::PreflightFailed on mismatch—update the require! around
ctx.accounts.vault_lp_token_account.amount and params.salvor_lp_amount in
salvage_pool.rs accordingly.
- Around line 651-657: The invoke_signed call currently collapses all CPI
failures into GraveVaultError::MathOverflow; instead remove the blanket
.map_err(|_| error!(GraveVaultError::MathOverflow)) and preserve the original
CPI error (or wrap it in a specific error variant). Locate the invoke_signed
invocation and either propagate the ProgramError returned by invoke_signed
(return Err(err) / map_err(Into::into)) or add a distinct error like
GraveVaultError::TransferFailed and include the underlying error details when
mapping (so insufficient lamports / bad account state are not lost). Ensure the
new handling references the same invoke_signed call and replaces the
MathOverflow remapping only.
- Around line 205-230: The build fails because the SalvagePool account fields
(e.g., vault_lp_token_account, vault_base_token_account and the vault memecoin
token account) use the init_if_needed attribute which requires the anchor-lang
"init-if-needed" feature; update the Cargo.toml dependency for anchor-lang
(either in the program's Cargo.toml or the workspace/root Cargo.toml if shared)
to enable the feature (e.g., add features = ["init-if-needed"] for anchor-lang =
"0.32.1") so the init_if_needed attribute compiles successfully.

In `@programs/grave-vault/src/lib.rs`:
- Around line 81-86: The program macro fails because the handler signature uses
SalvagePool<'info> but there is no Accounts-derived struct with the correct
lifetime; add or fix the SalvagePool accounts struct in
instructions/salvage_pool.rs by declaring pub struct SalvagePool<'info> with
#[derive(Accounts)] and ensure every account field uses the 'info lifetime
(e.g., AccountInfo<'info> or Account<'info, T>), include all required Anchor
constraints (seeds, mut, signer, owner, etc.) to match usage in
salvage_pool::handler, and ensure field names/types match the handler
expectations so the #[program] macro can generate the client accounts.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 29aa12b8-5e2d-4aa8-81ec-acbc5480ae56

📥 Commits

Reviewing files that changed from the base of the PR and between 704ca9c and c38bc94.

📒 Files selected for processing (13)
  • CHANGELOG.md
  • docs/PRE_MAINNET_CHECKLIST.md
  • docs/error_codes.md
  • programs/grave-vault/src/constants.rs
  • programs/grave-vault/src/cpi/jupiter.rs
  • programs/grave-vault/src/cpi/mod.rs
  • programs/grave-vault/src/cpi/orca_whirlpool.rs
  • programs/grave-vault/src/cpi/pump_swap.rs
  • programs/grave-vault/src/cpi/raydium_clmm.rs
  • programs/grave-vault/src/cpi/raydium_v4.rs
  • programs/grave-vault/src/errors.rs
  • programs/grave-vault/src/instructions/salvage_pool.rs
  • programs/grave-vault/src/lib.rs

Comment thread CHANGELOG.md
Comment on lines +22 to +27
### Unverified
- BPF compile via `anchor build` (deferred to CI on this PR).
- Live Raydium V4 fork test of the exact 18-account ordering. The `amm_authority` constant check provides one assertion; full integration is `CPI-009` in `PRE_MAINNET_CHECKLIST.md`.
- Real Jupiter v6 swap end-to-end. The CPI helper forwards what the salvor's bot quotes; verification is a localnet smoke test post-merge.
- Pool orientation: `base_is_coin_side` is currently hardcoded `true` (assumes WSOL is the pool's coin side). A SOL/X pool where WSOL is the PC side will need the bot to invert its submission ordering; a runtime parse of pool data to detect orientation is in `PRE-MAINNET-TODO(CPI)` comments in `salvage_pool.rs`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Pool orientation concern mentioned but not tracked in PRE_MAINNET_CHECKLIST.

Line 26 documents a pool orientation hardcoding issue with "PRE-MAINNET-TODO(CPI) comments" in the source, but no corresponding CPI-010 (or similar) row appears in PRE_MAINNET_CHECKLIST.md. Per the checklist convention (line 4: "Each row maps to one or more PRE-MAINNET-TODO markers in source"), this represents a tracking gap for an acknowledged pre-mainnet concern.

If the pool orientation assumption is a blocking or high-priority concern, add a numbered row (e.g., CPI-010) to the checklist with appropriate status (🟥/🟧) and verification criteria. If it's lower priority or addressed differently, clarify the relationship between this Unverified item and the checklist scope.

🤖 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 `@CHANGELOG.md` around lines 22 - 27, The CHANGELOG notes a hardcoded pool
orientation (base_is_coin_side in salvage_pool.rs) marked with
PRE-MAINNET-TODO(CPI) but there is no corresponding entry in
PRE_MAINNET_CHECKLIST.md; add a new checklist row (e.g., CPI-010) that
references the PRE-MAINNET-TODO marker, gives a status (🟥/🟧) and concrete
verification criteria (runtime detection of pool orientation or a documented
inversion procedure), or update the CHANGELOG text to point to an existing CPI
entry if this is already tracked; ensure the checklist row ID matches the TODO
marker and the description mentions salvage_pool.rs and base_is_coin_side so
reviewers can locate the code.

Comment thread programs/grave-vault/src/constants.rs Outdated
Comment on lines +63 to +81
// Build CPI. Jupiter's route doesn't require the destination token
// account to be in any specific position — it's part of `route_accounts`.
// We just forward what the salvor's quote produced.
let mut metas: Vec<AccountMeta> = Vec::with_capacity(input.route_accounts.len());
for acct in input.route_accounts.iter() {
metas.push(AccountMeta {
pubkey: *acct.key,
is_signer: acct.is_signer,
is_writable: acct.is_writable,
});
}
// Ensure vault_authority is marked signer in the meta list (it must be
// because we sign for it). The salvor's quote may or may not have
// flagged this; the actual signing happens via invoke_signed's seeds.
for meta in metas.iter_mut() {
if meta.pubkey == *input.vault_authority.key {
meta.is_signer = true;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing validation that vault_authority is present in route_accounts.

If the salvor's quote doesn't include vault_authority in route_accounts, the signer-marking loop (lines 77-81) won't find it, and the invoke_signed will fail because the signing account won't be in the account infos passed to the CPI. Consider adding an explicit check that vault_authority was found, or unconditionally appending it to both metas and account_infos.

🛡️ Proposed fix to validate vault_authority presence
     // Ensure vault_authority is marked signer in the meta list (it must be
     // because we sign for it). The salvor's quote may or may not have
     // flagged this; the actual signing happens via invoke_signed's seeds.
+    let mut vault_authority_found = false;
     for meta in metas.iter_mut() {
         if meta.pubkey == *input.vault_authority.key {
             meta.is_signer = true;
+            vault_authority_found = true;
         }
     }
+    require!(
+        vault_authority_found,
+        GraveVaultError::JupiterSwapFailed
+    );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Build CPI. Jupiter's route doesn't require the destination token
// account to be in any specific position — it's part of `route_accounts`.
// We just forward what the salvor's quote produced.
let mut metas: Vec<AccountMeta> = Vec::with_capacity(input.route_accounts.len());
for acct in input.route_accounts.iter() {
metas.push(AccountMeta {
pubkey: *acct.key,
is_signer: acct.is_signer,
is_writable: acct.is_writable,
});
}
// Ensure vault_authority is marked signer in the meta list (it must be
// because we sign for it). The salvor's quote may or may not have
// flagged this; the actual signing happens via invoke_signed's seeds.
for meta in metas.iter_mut() {
if meta.pubkey == *input.vault_authority.key {
meta.is_signer = true;
}
}
// Build CPI. Jupiter's route doesn't require the destination token
// account to be in any specific position — it's part of `route_accounts`.
// We just forward what the salvor's quote produced.
let mut metas: Vec<AccountMeta> = Vec::with_capacity(input.route_accounts.len());
for acct in input.route_accounts.iter() {
metas.push(AccountMeta {
pubkey: *acct.key,
is_signer: acct.is_signer,
is_writable: acct.is_writable,
});
}
// Ensure vault_authority is marked signer in the meta list (it must be
// because we sign for it). The salvor's quote may or may not have
// flagged this; the actual signing happens via invoke_signed's seeds.
let mut vault_authority_found = false;
for meta in metas.iter_mut() {
if meta.pubkey == *input.vault_authority.key {
meta.is_signer = true;
vault_authority_found = true;
}
}
require!(
vault_authority_found,
GraveVaultError::JupiterSwapFailed
);
🤖 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 `@programs/grave-vault/src/cpi/jupiter.rs` around lines 63 - 81, The code
currently sets meta.is_signer when meta.pubkey == *input.vault_authority.key but
never verifies the vault_authority was present; update the logic to track
whether vault_authority was found while building metas (e.g., set a boolean when
meta.pubkey == *input.vault_authority.key), and after the loop if not found
either return an explicit error or append a new AccountMeta for
*input.vault_authority.key with is_signer = true (and appropriate is_writable)
and also append the corresponding AccountInfo to the account_infos vector so the
signer account is included before calling invoke_signed; ensure you update the
same variables used later (metas, account_infos) and preserve the invoke_signed
seeds behavior.

Comment thread programs/grave-vault/src/cpi/jupiter.rs
Comment thread programs/grave-vault/src/cpi/mod.rs
Comment thread programs/grave-vault/src/instructions/salvage_pool.rs
Comment on lines +363 to +365
require!(
ctx.accounts.vault_lp_token_account.amount >= params.salvor_lp_amount,
GraveVaultError::PreflightFailed

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Enforce exact LP deposit invariant, not a lower bound.

Line 364 currently allows pre-existing LP in vault_lp_token_account via >=. The documented invariant in this flow is exact deposit-and-burn for params.salvor_lp_amount; use equality here.

Suggested fix
-    require!(
-        ctx.accounts.vault_lp_token_account.amount >= params.salvor_lp_amount,
-        GraveVaultError::PreflightFailed
-    );
+    require!(
+        ctx.accounts.vault_lp_token_account.amount == params.salvor_lp_amount,
+        GraveVaultError::PreflightFailed
+    );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
require!(
ctx.accounts.vault_lp_token_account.amount >= params.salvor_lp_amount,
GraveVaultError::PreflightFailed
require!(
ctx.accounts.vault_lp_token_account.amount == params.salvor_lp_amount,
GraveVaultError::PreflightFailed
);
🤖 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 `@programs/grave-vault/src/instructions/salvage_pool.rs` around lines 363 -
365, The preflight check uses a >= comparison allowing pre-existing LP; change
the invariant to require exact deposit-and-burn by replacing the condition in
the require! call so it asserts ctx.accounts.vault_lp_token_account.amount ==
params.salvor_lp_amount and still returns GraveVaultError::PreflightFailed on
mismatch—update the require! around ctx.accounts.vault_lp_token_account.amount
and params.salvor_lp_amount in salvage_pool.rs accordingly.

Comment on lines +651 to +657
invoke_signed(
&ix,
&[source.to_account_info(), to.clone()],
&[seeds],
)
.map_err(|_| error!(GraveVaultError::MathOverflow))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not remap transfer CPI failures to MathOverflow.

Line 656 collapses all invoke_signed failures into MathOverflow, which misclassifies real runtime faults (e.g., insufficient lamports, bad account state) and makes incident triage harder.

Suggested fix
-    invoke_signed(
-        &ix,
-        &[source.to_account_info(), to.clone()],
-        &[seeds],
-    )
-    .map_err(|_| error!(GraveVaultError::MathOverflow))
+    invoke_signed(
+        &ix,
+        &[source.to_account_info(), to.clone()],
+        &[seeds],
+    )?;
+    Ok(())
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
invoke_signed(
&ix,
&[source.to_account_info(), to.clone()],
&[seeds],
)
.map_err(|_| error!(GraveVaultError::MathOverflow))
}
invoke_signed(
&ix,
&[source.to_account_info(), to.clone()],
&[seeds],
)?;
Ok(())
}
🤖 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 `@programs/grave-vault/src/instructions/salvage_pool.rs` around lines 651 -
657, The invoke_signed call currently collapses all CPI failures into
GraveVaultError::MathOverflow; instead remove the blanket .map_err(|_|
error!(GraveVaultError::MathOverflow)) and preserve the original CPI error (or
wrap it in a specific error variant). Locate the invoke_signed invocation and
either propagate the ProgramError returned by invoke_signed (return Err(err) /
map_err(Into::into)) or add a distinct error like
GraveVaultError::TransferFailed and include the underlying error details when
mapping (so insufficient lamports / bad account state are not lost). Ensure the
new handling references the same invoke_signed call and replaces the
MathOverflow remapping only.

Comment on lines +81 to 86
pub fn salvage_pool<'info>(
ctx: Context<'_, '_, '_, 'info, SalvagePool<'info>>,
params: SalvagePoolParams,
) -> Result<()> {
instructions::salvage_pool::handler(ctx, params)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Anchor program macro compilation failure.

The pipeline reports that the #[program] macro expansion fails because SalvagePool<'info> does not implement the Accounts trait and the generated client accounts are missing. This signature change requires a correctly defined SalvagePool accounts struct in instructions/salvage_pool.rs.

The review stack context indicates that instructions/salvage_pool.rs is in layer 6 ("Salvage Pool Handler") but is not included in the current review batch. The explicit lifetime parameters in this signature are only valid if the SalvagePool accounts struct is properly defined with #[derive(Accounts)] and matching lifetime annotations.

Verify that the SalvagePool accounts struct:

  1. Has the #[derive(Accounts)] attribute
  2. Declares <'info> lifetime on the struct and all AccountInfo<'info> fields
  3. Properly implements all Anchor account constraints
🤖 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 `@programs/grave-vault/src/lib.rs` around lines 81 - 86, The program macro
fails because the handler signature uses SalvagePool<'info> but there is no
Accounts-derived struct with the correct lifetime; add or fix the SalvagePool
accounts struct in instructions/salvage_pool.rs by declaring pub struct
SalvagePool<'info> with #[derive(Accounts)] and ensure every account field uses
the 'info lifetime (e.g., AccountInfo<'info> or Account<'info, T>), include all
required Anchor constraints (seeds, mut, signer, owner, etc.) to match usage in
salvage_pool::handler, and ensure field names/types match the handler
expectations so the #[program] macro can generate the client accounts.

graveyieldprotocol and others added 2 commits May 19, 2026 17:17
Three locally-reproduced fixes (cargo clippy --all-targets -- -D warnings
goes from 28 errors to 0 with these):

1. `Cargo.toml`: add `features = ["init-if-needed"]` to anchor-lang.
   The `init_if_needed` constraint on vault_lp_token_account,
   vault_base_token_account, vault_memecoin_token_account requires
   this feature gate per Anchor's docs.

2. `constants.rs`: replace `anchor_lang::solana_program::pubkey!(...)`
   (×7) with the bare `pubkey!(...)` macro re-exported by Anchor's
   prelude. Anchor 0.32 dropped the `solana_program::pubkey` re-export
   path — only `account_info`, `clock`, `msg`, `entrypoint`,
   `program_error`, `pubkey`, `system_program`, `system_instruction`
   are exposed under the legacy module name. This matches the known
   Anchor 0.32 refactor failure pattern.

3. `salvage_pool.rs::handler`: bind outer `signer_seeds` to a named
   variable in the WSOL `close_account` block. The previous
   `&[signer_seeds]` inline created a temporary &[&[&[u8]]] freed
   before token::close_account consumed it (E0716). Matches the
   pattern in lazy_init_system_pda which already had this right.

All remaining changes are rustfmt drift (10 files) auto-applied via
`cargo fmt --all`. Verified clean locally: cargo fmt --check ✓,
cargo clippy --all-targets -- -D warnings ✓, cargo test ✓.

Anchor build (BPF compile) deferred to CI — the host-side issues
fixed here may or may not have masked a separate BPF-target failure.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two BPF-target-only failures that host cargo check / clippy don't surface:

1. salvage_pool.rs: Box-wrap 11 large account fields.
   `cargo-build-sbf` reported the `SalvagePool::try_accounts` function
   overflowing its 4096-byte BPF frame by 128 bytes (4224 estimated).
   `protocol_config`, `eligibility_cert`, `pool_registry`,
   `salvage_receipt`, four TokenAccounts, three Mints are now
   `Box<Account<...>>` — moves deserialized data off the stack onto
   the heap. Canonical Anchor pattern for large Accounts structs.

2. .github/workflows/ci.yml: Pin platform-tools v1.54 before
   `anchor build`. Solana 3.0.10 bundles platform-tools v1.51 which
   ships cargo 1.84 — too old for `edition2024` manifests pulled
   transitively by Anchor 0.32.1's SPL deps (blake3 0.12, hashbrown
   0.17, digest 0.11, crypto-common 0.2). cargo-build-sbf 3.0.10's
   --tools-version flag is silently ignored, and the workspace
   `[metadata.solana] tools-version = "v1.54"` pin isn't honored.
   Workaround: replace the cached platform-tools directory contents
   with v1.54 (cargo 1.89) before anchor build runs. The cache key
   stays `v1.51` because cargo-build-sbf 3.0.10 hardcodes it.

Locally reproduced both failures with the matching toolchain stack
(Solana 3.0.10 + platform-tools v1.51 + anchor 0.32.1). After both
fixes:
  - `cargo-build-sbf` finishes in 3.79s clean
  - `cargo clippy --all-targets -- -D warnings` clean
  - `cargo fmt --check` clean

PR #20 (m6) gets the same ci.yml change as a parallel fixup.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
graveyieldprotocol added a commit that referenced this pull request May 19, 2026
Same ci.yml change as PR #19's fixup #2: Solana 3.0.10's bundled
platform-tools v1.51 ships cargo 1.84 which can't parse `edition2024`
manifests pulled transitively by Anchor 0.32.1's SPL deps. Replace the
cached platform-tools directory with v1.54 (cargo 1.89) before
anchor build invokes cargo-build-sbf.

m6's Rust code itself is BPF-clean — `cargo-build-sbf` succeeds locally
once platform-tools v1.54 is active. The only change here is the CI
workflow step.

If PR #19 lands first with the same ci.yml change, this PR's rebase
resolves trivially (identical content). If PR #20 lands first, PR #19
rebases against this.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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 @.github/workflows/ci.yml:
- Around line 86-94: The CI step downloads and extracts platform-tools without
verification; update the job to verify the release artifact before extracting by
fetching the corresponding checksum or signature and validating it against the
downloaded file (the curl download to /tmp/platform-tools.tar.bz2) prior to
using tar to unpack into CACHE_DEST; use the published SHA256 (or a GPG
signature) from the release, fail the step if verification fails, and only then
proceed with rm -rf, mkdir -p and tar xjf into CACHE_DEST so the pipeline never
extracts unverified network content.

In `@programs/grave-vault/src/instructions/salvage_pool.rs`:
- Around line 453-455: The code currently uses only
ctx.accounts.vault_base_token_account.amount to compute total_recovered_wsol and
misses the rent-exempt lamports moved on WSOL account close; fix by capturing
the on-chain lamport balance of the token account and/or the recipient before
and after the close and using their difference as the true recovered lamports.
Concretely: before calling close_account, read let pre_token_lamports =
ctx.accounts.vault_base_token_account.to_account_info().lamports() (and
optionally pre_sol_holding =
ctx.accounts.vault_sol_holding_account.to_account_info().lamports()); perform
the close_account call, then read post_sol_holding =
ctx.accounts.vault_sol_holding_account.to_account_info().lamports() and set
total_recovered_wsol to (post_sol_holding - pre_sol_holding) (or if you only
captured the token account, use pre_token_lamports as the recovered amount)
instead of using vault_base_token_account.amount so the rent reserve is
included.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1dce735b-b64b-44c2-a74c-fd4c496bfd5b

📥 Commits

Reviewing files that changed from the base of the PR and between fac3b19 and 1484952.

📒 Files selected for processing (2)
  • .github/workflows/ci.yml
  • programs/grave-vault/src/instructions/salvage_pool.rs

Comment thread .github/workflows/ci.yml
Comment on lines +86 to +94
- name: Pin platform-tools v1.54 (edition2024 fix)
run: |
set -euo pipefail
curl -sSL -o /tmp/platform-tools.tar.bz2 \
"https://github.com/anza-xyz/platform-tools/releases/download/v1.54/platform-tools-linux-x86_64.tar.bz2"
CACHE_DEST="$HOME/.cache/solana/v1.51/platform-tools"
rm -rf "$CACHE_DEST"
mkdir -p "$CACHE_DEST"
tar xjf /tmp/platform-tools.tar.bz2 -C "$CACHE_DEST"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Verify the platform-tools archive before extracting it.

This step untars an executable toolchain straight from a network download into the runner cache. Without checking the published checksum or signature first, a compromised release asset becomes code execution in CI.

Proposed fix
       - name: Pin platform-tools v1.54 (edition2024 fix)
         run: |
           set -euo pipefail
           curl -sSL -o /tmp/platform-tools.tar.bz2 \
             "https://github.com/anza-xyz/platform-tools/releases/download/v1.54/platform-tools-linux-x86_64.tar.bz2"
+          curl -sSL -o /tmp/platform-tools.tar.bz2.sha256 \
+            "https://github.com/anza-xyz/platform-tools/releases/download/v1.54/platform-tools-linux-x86_64.tar.bz2.sha256"
+          sha256sum -c /tmp/platform-tools.tar.bz2.sha256
           CACHE_DEST="$HOME/.cache/solana/v1.51/platform-tools"
           rm -rf "$CACHE_DEST"
           mkdir -p "$CACHE_DEST"
           tar xjf /tmp/platform-tools.tar.bz2 -C "$CACHE_DEST"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Pin platform-tools v1.54 (edition2024 fix)
run: |
set -euo pipefail
curl -sSL -o /tmp/platform-tools.tar.bz2 \
"https://github.com/anza-xyz/platform-tools/releases/download/v1.54/platform-tools-linux-x86_64.tar.bz2"
CACHE_DEST="$HOME/.cache/solana/v1.51/platform-tools"
rm -rf "$CACHE_DEST"
mkdir -p "$CACHE_DEST"
tar xjf /tmp/platform-tools.tar.bz2 -C "$CACHE_DEST"
- name: Pin platform-tools v1.54 (edition2024 fix)
run: |
set -euo pipefail
curl -sSL -o /tmp/platform-tools.tar.bz2 \
"https://github.com/anza-xyz/platform-tools/releases/download/v1.54/platform-tools-linux-x86_64.tar.bz2"
curl -sSL -o /tmp/platform-tools.tar.bz2.sha256 \
"https://github.com/anza-xyz/platform-tools/releases/download/v1.54/platform-tools-linux-x86_64.tar.bz2.sha256"
sha256sum -c /tmp/platform-tools.tar.bz2.sha256
CACHE_DEST="$HOME/.cache/solana/v1.51/platform-tools"
rm -rf "$CACHE_DEST"
mkdir -p "$CACHE_DEST"
tar xjf /tmp/platform-tools.tar.bz2 -C "$CACHE_DEST"
🤖 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 @.github/workflows/ci.yml around lines 86 - 94, The CI step downloads and
extracts platform-tools without verification; update the job to verify the
release artifact before extracting by fetching the corresponding checksum or
signature and validating it against the downloaded file (the curl download to
/tmp/platform-tools.tar.bz2) prior to using tar to unpack into CACHE_DEST; use
the published SHA256 (or a GPG signature) from the release, fail the step if
verification fails, and only then proceed with rm -rf, mkdir -p and tar xjf into
CACHE_DEST so the pipeline never extracts unverified network content.

Comment on lines +453 to +455
ctx.accounts.vault_base_token_account.reload()?;
let total_recovered_wsol = ctx.accounts.vault_base_token_account.amount;
require!(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Official SPL Token / wrapped SOL docs: when a WSOL token account is closed, does the destination receive the account's lamports, including the rent-exempt reserve, rather than only the token amount?

💡 Result:

Yes, when a Wrapped SOL (WSOL) token account is closed using the SPL Token Program's CloseAccount instruction, the destination account receives the entire lamport balance of the token account [1][2]. This includes both the rent-exempt reserve (the minimum balance required to keep the account on-chain) and any remaining WSOL balance that was converted to native SOL during the closure process [1][3][4]. The CloseAccount instruction functions by transferring all lamports held by the source token account to the specified destination account, after which the source account is cleared and deleted from the blockchain [1][2]. While standard token accounts typically require a zero token balance before they can be closed, WSOL accounts are an exception; they can be closed even if they contain a balance, effectively unwrapping the tokens and returning the underlying native SOL to the destination [1][4].

Citations:


🏁 Script executed:

fd salvage_pool.rs

Repository: GraveYield-Protocol/protocol

Length of output: 126


🏁 Script executed:

head -n 520 programs/grave-vault/src/instructions/salvage_pool.rs | tail -n +450

Repository: GraveYield-Protocol/protocol

Length of output: 3168


🏁 Script executed:

sed -n '460,474p' programs/grave-vault/src/instructions/salvage_pool.rs

Repository: GraveYield-Protocol/protocol

Length of output: 720


🏁 Script executed:

sed -n '497,511p' programs/grave-vault/src/instructions/salvage_pool.rs

Repository: GraveYield-Protocol/protocol

Length of output: 800


🏁 Script executed:

sed -n '440,520p' programs/grave-vault/src/instructions/salvage_pool.rs

Repository: GraveYield-Protocol/protocol

Length of output: 3578


🏁 Script executed:

rg -n "transfer_from_vault_sol_holding" programs/grave-vault/src/instructions/salvage_pool.rs -A 5

Repository: GraveYield-Protocol/protocol

Length of output: 1096


🏁 Script executed:

rg -n "fn transfer_from_vault_sol_holding" programs/grave-vault/src/

Repository: GraveYield-Protocol/protocol

Length of output: 173


🏁 Script executed:

sed -n '630,660p' programs/grave-vault/src/instructions/salvage_pool.rs

Repository: GraveYield-Protocol/protocol

Length of output: 1032


🏁 Script executed:

wc -l programs/grave-vault/src/instructions/salvage_pool.rs

Repository: GraveYield-Protocol/protocol

Length of output: 130


🏁 Script executed:

rg -n "let total = total_recovered_wsol" programs/grave-vault/src/instructions/salvage_pool.rs

Repository: GraveYield-Protocol/protocol

Length of output: 114


🏁 Script executed:

rg -n "total_recovered_wsol" programs/grave-vault/src/instructions/salvage_pool.rs

Repository: GraveYield-Protocol/protocol

Length of output: 233


🏁 Script executed:

rg -n "close_account\|let total = " programs/grave-vault/src/instructions/salvage_pool.rs | head -30

Repository: GraveYield-Protocol/protocol

Length of output: 54


🏁 Script executed:

rg -B5 "close_account\|CloseAccount" programs/grave-vault/src/instructions/salvage_pool.rs

Repository: GraveYield-Protocol/protocol

Length of output: 54


🏁 Script executed:

sed -n '461,476p' programs/grave-vault/src/instructions/salvage_pool.rs

Repository: GraveYield-Protocol/protocol

Length of output: 783


🏁 Script executed:

grep -n "let total_recovered_wsol\|let total = " programs/grave-vault/src/instructions/salvage_pool.rs

Repository: GraveYield-Protocol/protocol

Length of output: 195


🏁 Script executed:

sed -n '1,100p' programs/grave-vault/src/instructions/salvage_pool.rs | grep -n "fn "

Repository: GraveYield-Protocol/protocol

Length of output: 54


🏁 Script executed:

grep -n "^pub fn\|^fn " programs/grave-vault/src/instructions/salvage_pool.rs

Repository: GraveYield-Protocol/protocol

Length of output: 182


Account for the rent reserve when calculating total recovered lamports.

When a WSOL token account closes, close_account transfers the entire lamport balance to the destination—including the rent-exempt reserve (~5,000 lamports). The current code uses only vault_base_token_account.amount to calculate total, missing the rent reserve that's transferred into vault_sol_holding_account. This leaves the recovered rent unallocated and under-reports the salvage proceeds.

Capture the balance before close and calculate the difference:

Proposed fix
     ctx.accounts.vault_base_token_account.reload()?;
     let total_recovered_wsol = ctx.accounts.vault_base_token_account.amount;
+    let pre_close_lamports = ctx.accounts.vault_sol_holding_account.lamports();
     require!(
         total_recovered_wsol > 0,
         GraveVaultError::AmmRedemptionFailed
     );

     {
         let bump = [ctx.bumps.vault_authority];
         let seeds: &[&[u8]] = &[VAULT_AUTHORITY_SEED, &bump];
         let signer_seeds: &[&[&[u8]]] = &[seeds];
         let close_ctx = CpiContext::new_with_signer(
             ctx.accounts.token_program.to_account_info(),
             CloseAccount {
                 account: ctx.accounts.vault_base_token_account.to_account_info(),
                 destination: ctx.accounts.vault_sol_holding_account.to_account_info(),
                 authority: ctx.accounts.vault_authority.to_account_info(),
             },
             signer_seeds,
         );
         token::close_account(close_ctx)?;
     }

     // ...
     
-    let total = total_recovered_wsol;
+    let total = ctx
+        .accounts
+        .vault_sol_holding_account
+        .lamports()
+        .checked_sub(pre_close_lamports)
+        .ok_or(error!(GraveVaultError::MathOverflow))?;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ctx.accounts.vault_base_token_account.reload()?;
let total_recovered_wsol = ctx.accounts.vault_base_token_account.amount;
require!(
ctx.accounts.vault_base_token_account.reload()?;
let total_recovered_wsol = ctx.accounts.vault_base_token_account.amount;
let pre_close_lamports = ctx.accounts.vault_sol_holding_account.lamports();
require!(
total_recovered_wsol > 0,
GraveVaultError::AmmRedemptionFailed
);
{
let bump = [ctx.bumps.vault_authority];
let seeds: &[&[u8]] = &[VAULT_AUTHORITY_SEED, &bump];
let signer_seeds: &[&[&[u8]]] = &[seeds];
let close_ctx = CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
CloseAccount {
account: ctx.accounts.vault_base_token_account.to_account_info(),
destination: ctx.accounts.vault_sol_holding_account.to_account_info(),
authority: ctx.accounts.vault_authority.to_account_info(),
},
signer_seeds,
);
token::close_account(close_ctx)?;
}
// ...
let total = ctx
.accounts
.vault_sol_holding_account
.lamports()
.checked_sub(pre_close_lamports)
.ok_or(error!(GraveVaultError::MathOverflow))?;
🤖 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 `@programs/grave-vault/src/instructions/salvage_pool.rs` around lines 453 -
455, The code currently uses only ctx.accounts.vault_base_token_account.amount
to compute total_recovered_wsol and misses the rent-exempt lamports moved on
WSOL account close; fix by capturing the on-chain lamport balance of the token
account and/or the recipient before and after the close and using their
difference as the true recovered lamports. Concretely: before calling
close_account, read let pre_token_lamports =
ctx.accounts.vault_base_token_account.to_account_info().lamports() (and
optionally pre_sol_holding =
ctx.accounts.vault_sol_holding_account.to_account_info().lamports()); perform
the close_account call, then read post_sol_holding =
ctx.accounts.vault_sol_holding_account.to_account_info().lamports() and set
total_recovered_wsol to (post_sol_holding - pre_sol_holding) (or if you only
captured the token account, use pre_token_lamports as the recovered amount)
instead of using vault_base_token_account.amount so the rent reserve is
included.

graveyieldprotocol and others added 2 commits May 20, 2026 00:16
Locally reproduced: `anchor build` passes the BPF compile stage
(cargo-build-sbf finishes clean with platform-tools v1.54 in place
from fixup #2) but fails the IDL-generation stage:

    info: syncing channel updates for nightly-x86_64-unknown-linux-gnu
    error: could not download file from 'https://static.rust-lang.org/...'
    Error: Building IDL failed.

Anchor 0.32.1's `anchor idl build` still invokes rustup to install a
nightly toolchain, which the workflow's `dtolnay/rust-toolchain@stable`
step doesn't pre-install. The CI runner can reach static.rust-lang.org
in principle, but rustup's auto-install path needs an explicit
toolchain declared.

Fix: pass `--no-idl` to skip IDL generation. The on-chain program
builds and verifies correctly without IDL; IDL is only required for
TypeScript client type generation, which is a separate workstream
(can land later as a CI step that installs nightly before invoking
`anchor idl build`).

Verified locally with anchor-cli 0.32.1 + platform-tools v1.54 +
Solana 3.0.10:
  anchor build --no-idl  → Finished `release` profile in 5.67s (clean)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Step-level CI timing on fixup #3 reveals the actual failure:

  Step 7 'Install Anchor CLI'        completed in  0s  conclusion=success
  Step 8 'Pin platform-tools v1.54'  completed in 48s  conclusion=success
  Step 9 'Anchor build'              completed in  1s  conclusion=failure

`cargo binstall --no-confirm --version 0.32.1 anchor-cli` silently
no-ops on anchor-cli 0.32.x — it exits 0 without installing `anchor`
on PATH, then `anchor build` exits immediately (1s) because the
binary doesn't exist. This is the same silent-no-op pattern I have
in failure-pattern memory; I had closed PR #18 thinking cargo-binstall
was working (based on PR #17's intermittent success), but it's actually
flaky/broken for 0.32.x consistently.

Fix: replace cargo binstall with `cargo install --locked --version
0.32.1 anchor-cli` + an `anchor --version` assertion. Source compile
takes ~5-7 min on a cold cache but is cached by Swatinem/rust-cache@v2,
so steady-state CI time is unchanged. The version assertion fails the
install step itself on any future regression instead of deferring to
the build step where the symptom is opaque (0s install + 1s build
failure is harder to diagnose than a clean install-step failure).

Combined with fixup #2 (platform-tools v1.54) and fixup #3 (--no-idl
to skip nightly-Rust IDL generation), this should clear anchor build.

Locally verified all three together produce a clean `anchor build
--no-idl` in 5.67s on m5 and 5.02s on m6.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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