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
91 changes: 91 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Changelog

All notable changes to the GraveYield protocol monorepo are documented here.
The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
Version bumps in this file refer to the workspace as a whole; per-program
version pinning lives in each program's `Cargo.toml`.

## [Unreleased]

## [v1.0.6] — 2026-05-10

### m3 — GraveVault `salvage_pool` pre-flight + cert freshness gates

This release lands milestone 3 of the canonical 10-step build sequence:
**GraveVault `salvage_pool` pre-flight + PoolRegistry**. The CPI bodies
for AMM `remove_liquidity` (m5), Jupiter swap (m6), and 40/40/20
distribution (m7) remain honest-stubbed and explicitly marked.

#### Added

- **`MIN_CERT_TTL_SECONDS = 600`** floor in `programs/grave-scanner/src/constants.rs`.
Hardcoded; raising it requires a program upgrade.
- **`ProtocolConfig.cert_ttl_seconds: i64`** field on the GraveScanner
ProtocolConfig (governance-configurable, 72h timelocked, default 3600s).
This replaces the previously-hardcoded `ELIGIBILITY_CERT_TTL_SECONDS`
const at the runtime path in `evaluate_pool_phase_2`. The const itself
is retained as `DEFAULT_CERT_TTL_SECONDS` for default-handling at init,
and an `#[deprecated]` alias is left at `ELIGIBILITY_CERT_TTL_SECONDS`
for backwards-compatible test fixtures.
- **Error 6019 `CertTtlBelowMinimum`** on GraveScanner. Raised by
`initialize` and `update_protocol_config` when a `cert_ttl_seconds`
parameter falls below `MIN_CERT_TTL_SECONDS`.
- **`init_if_needed` Anchor feature** on `programs/grave-vault/Cargo.toml`
for the `lp_holder_pool_vault` SystemAccount. First salvage of a pool
creates the 0-data system-owned PDA; subsequent salvages of the same
pool are gated upstream by the `pool_registry` init constraint.
Comment on lines +34 to +37

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 | 🟡 Minor | ⚡ Quick win

Fix the init_if_needed release-note claim; it contradicts the implemented Anchor 0.32 migration.

Line 34 and Line 56 document init_if_needed on SystemAccount, but this PR’s migration notes indicate the opposite path (manual lazy creation CPI after Anchor 0.32 rejection). Please update this section to match the actual final implementation.

Proposed changelog edit
-- **`init_if_needed` Anchor feature** on `programs/grave-vault/Cargo.toml`
-  for the `lp_holder_pool_vault` SystemAccount. First salvage of a pool
-  creates the 0-data system-owned PDA; subsequent salvages of the same
-  pool are gated upstream by the `pool_registry` init constraint.
+- **Anchor 0.32-compatible lazy vault init** for `lp_holder_pool_vault`:
+  replaced `init_if_needed` on `SystemAccount` with manual
+  `system_program::create_account` CPI when the PDA has zero lamports.
+  First salvage of a pool creates the 0-data system-owned PDA; subsequent
+  salvages of the same pool are gated upstream by the `pool_registry`
+  init constraint.
...
-- **`lp_holder_pool_vault`** in both `salvage_pool` and `claim_lp_proceeds`
-  migrated from `UncheckedAccount<'info>` to `SystemAccount<'info>`. In
-  `salvage_pool` the constraint adds `init_if_needed` + `space = 0`. The
+- **`lp_holder_pool_vault`** in `claim_lp_proceeds` migrated to
+  `SystemAccount<'info>` for stricter typing. In `salvage_pool`, lazy PDA
+  initialization is handled via manual CPI account creation (no
+  `init_if_needed` on `SystemAccount` under Anchor 0.32). The
   account remains charter-invariant unsweepable; only `claim_lp_proceeds`
   may debit it (against a valid Merkle proof, m6+).

Also applies to: 56-57

🤖 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 34 - 37, Update the CHANGELOG entry that claims an
`init_if_needed` Anchor feature on `SystemAccount` (mentioned around the
`programs/grave-vault/Cargo.toml` note and on lines referencing
`init_if_needed`) to reflect the actual implementation: remove or correct the
assertion that Anchor provided `init_if_needed` for `SystemAccount` and instead
state that the migration uses a manual lazy-creation CPI (post-Anchor 0.32
rejection) to create the 0-data system-owned PDA for `lp_holder_pool_vault`, and
ensure the note still mentions that subsequent salvages are gated by the
`pool_registry` init constraint; apply the same correction to the other
occurrence that currently references `init_if_needed`.


#### Changed

