Skip to content
Closed
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.

#### 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.

#### 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.)
33 changes: 27 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,33 @@ 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 @@ -127,9 +127,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
11 changes: 10 additions & 1 deletion programs/grave-vault/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ idl-build = [
anchor-debug = []

[dependencies]
anchor-lang = { workspace = true }
# `init-if-needed` is required for the lp_holder_pool_vault SystemAccount
# in `salvage_pool` — first salvage of a pool creates the system-owned
# 0-data PDA; subsequent salvages of the same pool are gated upstream by
# the `pool_registry` init constraint, so init_if_needed only matters on
# the first call. The Anchor footgun (init_if_needed silently accepting
# an unexpected pre-existing account with mismatched layout) does not
# apply here because SystemAccount<'info> + space=0 + system-owner is a
# total enumeration — any pre-existing account that doesn't match those
# bounds fails the type check.
anchor-lang = { workspace = true, features = ["init-if-needed"] }
anchor-spl = { workspace = true }
grave-scanner = { path = "../grave-scanner", features = ["cpi"] }
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