Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 26 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ concurrency:
cancel-in-progress: true

env:
SOLANA_VERSION: 2.0.21
# Solana 2.2.18 ships platform-tools v1.50 (cargo 1.85). Earlier 2.x
# versions (e.g. 2.0.21, 2.1.x) ship cargo 1.75/1.79, which cannot
# parse the `edition2024` manifests pulled in transitively by
# anchor-lang 0.31.1's solana-program 2.3.0 dep chain. The on-chain
# ABI is unchanged; only the BPF compile toolchain advances.
SOLANA_VERSION: 2.2.18
ANCHOR_VERSION: 0.31.1
# Host toolchain. Must match rust-toolchain.toml β€” the modern SPL crate
# ecosystem (digest 0.11 / block-buffer 0.12) requires edition2024 = 1.85+.
Expand Down Expand Up @@ -61,8 +66,14 @@ jobs:
run: |
sh -c "$(curl -sSfL https://release.anza.xyz/v${SOLANA_VERSION}/install)"
echo "$HOME/.local/share/solana/install/active_release/bin" >> "$GITHUB_PATH"
# cargo-binstall fetches a prebuilt anchor-cli binary in ~10s instead
# of compiling from source (~5-7 min, which has been intermittently
# cancelled on ubuntu-latest runners during the dependency-fetch
# phase). Drops total anchor-build job time from ~10 min to ~2 min.
- name: Install cargo-binstall
uses: cargo-bins/cargo-binstall@main
- name: Install Anchor CLI
run: cargo install --locked --version ${ANCHOR_VERSION} anchor-cli
run: cargo binstall --no-confirm --version ${ANCHOR_VERSION} anchor-cli
- name: Anchor build
run: anchor build

Expand All @@ -71,15 +82,22 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v6
with:
version: ${{ env.PNPM_VERSION }}
# pnpm/action-setup@v6 requires Node.js to already be installed
# (upstream removed the bundled Node in v6). setup-node MUST come
# first. The previous floating-tag failure at install time was this
# ordering, not a regression in v6.0.7/v6.0.8.
- uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
# NOTE: `cache: pnpm` was removed. setup-node hashes `**/pnpm-lock.yaml`
# to compute the cache key, and no lockfile is committed yet (scaffold
# gap). Re-add once a `pnpm-lock.yaml` lands at the repo root.
# pnpm/action-setup@v6 auto-detects pnpm version from the
# `packageManager` field in package.json (pnpm@9.12.0). No explicit
# `version:` input β€” that field is what we want to be the single
# source of truth for the pnpm pin anyway.
- uses: pnpm/action-setup@v6
# NOTE: `cache: pnpm` was removed from setup-node. setup-node hashes
# `**/pnpm-lock.yaml` to compute the cache key, and no lockfile is
# committed yet (scaffold gap). Re-add once a `pnpm-lock.yaml` lands
# at the repo root.
- run: pnpm install --frozen-lockfile=false
- run: pnpm -r typecheck

Expand Down
6 changes: 5 additions & 1 deletion Anchor.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
[toolchain]
anchor_version = "0.31.1"
solana_version = "2.0.21"
# 2.2.18 ships platform-tools v1.50 (cargo 1.85), the first BPF toolchain
# that handles the `edition2024` manifests pulled in transitively by
# anchor-lang 0.31.1. Earlier 2.x versions (2.0.21, 2.1.x) ship cargo
# 1.75/1.79 and fail at dep-fetch time. On-chain runtime ABI unchanged.
solana_version = "2.2.18"

[features]
resolution = true
Expand Down
8 changes: 8 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ readme = "README.md"
anchor-lang = "0.31.1"
anchor-spl = "0.31.1"

# cargo-build-sbf reads this to pick the platform-tools version for the
# BPF compile. v2.2.18's default is v1.48 (cargo 1.84), which can't parse
# the edition2024 manifests pulled in transitively by anchor-lang 0.31.1.
# v1.54 (2026-03-06) ships cargo 1.85+, which is the first BPF toolchain
# that handles edition2024 cleanly.
[workspace.metadata.solana]
tools-version = "v1.54"