- **`salvage_pool` pre-flight gates wired** in `programs/grave-vault/src/instructions/salvage_pool.rs`:
- Pause check (`ProtocolPaused`).
- **Cert freshness** via `EligibilityCert::is_expired(now)` (`EligibilityCertExpired`).
- **Cert criteria bitmap** must equal `0x3F` (all six derelict-pool
criteria validated at Phase 2) (`InvalidEligibilityCert`).
- **Cert pool / AMM binding** — `cert.amm_program_id == params.amm_program_id`
AND `cert.pool_address == params.pool_address` (`InvalidEligibilityCert`).
- Pool account address consistency (`PreflightFailed`).
- **`eligibility_cert` account** in `salvage_pool` migrated from
`UncheckedAccount<'info>` to `Account<'info, EligibilityCert>`. Anchor
now handles the 8-byte discriminator check and owner-program (`grave_scanner::ID`)
validation automatically; the previous manual ownership require! is
redundant and removed.
- **`lp_holder_pool_vault`** in both `salvage_pool` and `claim_lp_proceeds`
migrated from `UncheckedAccount<'info>` to `SystemAccount<'info>`. In
`salvage_pool` the constraint adds `init_if_needed` + `space = 0`. The
account remains charter-invariant unsweepable; only `claim_lp_proceeds`
may debit it (against a valid Merkle proof, m6+).
- **`evaluate_pool_phase_2`** now reads `cfg.cert_ttl_seconds` from
ProtocolConfig instead of the hardcoded const when stamping
`cert.expires_at`.

#### Honest stubs (audit-pending, unchanged from v1.0.5)

- AMM `remove_liquidity` CPI for Raydium V4: wired in v1.0.5; not yet
integration-tested against a seeded localnet pool (OpenBook seed harness
is a v1.1 deliverable).
- AMM adapters for Raydium CLMM, Orca Whirlpool, PumpSwap: revert
`AmmAdapterUnimplemented`.
- Locker release adapters (UNCX / PinkSale / Team Finance): revert
`LockerAdapterUnimplemented`.
- Jupiter v6 swap CPI: not yet wired; m6 deliverable.
- 40/40/20 distribution math: not yet wired; m7 deliverable.
`SalvageReceipt` distribution fields are zeroed at init.
- LP-holder Merkle proof verification in `claim_lp_proceeds`: returns
`InvalidClaimProof` until m6 wires the SHA-256 sorted-pair verification.

#### Verification status

- `cargo check`: not yet run in this sandbox — pending Seth's
ship-now-vs-verify-first call. v1.0.7 will be the post-verification
patch with any compile fixes named in the CHANGELOG.

Comment on lines +80 to +83

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 | 🟡 Minor | ⚡ Quick win

Update verification status to reflect this PR’s stated check results.

Line 80 says cargo check is pending, but the PR notes claim local fmt/clippy/tests/build and BPF compile already passed. This mismatch can mislead release readiness tracking.

🤖 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 80 - 83, Update the verification status paragraph
that currently claims "`cargo check`: not yet run" to reflect the actual PR
verification results (local fmt/clippy/tests/build and BPF compile passed) by
editing the CHANGELOG entry text so it accurately states which checks have
passed and that cargo check was performed (or remove the "pending" note), and
ensure the phrasing matches the PR's reported checks and version v1.0.7 mention.

#### Pre-mainnet checklist

- Replace placeholder program IDs in both crates' `declare_id!` and
`Anchor.toml` with real keypairs via `anchor keys list && anchor keys sync`.
- Re-deploy ProtocolConfig PDAs on devnet — adding `cert_ttl_seconds`
changes `INIT_SPACE` and existing config accounts will fail `realloc`
unless rotated through a fresh `initialize`. (Pre-mainnet: no live
config exists, so this is a no-op for the canonical deploy path.)
35 changes: 29 additions & 6 deletions programs/grave-scanner/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
// GraveScanner constants — locked thresholds and PDA seeds.
// Do not change without updating docs/architecture/eligibility-anchors.md.

// =====================================================================
// =================================================================
// Eligibility thresholds (Charter-locked at launch, governance-tunable
// within ranges enforced by `update_protocol_config`).
// =====================================================================
// =================================================================

/// Minimum trading inactivity to consider a pool derelict (Criterion 1).
/// 90 days, expressed in seconds.
Expand Down Expand Up @@ -38,12 +38,35 @@ pub const DEFAULT_LP_BURN_DUST_THRESHOLD: u64 = 1_000;
/// `sweep_stale_anchor` to reclaim rent.
pub const DEFAULT_ANCHOR_STALENESS_SECONDS: u64 = 14 * 24 * 60 * 60;

/// EligibilityCert TTL — 1 hour, expressed in seconds.
pub const ELIGIBILITY_CERT_TTL_SECONDS: i64 = 60 * 60;
/// Default `EligibilityCert` TTL — 1 hour, expressed in seconds. Governance
/// can lower or raise this via `update_protocol_config` but never below
/// `MIN_CERT_TTL_SECONDS`.
///
/// Used as the default value when `initialize` is called with
/// `cert_ttl_seconds = 0`. Live values live in `ProtocolConfig.cert_ttl_seconds`
/// (see `state/protocol_config.rs`).
pub const DEFAULT_CERT_TTL_SECONDS: i64 = 60 * 60;

