feat(m5): GraveVault salvage_pool execution path#19
Conversation
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>
|
Warning Rate limit exceeded
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 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. 📝 WalkthroughWalkthroughThis PR implements the m5 milestone execution path for Changesm5: Salvage Pool Execution Path with AMM CPI Dispatch and Distribution
Sequence Diagram: high-level salvage flowsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsGit: Failed to clone repository. Please run the 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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (13)
CHANGELOG.mddocs/PRE_MAINNET_CHECKLIST.mddocs/error_codes.mdprograms/grave-vault/src/constants.rsprograms/grave-vault/src/cpi/jupiter.rsprograms/grave-vault/src/cpi/mod.rsprograms/grave-vault/src/cpi/orca_whirlpool.rsprograms/grave-vault/src/cpi/pump_swap.rsprograms/grave-vault/src/cpi/raydium_clmm.rsprograms/grave-vault/src/cpi/raydium_v4.rsprograms/grave-vault/src/errors.rsprograms/grave-vault/src/instructions/salvage_pool.rsprograms/grave-vault/src/lib.rs
| ### 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`. | ||
|
|
There was a problem hiding this comment.
🛠️ 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.
| // 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; | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| // 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.
| require!( | ||
| ctx.accounts.vault_lp_token_account.amount >= params.salvor_lp_amount, | ||
| GraveVaultError::PreflightFailed |
There was a problem hiding this comment.
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.
| 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.
| invoke_signed( | ||
| &ix, | ||
| &[source.to_account_info(), to.clone()], | ||
| &[seeds], | ||
| ) | ||
| .map_err(|_| error!(GraveVaultError::MathOverflow)) | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| pub fn salvage_pool<'info>( | ||
| ctx: Context<'_, '_, '_, 'info, SalvagePool<'info>>, | ||
| params: SalvagePoolParams, | ||
| ) -> Result<()> { | ||
| instructions::salvage_pool::handler(ctx, params) | ||
| } |
There was a problem hiding this comment.
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:
- Has the
#[derive(Accounts)]attribute - Declares
<'info>lifetime on the struct and allAccountInfo<'info>fields - 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.
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>
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>
There was a problem hiding this comment.
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
📒 Files selected for processing (2)
.github/workflows/ci.ymlprograms/grave-vault/src/instructions/salvage_pool.rs
| - 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" |
There was a problem hiding this comment.
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.
| - 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.
| ctx.accounts.vault_base_token_account.reload()?; | ||
| let total_recovered_wsol = ctx.accounts.vault_base_token_account.amount; | ||
| require!( |
There was a problem hiding this comment.
🧩 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:
- 1: https://solana.com/docs/tokens/basics/close-account
- 2: https://github.com/solana-foundation/solana-com/blob/main/apps/docs/content/docs/en/tokens/basics/close-account.mdx
- 3: https://www.solwipe.com/blog/what-happens-when-you-close-token-account
- 4: https://github.com/solana-foundation/developer-content/blob/main/content/cookbook/tokens/close-token-accounts.md?plain=1
🏁 Script executed:
fd salvage_pool.rsRepository: GraveYield-Protocol/protocol
Length of output: 126
🏁 Script executed:
head -n 520 programs/grave-vault/src/instructions/salvage_pool.rs | tail -n +450Repository: GraveYield-Protocol/protocol
Length of output: 3168
🏁 Script executed:
sed -n '460,474p' programs/grave-vault/src/instructions/salvage_pool.rsRepository: GraveYield-Protocol/protocol
Length of output: 720
🏁 Script executed:
sed -n '497,511p' programs/grave-vault/src/instructions/salvage_pool.rsRepository: GraveYield-Protocol/protocol
Length of output: 800
🏁 Script executed:
sed -n '440,520p' programs/grave-vault/src/instructions/salvage_pool.rsRepository: 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 5Repository: 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.rsRepository: GraveYield-Protocol/protocol
Length of output: 1032
🏁 Script executed:
wc -l programs/grave-vault/src/instructions/salvage_pool.rsRepository: GraveYield-Protocol/protocol
Length of output: 130
🏁 Script executed:
rg -n "let total = total_recovered_wsol" programs/grave-vault/src/instructions/salvage_pool.rsRepository: GraveYield-Protocol/protocol
Length of output: 114
🏁 Script executed:
rg -n "total_recovered_wsol" programs/grave-vault/src/instructions/salvage_pool.rsRepository: GraveYield-Protocol/protocol
Length of output: 233
🏁 Script executed:
rg -n "close_account\|let total = " programs/grave-vault/src/instructions/salvage_pool.rs | head -30Repository: GraveYield-Protocol/protocol
Length of output: 54
🏁 Script executed:
rg -B5 "close_account\|CloseAccount" programs/grave-vault/src/instructions/salvage_pool.rsRepository: GraveYield-Protocol/protocol
Length of output: 54
🏁 Script executed:
sed -n '461,476p' programs/grave-vault/src/instructions/salvage_pool.rsRepository: GraveYield-Protocol/protocol
Length of output: 783
🏁 Script executed:
grep -n "let total_recovered_wsol\|let total = " programs/grave-vault/src/instructions/salvage_pool.rsRepository: 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.rsRepository: 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.
| 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.
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>
Summary
Wires the full Raydium V4 SOL/X salvage flow end-to-end.
salvage_poolnow actually executes a salvage instead of just running pre-flight gates:lp_holder_pool_vault,vault_sol_holding_account)params.lp_total_supply_at_snapshot == lp_mint.supply(InvalidSnapshotData7018 otherwise)TransferCPI)remove_liquiditydispatch bypool.owner— Raydium V4 real, CLMM/Orca/PumpSwap revertAmmCpiUnimplemented7017 (honest-stub pattern)vault_base.amount - removal.base_received ≥ min_quote_output_lamports)spl_token::close_account(dest=vault_sol_holding_account)system_program::transfercalls (vault_authority PDA-signs); u128 math; rounding remainder → protocolPoolRegistry+SalvageReceipt; emitPoolSalvaged+SalvageCompletedPR scope (Option B, code-path split)
This PR is the salvage_pool side. The matching
claim_lp_proceedsMerkle verification +ClaimRecorddouble-claim defense + SOL transfer fromlp_holder_pool_vaultships 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)
salvage_pool.vault_authorityPDA-signssource_lp_ownerfor the Raydium V4 withdraw viainvoke_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.PoolRegistry.lp_snapshot_merkle_root(notSalvageReceipt). m6's claim path will verify against PoolRegistry.docs/error_codes.md(sync convention I memorized as a failure pattern).UnsupportedBaseToken7019 (v1.1 needs a token-account variant oflp_holder_pool_vault+protocol_treasury).What's in this PR
New files (
programs/grave-vault/src/cpi/):mod.rs— dispatcher bypool.ownerraydium_v4.rs— real Raydium V4withdrawCPI (18-account list perprocessor.rs::process_withdraw, 9-byte data[tag=4][amount_le],amm_authorityconstant validation against5Q544…)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 (revertAmmCpiUnimplemented)Modified:
lib.rs—+ pub mod cpi;;salvage_poolsignature now threads'infoexplicitly 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_BPSinstructions/salvage_pool.rs— full handler rewrite + extendedAccountsstruct (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 toSo111…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 breakdownTest 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 --checkgreen (local verification deferred; CI will catch)cargo clippy --all-targets -- -D warningsgreenanchor buildgreen (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-lintgreen (pre-checked locally: PASS, no forbidden vocabulary)pnpm typecheckgreen (untouched; should remain green)Unverified (transparent)
amm_authorityconstant check (account[0]of remaining_accounts must equal5Q544…) catches an obviously-wrong layout but not subtle swaps like open_orders ↔ target_orders. Tracked asPRE_MAINNET_CHECKLIST.mdrow CPI-009 (🟥 blocking).route_data+ accounts verbatim; verification is localnet smoke post-merge.base_is_coin_sideis currently hardcodedtrue(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 aPRE-MAINNET-TODO(CPI)insalvage_pool.rs.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
Documentation
Chores