[profile.release]
overflow-checks = true
lto = "fat"
Expand Down
15 changes: 12 additions & 3 deletions docs/PRE_MAINNET_CHECKLIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,12 @@ Status legend: πŸŸ₯ blocking Β· 🟧 high-priority Β· 🟑 medium Β· ⬜ trackin
| ID | File | Status | Description |
| --- | --- | --- | --- |
| ORACLE-001 | `programs/grave-scanner/src/instructions/record_launch_price.rs` | 🟧 | Cross-check `launch_price_q64x64` against on-chain pool reserves at the supplied `first_swap_slot` rather than trusting the caller. Reverts with `PoolDataParseError` on mismatch. Verify against AMM transaction history at the recorded slot. |
| ORACLE-002 | `programs/grave-scanner/src/instructions/evaluate_pool_phase1.rs` (also Phase 2) | πŸŸ₯ | Cryptographic proof of last-swap timestamp. Currently passed as a parameter and trusted; production handler must verify via adapter-provided last-swap proof or signed indexer attestation. |
| ORACLE-002 | `programs/grave-scanner/src/instructions/evaluate_pool_phase1.rs` (also Phase 2) | πŸŸ₯ | Cryptographic proof of last-swap timestamp. Currently passed as a parameter and trusted; production handler must verify via adapter-provided last-swap proof or signed indexer attestation. Raydium V4 adapter (m4) returns `0` as a sentinel since AmmInfo doesn't carry a last-swap timestamp β€” handler still reads the param until this row retires. |

### CPI

| ID | File | Status | Description |
| --- | --- | --- | --- |
| CPI-001 | `programs/grave-scanner/src/adapters/raydium_v4.rs` | πŸŸ₯ | Raydium V4 pool layout parsing. Reverts with `AmmAdapterUnimplemented`. Verify against mainnet pool fixtures (e.g. `9d9mb8kooFfaD3SctgZtkxQypkshx6ezhbKio89ixyy2` SOL/USDC) and Raydium SDK source-of-truth field offsets. |
| CPI-002 | `programs/grave-scanner/src/adapters/raydium_clmm.rs` | 🟧 | Raydium CLMM pool layout parsing + tick-range reserve calculation. v1.1 milestone. |
| CPI-003 | `programs/grave-scanner/src/adapters/orca_whirlpool.rs` | 🟧 | Orca Whirlpool layout + token-vault reserve aggregation. |
| CPI-004 | `programs/grave-scanner/src/adapters/pumpswap.rs` | 🟧 | PumpSwap pool layout parsing. |
Expand All @@ -72,12 +71,22 @@ Status legend: πŸŸ₯ blocking Β· 🟧 high-priority Β· 🟑 medium Β· ⬜ trackin
1. Implement the change. Replace the `PRE-MAINNET-TODO(...)` marker with
either a `// TODO(post-launch): ...` if there is residual cleanup, or
delete it entirely if the resolution is complete.
2. Move the row in this file under a `## Retired` section with a SHA
2. Move the row in this file under the `## Retired` section with a SHA
reference to the implementing PR.
3. CI's pre-mainnet-todo audit (added in a follow-up PR) cross-checks
that every grep'd marker has a matching live row in this file and
vice versa.

## Retired

Rows retired by shipped implementations. Each entry references the
implementing PR; the merge SHA is filled in by a tiny follow-up commit
after the PR lands so the row can be tagged to its exact post-merge SHA.

| ID | File | Retired by | Merge SHA |
| --- | --- | --- | --- |
| CPI-001 | `programs/grave-scanner/src/adapters/raydium_v4.rs` | PR #13 (m4: Raydium V4 layout adapter) | `<filled by post-merge fix-up commit>` |

## Audit handoff

When handing this checklist to OtterSec / Neodyme, run
Expand Down
17 changes: 10 additions & 7 deletions programs/grave-scanner/src/adapters/meteora.rs
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
// SPDX-License-Identifier: Apache-2.0
//
// Meteora DLMM / Dynamic Pools adapter.
// Meteora (DLMM + Dynamic AMM) pool adapter β€” stub.
//
// Mainnet program ID is parameterizable per pool variant (DLMM vs
// Dynamic AMM); the placeholder below is the DLMM program. Meteora
// support is a v1.1 roadmap item; V4/Whirlpool cover the v1.0 launch
// scope.
// Mainnet program ID is AUDIT-PENDING β€” both Meteora DLMM and Dynamic AMM
// are supported targets; the canonical placeholder here resolves to the
// DLMM program at the time of writing but the KEYS-001 row in
// docs/PRE_MAINNET_CHECKLIST.md tracks the confirmation step.

use anchor_lang::prelude::*;
use anchor_lang::solana_program::pubkey;

use super::PoolData;
use crate::errors::GraveScannerError;

/// Meteora DLMM program ID.
/// Mainnet Meteora DLMM program ID β€” placeholder until confirmed.
///
/// PRE-MAINNET-TODO(KEYS): confirm Meteora DLMM mainnet program ID and add Dynamic AMM variant | reverts: UnsupportedAmm if owner mismatch | verify: against Meteora SDK constants
pub const PROGRAM_ID: Pubkey = pubkey!("LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo");