/// Hardcoded floor on `cert_ttl_seconds`. Governance cannot configure a
/// cert TTL shorter than this, even by accident — `update_protocol_config`
/// rejects any value below this with `CertTtlBelowMinimum`.
///
/// Rationale: a cert TTL below 10 minutes makes the certify-and-salvage
/// bundle helper brittle against ordinary mempool latency. Raising this
/// floor requires a program upgrade, not a config update.
pub const MIN_CERT_TTL_SECONDS: i64 = 600;

/// `EligibilityCert` TTL constant retained for backwards-compatible imports
/// (e.g., older test fixtures). Prefer `ProtocolConfig.cert_ttl_seconds`
/// at runtime; this alias mirrors `DEFAULT_CERT_TTL_SECONDS`.
#[deprecated(
note = "Use ProtocolConfig.cert_ttl_seconds at runtime, or DEFAULT_CERT_TTL_SECONDS for defaults."
)]
pub const ELIGIBILITY_CERT_TTL_SECONDS: i64 = DEFAULT_CERT_TTL_SECONDS;

// =====================================================================
// =================================================================
// PDA seeds.
// =====================================================================
// =================================================================

pub const PROTOCOL_CONFIG_SEED: &[u8] = b"protocol_config";
pub const ELIGIBILITY_ANCHOR_SEED: &[u8] = b"eligibility_anchor";
Expand Down
8 changes: 7 additions & 1 deletion programs/grave-scanner/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
//
// Anchor's `#[error_code]` macro adds a default offset of 6000 to each
// variant's Rust discriminant. To produce the canonical spec codes
// 6000..=6018, the discriminants below are 0..=18 (with the 12..=14 gap
// 6000..=6019, the discriminants below are 0..=19 (with the 12..=14 gap
// preserved for future v4.x additions).
//
// Do not renumber existing variants. New variants append at the next
Expand Down Expand Up @@ -90,4 +90,10 @@ pub enum GraveScannerError {
/// staleness window elapsed.
#[msg("AnchorNotStale: staleness window has not yet elapsed.")]
AnchorNotStale = 18,

/// On-chain code 6019. `update_protocol_config` rejected a
/// `cert_ttl_seconds` value below the hardcoded `MIN_CERT_TTL_SECONDS`
/// floor (600s = 10 min). Raising the floor requires a program upgrade.
#[msg("CertTtlBelowMinimum: cert_ttl_seconds below MIN_CERT_TTL_SECONDS floor.")]
CertTtlBelowMinimum = 19,
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
// SPDX-License-Identifier: Apache-2.0
//
// Phase 2 of evaluate_pool. Re-verifies all six criteria after the
// multi-epoch confirmation gap and issues an `EligibilityCert` (TTL = 1
// hour). GraveVault consumes the cert to authorise `salvage_pool`.
// multi-epoch confirmation gap and issues an `EligibilityCert` (TTL =
// `ProtocolConfig.cert_ttl_seconds`, governance-configurable, default 1h,
// floored at MIN_CERT_TTL_SECONDS=600s). GraveVault consumes the cert to
// authorise `salvage_pool`.
//
// Phase 2 also enforces that the bitmap matches the originating
// EligibilityAnchor — a Phase 1 pass cannot be downgraded silently.

use anchor_lang::prelude::*;

use crate::adapters::{self, PoolData};
use crate::constants::{
ELIGIBILITY_ANCHOR_SEED, ELIGIBILITY_CERT_SEED, ELIGIBILITY_CERT_TTL_SECONDS, LAUNCH_PRICE_SEED,
};
use crate::constants::{ELIGIBILITY_ANCHOR_SEED, ELIGIBILITY_CERT_SEED, LAUNCH_PRICE_SEED};
use crate::criteria::{self, CriteriaInputs, CriteriaThresholds, Phase};
use crate::errors::GraveScannerError;
use crate::state::{EligibilityAnchor, EligibilityCert, LaunchPrice, ProtocolConfig};
Expand Down Expand Up @@ -131,9 +131,12 @@ pub fn handler(ctx: Context<EvaluatePoolPhase2>, params: EvaluatePoolPhase2Param
cert.anchor_epoch = anchor_account.first_eligible_epoch;
cert.cert_epoch = clock.epoch;
cert.issued_at = clock.unix_timestamp;
// TTL is governance-configurable per ProtocolConfig (with a hardcoded
// floor enforced in `update_protocol_config`). Reading here keeps cert
// freshness in lockstep with the live config.
cert.expires_at = clock
.unix_timestamp
.checked_add(ELIGIBILITY_CERT_TTL_SECONDS)
.checked_add(cfg.cert_ttl_seconds)
.ok_or(GraveScannerError::MathOverflow)?;
cert.criteria_bitmap = bitmap;
cert.bump = ctx.bumps.eligibility_cert;
Expand Down
16 changes: 16 additions & 0 deletions programs/grave-scanner/src/instructions/initialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use anchor_lang::prelude::*;

use crate::constants::*;
use crate::errors::GraveScannerError;
use crate::state::ProtocolConfig;

#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
Expand All @@ -14,6 +15,10 @@ pub struct InitializeParams {
pub min_tvl_lamports: u64,
pub anchor_staleness_seconds: u64,
pub lp_burn_dust_threshold: u64,
/// EligibilityCert TTL in seconds. 0 = default to `DEFAULT_CERT_TTL_SECONDS`
/// (3600). Values below `MIN_CERT_TTL_SECONDS` (600) are rejected with
/// `CertTtlBelowMinimum`.
pub cert_ttl_seconds: i64,
}

#[derive(Accounts)]
Expand Down Expand Up @@ -66,6 +71,17 @@ pub fn handler(ctx: Context<Initialize>, params: InitializeParams) -> Result<()>
params.lp_burn_dust_threshold
};

let cert_ttl = if params.cert_ttl_seconds == 0 {
DEFAULT_CERT_TTL_SECONDS
} else {
params.cert_ttl_seconds
};
require!(
cert_ttl >= MIN_CERT_TTL_SECONDS,
GraveScannerError::CertTtlBelowMinimum
);
cfg.cert_ttl_seconds = cert_ttl;

cfg.paused = false;
cfg.bump = ctx.bumps.protocol_config;
cfg._reserved = [0u8; 64];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

use anchor_lang::prelude::*;

use crate::constants::MIN_CERT_TTL_SECONDS;
use crate::errors::GraveScannerError;
use crate::state::ProtocolConfig;

Expand All @@ -14,6 +15,9 @@ pub struct UpdateProtocolConfigParams {
pub price_collapse_bps: Option<u16>,
pub min_tvl_lamports: Option<u64>,
pub anchor_staleness_seconds: Option<u64>,
/// EligibilityCert TTL in seconds. Floor: `MIN_CERT_TTL_SECONDS` (600).
/// Any value below the floor reverts with `CertTtlBelowMinimum`.
pub cert_ttl_seconds: Option<i64>,
}

#[derive(Accounts)]
Expand Down Expand Up @@ -48,6 +52,15 @@ pub fn handler(
if let Some(v) = params.anchor_staleness_seconds {
cfg.anchor_staleness_seconds = v;
}
if let Some(v) = params.cert_ttl_seconds {
// Hardcoded floor: governance cannot push cert_ttl below 600s.
// Raising the floor requires a program upgrade.
require!(
v >= MIN_CERT_TTL_SECONDS,
GraveScannerError::CertTtlBelowMinimum
);
cfg.cert_ttl_seconds = v;
}

Ok(())
}
8 changes: 8 additions & 0 deletions programs/grave-scanner/src/state/protocol_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ pub struct ProtocolConfig {
/// with `lp_supply <= lp_burn_dust_threshold` are treated as burned.
pub lp_burn_dust_threshold: u64,

/// `EligibilityCert` TTL in seconds. Governance-configurable via
/// `update_protocol_config`, but bounded below by
/// `MIN_CERT_TTL_SECONDS` (600s) — `update_protocol_config` rejects
/// any value below the floor with `CertTtlBelowMinimum`.
///
/// Read by `evaluate_pool_phase_2` when stamping `cert.expires_at`.
pub cert_ttl_seconds: i64,

/// Emergency pause flag. When `true`, `evaluate_pool_*` reverts with
/// `ProtocolPaused`. Has no effect on rent reclaim (`sweep_stale_anchor`)
/// or governance instructions. Per spec: GraveVault's
Expand Down
4 changes: 2 additions & 2 deletions programs/grave-vault/src/instructions/claim_lp_proceeds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,14 @@ pub struct ClaimLpProceeds<'info> {
)]
pub claim_record: Account<'info, ClaimRecord>,

/// CHECK: same `lp_holder_pool_vault` written to by salvage_pool.
/// Same `lp_holder_pool_vault` written to by salvage_pool.
/// Charter-invariant: only `claim_lp_proceeds` may debit this account.
#[account(
mut,
seeds = [LP_HOLDER_POOL_SEED, params.pool_address.as_ref()],
bump,
)]
pub lp_holder_pool_vault: UncheckedAccount<'info>,
pub lp_holder_pool_vault: SystemAccount<'info>,

#[account(mut)]
pub lp_holder: Signer<'info>,
Expand Down
Loading
Loading