Skip to content

feat(m6): claim_lp_proceeds Merkle verification + SOL transfer#20

Open
graveyieldprotocol wants to merge 5 commits into
mainfrom
feat/m6-grave-vault-claim-merkle
Open

feat(m6): claim_lp_proceeds Merkle verification + SOL transfer#20
graveyieldprotocol wants to merge 5 commits into
mainfrom
feat/m6-grave-vault-claim-merkle

Conversation

@graveyieldprotocol

Copy link
Copy Markdown
Collaborator

Summary

Wires the second half of GraveVault's settlement flow. claim_lp_proceeds now actually executes a claim instead of stubbing the proof + transfer:

  1. Verify Merkle proof of (lp_holder, lp_balance_at_snapshot) against pool_registry.lp_snapshot_merkle_root (sealed by salvage_pool at salvage time)
  2. Defensive reject: lp_balance_at_snapshot > 0 AND lp_total_supply_at_snapshot > 0
  3. Compute pro-rata via u128 intermediate; conservation check claimed + amount <= total
  4. SOL transfer from lp_holder_pool_vault to lp_holder via system_program::transfer (PDA-signs with its own seeds via invoke_signed)
  5. Init ClaimRecord PDA (canonical double-claim defense — second claim by same (pool, holder) pair fails at the init constraint before lamports move)
  6. Emit LpClaimProcessed

PR scope (Option B, code-path split)

This PR is the claim_lp_proceeds side. The matching salvage_pool execution path (Raydium V4 + Jupiter + 40/40/20 distribution) is in PR #19 (m5). The two PRs have no file overlap:

  • m5 touches: cpi/ (new), instructions/salvage_pool.rs, errors.rs, constants.rs
  • m6 touches: merkle.rs (new), instructions/claim_lp_proceeds.rs
  • Both touch lib.rs and CHANGELOG.md in non-overlapping sections (m5 adds pub mod cpi;, m6 adds pub mod merkle;)

Whichever PR lands second will need a clean rebase; Git's three-way merge handles it.

What's in this PR

New fileprograms/grave-vault/src/merkle.rs:

  • compute_leaf(holder, balance) -> [u8; 32] = sha256(pubkey || balance_le_u64)
  • verify_proof(root, leaf, proof) -> bool = sorted-pair walk (OZ / Uniswap convention)
  • 7 host unit tests: deterministic leaf, distinct leaf on balance/pubkey change, two-leaf tree, four-leaf balanced tree, sorted-pair order invariance, empty-proof-as-leaf edge case, tampered leaf rejection

Modifiedprograms/grave-vault/src/instructions/claim_lp_proceeds.rs:

  • Replaces m3's require!(!params.merkle_proof.is_empty(), …) stub with a real merkle::verify_proof(...) call against pool_registry.lp_snapshot_merkle_root
  • Replaces m3's TODO(GraveVault m6): SOL transfer comment with a real system_program::transfer CPI signed by lp_holder_pool_vault's own seeds via invoke_signed
  • Adds defensive checks for zero balance + zero total supply
  • Preserves m3's pro-rata math, conservation check, ClaimRecord init, and event emission

Modifiedprograms/grave-vault/src/lib.rs:

  • + pub mod merkle;

ModifiedCHANGELOG.md:

  • m6 entry with added / changed / sync-convention / unverified breakdown

Error codes

No new error codes. InvalidClaimProof (7010) and ClaimAlreadyProcessed (7011) — both already on main — cover the m6 surface. docs/error_codes.md unchanged.

Test plan

  • cargo fmt --check green
  • cargo clippy --all-targets -- -D warnings green
  • cargo test --lib --package grave-vault merkle — 7 unit tests pass
  • anchor build green
  • terminology-lint green (pre-checked locally: PASS)
  • CodeRabbit review (mandatory pre-merge)