/// Parse a Meteora pool account into `PoolData`.
///
/// PRE-MAINNET-TODO(CPI): Meteora DLMM/Dynamic AMM pool layout parsing | reverts: AmmAdapterUnimplemented | verify: against Meteora SDK
pub fn parse(_pool_account_info: &AccountInfo) -> Result<PoolData> {
pub fn parse(
_pool_account_info: &AccountInfo,
_remaining_accounts: &[AccountInfo],
) -> Result<PoolData> {
err!(GraveScannerError::AmmAdapterUnimplemented)
}
39 changes: 29 additions & 10 deletions programs/grave-scanner/src/adapters/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@
// `criteria` evaluator. New AMM support is added by implementing a new
// adapter module here.
//
// All adapter parse functions in this milestone are honest stubs that
// revert with a named error code (`AmmAdapterUnimplemented` for AMMs,
// `LockerAdapterUnimplemented` for lockers). The architectural surface
// is exercised by virtue of compilation; runtime behavior is supplied
// by follow-up PRs that implement the actual byte-layout parsing.
// Convention since m4 (Raydium V4 layout adapter): each adapter takes
// both the pool account AND the `remaining_accounts` slice. Reserves and
// LP supply for the pool live on separate SPL token accounts (the pool's
// coin_vault, pc_vault, lp_mint) β€” these MUST be included in
// `remaining_accounts` by the caller. Adapters look them up by Pubkey.
// Adapters whose layout parsing isn't yet implemented continue to revert
// `AmmAdapterUnimplemented`.
//
// Locker adapter (`locker.rs`) follows the same `remaining_accounts`
// convention for UNCX / PinkSale / Team Finance locker container accounts.
//
// Auditor's one-liner: `grep -rn "PRE-MAINNET-TODO" programs/`.

Expand All @@ -33,6 +38,11 @@ pub mod raydium_v4;
#[derive(Clone, Copy, Debug)]
pub struct PoolData {
/// Unix timestamp of the last on-chain swap. Used for Criterion 1.
/// AMM-specific: Raydium V4 does not store a last-swap timestamp on
/// the pool account; in that case the adapter returns 0 and the
/// handler reads `last_swap_unix_ts` from instruction params instead
/// (gated by the ORACLE-002 PRE-MAINNET-TODO until indexer-signed
/// attestation lands).
pub last_swap_unix_ts: i64,
/// Base-side reserves (the memecoin / measured token).
pub base_reserve: u64,
Expand All @@ -44,6 +54,8 @@ pub struct PoolData {
pub base_mint: Pubkey,
/// Mint of the quote token.
pub quote_mint: Pubkey,
/// LP mint β€” needed for the Criterion 5 locker check.
pub lp_mint: Pubkey,
}

impl PoolData {
Expand All @@ -70,9 +82,16 @@ impl PoolData {
/// IDs declared in each adapter module. Mismatches return
/// `UnsupportedAmm`; supported AMMs whose parser is not yet implemented
/// return `AmmAdapterUnimplemented`.
///
/// `remaining_accounts` is the full instruction `remaining_accounts`
/// slice. Adapters look up the pool's vault and lp_mint accounts here
/// (via inline iteration β€” no helper function carries a reference
/// across a call boundary, so no `'info` lifetime parameter is required
/// on this dispatch or its callers).
pub fn extract_pool_data(
pool_account_info: &AccountInfo,
expected_pool_address: &Pubkey,
remaining_accounts: &[AccountInfo],
) -> Result<PoolData> {
require_keys_eq!(
*pool_account_info.key,
Expand All @@ -82,15 +101,15 @@ pub fn extract_pool_data(

let owner = pool_account_info.owner;
if owner == &raydium_v4::PROGRAM_ID {
raydium_v4::parse(pool_account_info)
raydium_v4::parse(pool_account_info, remaining_accounts)
} else if owner == &raydium_clmm::PROGRAM_ID {
raydium_clmm::parse(pool_account_info)
raydium_clmm::parse(pool_account_info, remaining_accounts)
} else if owner == &orca_whirlpool::PROGRAM_ID {
orca_whirlpool::parse(pool_account_info)
orca_whirlpool::parse(pool_account_info, remaining_accounts)
} else if owner == &pumpswap::PROGRAM_ID {
pumpswap::parse(pool_account_info)
pumpswap::parse(pool_account_info, remaining_accounts)
} else if owner == &meteora::PROGRAM_ID {
meteora::parse(pool_account_info)
meteora::parse(pool_account_info, remaining_accounts)
} else {
err!(GraveScannerError::UnsupportedAmm)
}
Expand Down
15 changes: 8 additions & 7 deletions programs/grave-scanner/src/adapters/orca_whirlpool.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
// SPDX-License-Identifier: Apache-2.0
//
// Orca Whirlpool pool adapter.
// Orca Whirlpool pool adapter β€” stub.
//
// Mainnet program ID: whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc
//
// Whirlpool accounts hold sqrt_price_x64 and tick state; reserves
// are computed from token-vault accounts owned by the whirlpool. The
// `Whirlpool::reward_last_updated_timestamp` and explicit on-chain
// counters can serve as an upper bound on the last-swap timestamp.
// Whirlpool is also concentrated-liquidity; reserves require token-vault
// aggregation rather than a single base/quote read.

use anchor_lang::prelude::*;
use anchor_lang::solana_program::pubkey;
Expand All @@ -18,9 +16,12 @@ use crate::errors::GraveScannerError;
/// Mainnet Orca Whirlpool program ID.
pub const PROGRAM_ID: Pubkey = pubkey!("whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc");

/// Parse an Orca Whirlpool account into `PoolData`.
/// Parse an Orca Whirlpool pool account into `PoolData`.
///
/// PRE-MAINNET-TODO(CPI): Orca Whirlpool layout parsing + token-vault reserve aggregation | reverts: AmmAdapterUnimplemented | verify: against Whirlpool SDK and mainnet pool fixtures
pub fn parse(_pool_account_info: &AccountInfo) -> Result<PoolData> {
pub fn parse(
_pool_account_info: &AccountInfo,
_remaining_accounts: &[AccountInfo],
) -> Result<PoolData> {
err!(GraveScannerError::AmmAdapterUnimplemented)
}
14 changes: 7 additions & 7 deletions programs/grave-scanner/src/adapters/pumpswap.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
// SPDX-License-Identifier: Apache-2.0
//
// PumpSwap (pump.fun's bonding-curve graduated AMM) pool adapter.
// PumpSwap (pump.fun graduated AMM) pool adapter β€” stub.
//
// Mainnet program ID: PSwapMdSai8tjrEXcxFeQth87xC4rRsa4VA5mhGhXkP
//
// PumpSwap is the post-graduation AMM for pump.fun tokens that
// reach the bonding-curve cap. Pre-graduation pools live in the
// pump.fun program itself and are out of scope for derelict-pool
// salvage (graduations are gated by liquidity thresholds GraveYield
// would never trigger on).
// PumpSwap pools share a similar two-vault shape with Raydium V4, but
// the AMM account layout differs in field offsets and the vault types.

use anchor_lang::prelude::*;
use anchor_lang::solana_program::pubkey;
Expand All @@ -22,6 +19,9 @@ pub const PROGRAM_ID: Pubkey = pubkey!("PSwapMdSai8tjrEXcxFeQth87xC4rRsa4VA5mhGh
/// Parse a PumpSwap pool account into `PoolData`.
///
/// PRE-MAINNET-TODO(CPI): PumpSwap pool layout parsing | reverts: AmmAdapterUnimplemented | verify: against PumpSwap SDK and mainnet pool fixtures
pub fn parse(_pool_account_info: &AccountInfo) -> Result<PoolData> {
pub fn parse(
_pool_account_info: &AccountInfo,
_remaining_accounts: &[AccountInfo],
) -> Result<PoolData> {
err!(GraveScannerError::AmmAdapterUnimplemented)
}
14 changes: 8 additions & 6 deletions programs/grave-scanner/src/adapters/raydium_clmm.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
// SPDX-License-Identifier: Apache-2.0
//
// Raydium CLMM (concentrated liquidity) pool adapter.
// Raydium CLMM (Concentrated Liquidity Market Maker) pool adapter β€” stub.
//
// Mainnet program ID: CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK
//
// CLMM pool state lives in `PoolState` accounts; reserves are
// derived from the active tick range and not directly stored as
// "amounts" the way V4 does. Salvage of CLMM positions is a v1.1
// roadmap item β€” V4 covers the bulk of derelict pools.
// Concentrated liquidity introduces tick-range reserve calculation; this
// adapter must aggregate across active tick ranges rather than reading a
// single vault pair. v1.1 milestone.

use anchor_lang::prelude::*;
use anchor_lang::solana_program::pubkey;
Expand All @@ -21,6 +20,9 @@ pub const PROGRAM_ID: Pubkey = pubkey!("CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKg
/// Parse a Raydium CLMM pool account into `PoolData`.
///
/// PRE-MAINNET-TODO(CPI): Raydium CLMM pool layout parsing + tick-range reserve calculation | reverts: AmmAdapterUnimplemented | verify: against mainnet pool fixtures and Raydium CLMM SDK
pub fn parse(_pool_account_info: &AccountInfo) -> Result<PoolData> {
pub fn parse(
_pool_account_info: &AccountInfo,
_remaining_accounts: &[AccountInfo],
) -> Result<PoolData> {
err!(GraveScannerError::AmmAdapterUnimplemented)
}
Loading