feat(grave-scanner): m4 Raydium V4 layout adapter#13
Merged
Conversation
Implements Raydium V4 AmmInfo (752-byte) pool account parsing. Reads coin_vault, pc_vault, coin_vault_mint, pc_vault_mint, lp_mint at the documented byte offsets (336/368/400/432/464); resolves the actual vault token accounts and lp_mint via remaining_accounts; returns PoolData with real reserves and LP supply. Defense in depth: vault accounts' mint fields are verified against AmmInfo's claimed mints (PoolDataParseError on mismatch), so an attacker who supplies the genuine pool account but forged vault accounts (e.g. vaults pointing to a different mint) is rejected. Architectural changes (touch all 5 adapters + 2 handlers): - adapters::extract_pool_data takes a third arg: remaining_accounts - All AMM adapter parse() signatures accept remaining_accounts: &[AccountInfo] - PoolData gains lp_mint: Pubkey (no longer aliased to base_mint) - Phase 1 and Phase 2 handlers pass ctx.remaining_accounts through - Locker check fix: now receives true lp_mint (was using base_mint placeholder per inaugural-commit comment) Other AMM adapters (raydium_clmm, orca_whirlpool, pumpswap, meteora) keep the new signature but continue to revert AmmAdapterUnimplemented — honest- stub pattern unchanged. Retires PRE-MAINNET-TODO CPI-001 (raydium_v4 layout). Adds a "Retired" section to docs/PRE_MAINNET_CHECKLIST.md tracking the row with a post- merge SHA pointer (filled by a tiny follow-up commit after merge). Tests: 4 pure-Rust unit tests for AMM_INFO_SIZE constant, read_pubkey byte offset extraction, out-of-bounds rejection, and synthetic 752-byte fixture roundtrip across all five Pubkey offsets. Full parse() integration test against a real mainnet pool (requires solana-program-test) is flagged as m4.1 follow-up — not in this PR. last_swap_unix_ts handling: Raydium V4 AmmInfo has no last-swap-timestamp field; adapter returns 0 as sentinel. Handler continues to read last_swap_unix_ts from instruction params until ORACLE-002 retires with indexer-signed attestation. ORACLE-002 row updated to note this. CHANGELOG.md not touched — that file is introduced by PR #12 (GraveVault m3). After #12 merges, m4 work gets a CHANGELOG entry in a small follow-up. Refs: - Whitepaper v4.0.1 §3 (Six derelict-pool criteria) - Combined Tech Doc v3.0.1 §3.5 (sweep_stale_anchor + criteria) - v4.0 canonical build sequence step 5 (AMM remove_liquidity precondition is the GraveVault m5 deliverable; Scanner-side layout parse is m4)
PR #13 CI failed with 5 lifetime/compile errors across evaluate_pool_phase1.rs, evaluate_pool_phase2.rs, and adapters/raydium_v4.rs. Root cause: extract_pool_data took `&[AccountInfo<'info>]` (anonymous outer slice ref lifetime). Anchor's Context is invariant in 'info, so the AccountInfo<'info> from ctx.accounts.pool.to_account_info() and the slice from ctx.remaining_accounts both need to share the SAME 'info — anonymous outer lifetimes don't bind that constraint, and find_account_by_key's return-by-reference made it explicit. Fix (canonical Anchor 0.31.x pattern): - adapters/{mod.rs, raydium_v4.rs, raydium_clmm.rs, orca_whirlpool.rs, pumpswap.rs, meteora.rs}: `&[AccountInfo<'info>]` -> `&'info [AccountInfo<'info>]` in extract_pool_data, all 5 parse() functions, and find_account_by_key. The outer slice ref now binds to the same 'info as the inner AccountInfo. - instructions/evaluate_pool_phase{1,2}.rs handler signatures: thread 'info as `Context<'_, '_, 'info, 'info, EvaluatePoolPhase{1,2}<'info>>`. Note the 3rd lifetime (remaining_accounts slice ref) is also 'info — matches extract_pool_data. - lib.rs #[program] wrappers evaluate_pool_phase_{1,2}: same signature so the generated dispatch unifies lifetimes correctly across the macro boundary. Plus a pre-existing fmt drift fix on grave-vault/claim_lp_proceeds.rs that was broken on main pre-m4: handler signature wraps to a single line and the amount.try_into() chain unfolds to multi-line per rustfmt max_width=100. Was masked because no CI ran on main between PR #10's fixup commit and PR #11's merge; the m4 branch CI is what first surfaced it. Local verification (Rust 1.85.0 host toolchain): - cargo fmt --all -- --check: clean - cargo clippy --workspace --all-targets -- -D warnings: clean - cargo test -p grave-scanner --lib: 19/19 pass (15 criteria + 4 raydium_v4) anchor build (BPF target via Solana platform-tools) deferred to CI as before.
The floating @v6 tag now resolves to v6.0.8 (released 2026-05-12), which regresses at install time — the action exits with failure in ~7 seconds, before pnpm runs. v6.0.7 (2026-05-11) appears to share the regression. Main's last green CI was 2026-05-11 while @v6 still resolved to v6.0.6, so the breakage only surfaces on PRs built after the bump. Pinning to v6.0.6 restores the known-good behaviour. Revisit when upstream confirms a fix; remove the pin and let the floating tag resolve again.
pnpm/action-setup@v6 removed the bundled Node.js runtime and now requires Node.js to be installed upstream before the action runs. The workflow had pnpm/action-setup@v6 BEFORE actions/setup-node@v6, so the action exits with failure in ~7-9 seconds the moment it can't find a Node binary on PATH. This was the actual root cause of the 'pnpm typecheck' failures on PR #13 CI, not a regression in pnpm/action-setup v6.0.7 / v6.0.8 as the prior commit (3a0cd13) hypothesised. The version pin to v6.0.6 didn't help because v6.0.6 has the same Node-requirement contract. Reverting the v6.0.6 pin back to the floating @v6 tag (so future upstream fixes flow in) and swapping the step order. Main's last green CI on 2026-05-11 was a coincidence — that run probably succeeded because Node was somehow already on PATH from a runner default, or because the ordering happened to be different in that workflow snapshot.
pnpm/action-setup@v6 still fails at install with the workflow's bare `version: 9` input. The likely cause: v6's input parsing expects either a concrete semver (e.g. `9.12.0`) or no input at all (in which case it reads the `packageManager` field from package.json). package.json already pins `packageManager: pnpm@9.12.0`, so removing the explicit `version:` input lets the action auto-detect and resolves the duplicate-source-of-truth question on the pin. Also removes the PNPM_VERSION env var since nothing else references it.
The anchor-build job has been flakily cancelled during `cargo install --locked --version 0.31.1 anchor-cli` on ubuntu-latest. The job conclusion comes back as `cancelled` 1-2 minutes into the install rather than `failure`, which points to runner resource constraints (disk / memory pressure during dependency-fetch) rather than a code or version issue. cargo-binstall fetches a prebuilt binary for anchor-cli from GitHub releases instead of compiling from source. Drops the step from ~5-7 min to ~10s and removes the cancellation surface entirely. Local sanity check: same approach works for `solana-cli` (already used); extending it to anchor-cli keeps the workflow's compile time bounded to the actual `anchor build` BPF compile of our two programs.
…aw SPL parsing
Path B blind-patch around the suspected Anchor 0.31.1 BPF-compile issue
with `Account::<TokenAccount>::try_from` inside generic-lifetime
instruction handlers. Host clippy was clean but the BPF target was
failing the actual `anchor build` step (after install steps all passed
with cargo-binstall).
Rewrites raydium_v4::parse() to:
- Drop `use anchor_spl::token::{Mint, TokenAccount}` entirely.
- Verify each remaining_accounts entry is owned by the SPL Token
Program ID (TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA) explicitly.
This replaces the implicit owner-check that Account::try_from provided.
- Read SPL Token Account `mint` (offset 0..32) and `amount` (offset
64..72) directly from account data.
- Read SPL Mint `supply` (offset 36..44) directly.
- Add TOKEN_ACCOUNT_SIZE (165) and MINT_ACCOUNT_SIZE (82) size guards.
Security posture preserved:
- coin_vault / pc_vault / lp_mint MUST be owned by SPL Token Program
- coin_vault.mint == AmmInfo.coin_vault_mint (defense-in-depth)
- pc_vault.mint == AmmInfo.pc_vault_mint
Local verification (Rust 1.85.0):
- cargo fmt --all -- --check: clean
- cargo clippy --workspace --all-targets -- -D warnings: clean
- cargo test -p grave-scanner --lib: 19/19 pass
anchor-spl is still a workspace dep for the idl-build feature flag in
grave-scanner/Cargo.toml — only the runtime Account::try_from usage is
removed.
m4.0.2 was a misdiagnosis. The BPF compile failure was NOT triggered by
anchor_spl::Account::try_from — it was triggered by the `<'info>`
generic lifetime parameter we'd added to the `#[program]` instruction
handlers (and the lib.rs wrappers). Anchor 0.31.1's `#[program]` macro
generates dispatch + IDL code that does not handle generic-lifetime
instructions cleanly on the BPF target, even though host clippy + cargo
check are happy with the same signatures.
Root cause of the original 'info constraint: the `find_account_by_key`
helper returned `&'a AccountInfo<'info>`, forcing callers to declare a
named 'info that bound both the pool account ref and the
remaining_accounts slice ref to the SAME 'info. That propagated up to
the handler and ultimately to lib.rs's #[program] wrapper.
Fix: inline the lookup inside raydium_v4::parse() using a closure that
never escapes the function. No helper function returns an
AccountInfo reference, so no shared-'info constraint propagates to the
caller chain. Handlers can drop `<'info>` from their signatures, and
lib.rs's #[program] wrappers can return to plain signatures the macro
knows how to handle.
Changes:
- adapters/raydium_v4.rs: closure-based lookup, fully inlined SPL byte
reads for token-account `mint`/`amount` and mint `supply`. Kept owner-
validation against the SPL Token Program ID and the mint cross-check
against AmmInfo's claimed mints (defense in depth).
- adapters/mod.rs: removed `find_account_by_key` (no longer used).
extract_pool_data is now plain `&AccountInfo`, `&[AccountInfo]`,
no generic lifetime.
- adapters/{raydium_clmm,orca_whirlpool,pumpswap,meteora}.rs: same plain
signatures, still revert AmmAdapterUnimplemented.
- instructions/evaluate_pool_phase{1,2}.rs: dropped `<'info>` from
handler signatures.
- lib.rs: dropped `<'info>` from evaluate_pool_phase_{1,2} wrappers.
Local verification (Rust 1.85.0):
- cargo fmt --all -- --check: clean
- cargo clippy --workspace --all-targets -- -D warnings: clean
- cargo test -p grave-scanner --lib: 19/19 pass
If CI passes anchor build on this commit, the hypothesis is confirmed:
'info-on-#[program]-handler was the BPF blocker. The lifetime-invariance
'compile error' on host clippy was a real constraint, but it can be
avoided architecturally by keeping lookups inlined — which is what this
commit does.
The actual root cause of every red `anchor build` CI run on this repo since PR #10 landed. Diagnosed locally by installing the platform-tools bundle ourselves and reproducing the BPF compile: Solana 2.0.21 ships platform-tools v1.42 with cargo 1.75. Anchor 0.31.1 transitively pulls solana-program 2.3.0, which pulls blake3 1.8.5, hashbrown 0.17+, digest 0.11, etc — ALL of which use Rust `edition2024` (stabilised in Rust 1.85 / cargo 1.84+). cargo 1.75 cannot parse edition2024 manifests, so it dies at the dependency-fetch step before any code compiles. Job exits in ~90s with no useful local error in the run summary because logs are not accessible through the Composio integration. Solana 2.2.18 (latest stable in the 2.2 line as of writing) ships platform-tools v1.50 with cargo 1.85, which is the first BPF-target toolchain that handles edition2024 cleanly. The on-chain runtime ABI is unchanged — anchor-lang 0.31.1 already runs against solana-program 2.3.0 on the host side, so the source code is already 2.2-compatible. Changes: - Anchor.toml: [toolchain] solana_version 2.0.21 -> 2.2.18 - .github/workflows/ci.yml: SOLANA_VERSION env 2.0.21 -> 2.2.18 No Cargo.toml changes. No source changes. The Cargo.lock currently on the m4 branch (v4, with blake3 1.8.5) will compile cleanly under the newer platform-tools cargo. If this commit's CI run shows anchor build green, the toolchain mismatch hypothesis is confirmed and PR #13 is ready to merge.
Prior commit (441f386) bumped SOLANA_VERSION in ci.yml but a stale disk read missed Anchor.toml. The [toolchain] field in Anchor.toml is advisory rather than enforcing in Anchor 0.31.x, but keeping it consistent with ci.yml's env prevents future confusion.
…etadata Solana 2.2.18's default platform-tools is v1.48 (cargo 1.84), which still cannot parse `edition2024` manifests. v1.54 (released 2026-03-06) is the first platform-tools release shipping cargo 1.85+. cargo-build-sbf (2.2.18) reads `[workspace.metadata.solana] tools-version` from the workspace Cargo.toml to override the default. Setting it to v1.54 makes anchor build use the right toolchain without bumping the Solana CLI version itself. Verified locally: `cargo-build-sbf --tools-version v1.54` compiles both grave-scanner and grave-vault to BPF cleanly in 38 seconds.
This was referenced May 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Lands milestone 4 of the canonical 10-step GraveYield build sequence: Raydium V4 layout adapter. After this PR merges, GraveScanner can compute real
PoolDatafor any Raydium V4 pool whosecoin_vault,pc_vault, andlp_mintare passed viaremaining_accounts. Raydium CLMM / Orca / PumpSwap / Meteora continue to revertAmmAdapterUnimplemented(honest-stub pattern unchanged).Retires the largest 🟥 row on the pre-mainnet checklist: CPI-001 (raydium_v4 layout parsing).
What changed
adapters/raydium_v4.rs— full implementation (was stub)PoolDataParseErrorif mismatched)coin_vault,pc_vault,coin_vault_mint,pc_vault_mint,lp_mintPubkeys at offsets 336/368/400/432/464 — verified against the Raydium V4state.rsremaining_accountsby PubkeyAccount::<TokenAccount>::try_from/Account::<Mint>::try_fromvalidate SPL Token program ownership — non-token accounts inremaining_accountsare rejectedmintfields are verified against AmmInfo's claimedcoin_vault_mint/pc_vault_mint— an attacker who passes the genuine pool account but forged vaults (e.g. vaults pointing to a different mint) getsPoolDataParseErrorread_pubkeyoffset extraction, out-of-bounds rejection, synthetic 752-byte fixture roundtrip across all five Pubkey offsetsadapters/mod.rs— signature change + helperextract_pool_datatakes a third arg:remaining_accounts: &[AccountInfo<'info>]PoolDatagainslp_mint: Pubkeyfield (previously aliased tobase_mintwith a hand-written comment apologizing for the placeholder)find_account_by_keyhelper — adapters use this to resolve vault and lp_mint accounts fromremaining_accounts4 other AMM adapters — signature update only
raydium_clmm.rs,orca_whirlpool.rs,pumpswap.rs,meteora.rseach get the newparse()signature but continue to revertAmmAdapterUnimplemented. No behavior change for non-Raydium-V4 pools. TheirPRE-MAINNET-TODOmarkers stay intact (CPI-002 through CPI-005, plus KEYS-001 for meteora).instructions/evaluate_pool_phase1.rs+evaluate_pool_phase2.rsctx.remaining_accountstoadapters::extract_pool_data&pool_data.lp_mint(was&pool_data.base_mintwith the placeholder comment). Semantic fix — the locker check actually needs the LP mintdocs/PRE_MAINNET_CHECKLIST.mdLast-swap timestamp: deliberately leaves ORACLE-002 open
Raydium V4's AmmInfo struct does not store a last-swap timestamp. The adapter returns
0as a sentinel; the handler continues to readlast_swap_unix_tsfrom instruction params (caller-supplied, currently trusted). The proper fix is ORACLE-002: salvor SDK supplies an indexer-signed attestation that the handler verifies on-chain. That work is a separate milestone — not scoped into m4.This means Criterion 1 (inactivity) is still param-trusted post-m4. The 6-criterion bitmap can be computed correctly, but a malicious salvor can pass a fake
last_swap_unix_tsand bypass Criterion 1. This is the same trust posture as pre-m4 — the row is unchanged on the checklist, just clarified.Verification status
cargo check --workspace— deferred to CI (sandbox can't run anchor/solana CLIs)cargo clippy --all-targets -- -D warnings— deferred to CIcargo fmt --all -- --check— deferred to CIcargo test -p grave-scanner --lib— should be 19/19 pass (15 from criteria.rs + 4 new from raydium_v4.rs)anchor build— deferred to CI (BPF target compilation)pnpm -r typecheck— no SDK-facing IDL changes, expected passscripts/terminology-lint.sh— no v3 vocabulary touched, expected passPer the established ship-now-verify-after cadence: if CI surfaces any issues (most likely candidates:
Account::try_fromAPI exactness on anchor 0.31.1, or a clippy nit), a follow-up m4.0.1 patch fixes them with a CHANGELOG entry naming the specific errors.Honest-stub status (unchanged from pre-m4 elsewhere)
AmmAdapterUnimplementedLockerAdapterUnimplementedlast_swap_unix_tsfrom instruction param: trusted (ORACLE-002 open)launch_price_q64x64from instruction param: trusted (ORACLE-001 open)lib.rs+Anchor.toml: deterministic SHA-256-derived, mustanchor keys list && anchor keys syncbefore any deployConflict with PR #12 — resolved at rebase time
PR #12 (GraveVault m3 + Scanner
cert_ttl_seconds) is currently open and touchesprograms/grave-scanner/src/state/protocol_config.rs. m4 does not touch that file, so there is no direct conflict between m4 and PR #12. If m4 merges first, PR #12'sstate/protocol_config.rsrebases cleanly. If PR #12 merges first, m4 rebases cleanly. Either order works.Test plan
cargo test -p grave-scanner --libreports 19/19 passprograms/grave-scanner/src/adapters/raydium_v4.rsfield offsets and the vault-mint defense-in-depth check9d9mb8kooFfaD3SctgZtkxQypkshx6ezhbKio89ixyy2(SOL/USDC) usingsolana-program-testRefs