Unverified (transparent)

  • End-to-end localnet smoke test: salvage a Raydium V4 SOL/X pool via m5, then claim from multiple holders against the sealed root. Blocked on m5 (PR feat(m5): GraveVault salvage_pool execution path #19) landing first — this is the next step after both PRs merge.
  • Real off-chain GraveScanner v2 indexer integration. The leaf encoding (sha256(pubkey || balance_le_u64)) is documented in merkle.rs — the off-chain builder MUST match it byte-for-byte. Indexer work is m9 (separate workstream).
  • BPF compile via anchor build — deferred to CI per the validated parallel-track cadence.

Honest disclaimers

Same ship-now-verify-after cadence as m5. Anything CodeRabbit or CI flags will be addressed as fix-up commits on this PR. The CHANGELOG enumerates what's not yet proven.

🤖 Generated with Claude Code

Wires the second half of the salvage settlement flow: original LP
holders prove inclusion in the snapshot recorded by salvage_pool and
withdraw their pro-rata share from lp_holder_pool_vault.

New `merkle.rs` module implements SHA-256 sorted-pair verification
(OpenZeppelin / Uniswap convention) with 7 host unit tests covering
single-leaf, two-leaf, four-leaf trees, sort invariance, empty proofs,
and tampering.

claim_lp_proceeds handler replaces m3's two TODOs:
- Merkle proof check (was `!proof.is_empty()` stub)
- SOL transfer from lp_holder_pool_vault to lp_holder (was TODO comment)

PR scope per Option B (2 PRs by code path): this is the
claim_lp_proceeds side. The matching salvage_pool execution path
(Raydium V4 + Jupiter + 40/40/20 distribution) is PR #19. The two PRs
have NO file overlap — m5 touches instructions/salvage_pool.rs and
cpi/, m6 touches instructions/claim_lp_proceeds.rs and merkle.rs. Both
modify lib.rs in different sections so the rebase-at-merge is clean.

No new error codes — InvalidClaimProof (7010) and ClaimAlreadyProcessed
(7011) already cover the m6 surface.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 16, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@graveyieldprotocol has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 52 minutes and 51 seconds before requesting another review.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 96a0d7f0-9f26-4685-b008-86be6dd762ea

📥 Commits

Reviewing files that changed from the base of the PR and between 704ca9c and 342d8cb.

📒 Files selected for processing (6)
  • .github/workflows/ci.yml
  • CHANGELOG.md
  • programs/grave-vault/Cargo.toml
  • programs/grave-vault/src/instructions/claim_lp_proceeds.rs
  • programs/grave-vault/src/lib.rs
  • programs/grave-vault/src/merkle.rs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/m6-grave-vault-claim-merkle

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Two locally-reproduced fixes (cargo clippy --all-targets -- -D warnings
goes from 1 error to 0; 7/7 merkle.rs unit tests pass):

1. `Cargo.toml`: add `solana-sha256-hasher = "2"` dep. Anchor 0.32
   dropped the `solana_program::hash` re-export — the curated
   re-export list only includes account_info, clock, msg, entrypoint,
   program_error, pubkey, system_program, system_instruction. SHA-256
   hashing lives in the standalone solana-sha256-hasher crate, which
   is already a transitive dep but needs to be declared to be imported.

2. `merkle.rs`: change `use anchor_lang::solana_program::hash::hash;`
   to `use solana_sha256_hasher::hash;` matching the new home.

All remaining changes are rustfmt drift (3 files) auto-applied via
`cargo fmt --all`. Verified clean locally:
  - cargo fmt --check ✓
  - cargo clippy --all-targets -- -D warnings ✓
  - cargo test --lib --package grave-vault: 8/8 tests pass
    (7 merkle::tests::* + 1 test_id from lib.rs scaffold)

Anchor build (BPF compile) deferred to CI.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
graveyieldprotocol added a commit that referenced this pull request May 19, 2026
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>
graveyieldprotocol and others added 3 commits May 19, 2026 19:02
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>
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant