Skip to content

feat(stake,#171): PDA-signed RecoverFlushedInsurance — unstrand flushed insurance post-burn - #249

Merged
dcccrypto merged 1 commit into
mainfrom
fix/171-recover-flushed-insurance
Jun 23, 2026
Merged

feat(stake,#171): PDA-signed RecoverFlushedInsurance — unstrand flushed insurance post-burn#249
dcccrypto merged 1 commit into
mainfrom
fix/171-recover-flushed-insurance

Conversation

@dcccrypto

@dcccrypto dcccrypto commented Jun 23, 2026

Copy link
Copy Markdown
Owner

Per your decision (PDA-signed recovery CPI). The secure-bind wind-down stranded flushed insurance: post-BurnAssetAdmin only the bound vault_auth PDA could withdraw it, but no stake instruction did a PDA-signed withdraw → recovery needed a non-obvious rotate-back that the burn blocks.

RecoverFlushedInsurance (tag 23, permissionless) invoke_signed's the vault_auth PDA to call the wrapper's WithdrawInsuranceAsset (tag 57, asset 0 — auth = insurance_operator = the PDA), pulling flushed insurance back into the stake vault; total_returned += amount.

Security (verified from multiple standpoints + e2e, re-run independently):

  • Drainpool.vault == vault.key checked before the CPI; recovered tokens can only land in the pool vault (non-pool dest → InvalidPda). Permissionless is safe since funds can't reach the caller.
  • Post-burn — tag 57 gates on insurance_operator (kind 2); BurnAssetAdmin (kind 0) never touches it → recovery works after the irreversible burn, no rotate-back (proven by recover_flushed_insurance_after_burn).
  • Conservation — cap amount <= total_flushed − total_returned keeps returned <= flushed.

build-sbf clean (both programs); 142 lib + 10 v17 cross-program e2e + 67 unit + 46 integration, 0 failures. NOTE: a prior attempt on a stale local clone was discarded (wrong tags); this is a clean fresh-main implementation, verified line-by-line. Closes #171.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added ability to recover flushed insurance assets back to the pool vault through a new permissionless instruction.
  • Tests

    • Added end-to-end tests validating insurance recovery behavior, including success and failure scenarios.

…ed insurance post-burn

The secure-bind wind-down (BindInsuranceAuthority tag 19 binds asset-0 insurance_authority
+ insurance_operator to the vault_auth PDA; BurnAssetAdmin tag 21 makes it irreversible)
stranded flushed insurance: only the bound PDA could withdraw it, but no stake instruction
did a PDA-signed withdraw CPI, so recovery required a non-obvious rotate-back that is BLOCKED
after the burn.

Adds RecoverFlushedInsurance (tag 23, permissionless): invoke_signed's the vault_auth PDA to
call the wrapper's WithdrawInsuranceAsset (tag 57, asset 0 — auth = insurance_operator = the
PDA, live mode, the symmetric inverse of FlushToInsurance), pulling flushed insurance back into
the STAKE VAULT. total_returned += amount after the CPI.

Security (verified from multiple standpoints + e2e):
- DRAIN: dest is checked `pool.vault == vault.key` BEFORE the CPI, so recovered tokens can only
  land in the pool's own vault; a non-pool dest is rejected (Custom InvalidPda). Permissionless
  is safe because funds can only reach pool.vault, not the caller.
- POST-BURN: tag 57 gates on insurance_operator (kind 2), which BurnAssetAdmin (kind 0) never
  touches — recovery works after the irreversible burn with no rotate-back (proven by
  recover_flushed_insurance_after_burn).
- CONSERVATION: cap amount <= total_flushed - total_returned keeps total_returned <= total_flushed;
  returned += amount with amount physically in the vault.

Verification: cargo build-sbf clean (stake + wrapper); 142 lib + 10 v17 cross-program e2e
(bind→flush→burn→recover + drain-reject + nothing-flushed-reject) + 67 unit + 46 integration,
0 failures (re-run independently). NOT deployed (cutover-gated). Closes #171.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dcccrypto
dcccrypto merged commit c3edb7f into main Jun 23, 2026
@dcccrypto
dcccrypto deleted the fix/171-recover-flushed-insurance branch June 23, 2026 23:31
@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f8e467e1-3a06-4c6c-a010-d3b3bf377991

📥 Commits

Reviewing files that changed from the base of the PR and between 8cb106a and 20f2fe6.

📒 Files selected for processing (4)
  • src/cpi.rs
  • src/instruction.rs
  • src/processor.rs
  • tests/v17_stake_insurance_e2e.rs

📝 Walkthrough

Walkthrough

Adds a permissionless RecoverFlushedInsurance instruction (stake tag 23) that lets anyone recover flushed insurance back into the pool vault after the secure-bind wind-down. It introduces a new CPI primitive cpi_withdraw_insurance_asset (wrapper tag 57, 19-byte wire format), a processor handler with multi-step validation and PDA-signed CPI, and end-to-end tests covering positive and negative scenarios.

Changes

RecoverFlushedInsurance permissionless recovery instruction

Layer / File(s) Summary
WithdrawInsuranceAsset CPI helper (tag 57)
src/cpi.rs
Defines TAG_WITHDRAW_INSURANCE_ASSET = 57, implements cpi_withdraw_insurance_asset encoding the 19-byte wire (tag + u16 asset_index=0 + u128 amount), builds the 6-account Instruction, and executes it via invoke_signed; adds a canary test asserting the exact wire shape and guarding against tag/width regressions.
RecoverFlushedInsurance instruction variant and deserialization (tag 23)
src/instruction.rs
Adds StakeInstruction::RecoverFlushedInsurance { amount: u64 } (tag 23) with account/payload documentation, extends unpack with an 8-byte little-endian u64 branch, and adds unit tests for round-trip, short-payload rejection, trailing-byte rejection, and truncated-payload matrix coverage.
Processor dispatch and handler
src/processor.rs
Wires tag 23 to process_recover_flushed_insurance, which validates SPL token program, pool ownership/version/mode (insurance pools only), non-zero amount bounded by total_flushed - total_returned, wrapper program match, drain-to-pool-vault check, and vault_auth PDA derivation; calls cpi_withdraw_insurance_asset signed as the PDA, then increments pool.total_returned.
E2E tests: positive recovery and negative guards
tests/v17_stake_insurance_e2e.rs
Adds recover_flushed_insurance_ix encoder and three e2e tests: a positive bind→flush→burn→recover flow asserting token conservation and accounting, wrong-destination rejection (InvalidPda), and nothing-flushed rejection (InsufficientVaultBalance).

Sequence Diagram(s)

sequenceDiagram
  actor PermissionlessCaller
  participant StakeProgram
  participant PoolState
  participant vault_auth PDA
  participant PercolatorProgram
  participant WrapperVault
  participant PoolVault

  PermissionlessCaller->>StakeProgram: RecoverFlushedInsurance { amount }
  StakeProgram->>PoolState: validate mode=0, amount <= total_flushed - total_returned
  StakeProgram->>StakeProgram: verify dest == pool.vault (drain check)
  StakeProgram->>StakeProgram: derive & verify vault_auth PDA
  StakeProgram->>PercolatorProgram: cpi_withdraw_insurance_asset (signed as vault_auth PDA)
  PercolatorProgram->>WrapperVault: debit amount
  PercolatorProgram->>PoolVault: credit amount
  PercolatorProgram-->>StakeProgram: Ok
  StakeProgram->>PoolState: total_returned += amount
  StakeProgram-->>PermissionlessCaller: Ok
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • dcccrypto/percolator-stake#98: Modifies process_flush_to_insurance and related accounting around pool.total_returned and flushed/returned balances in src/processor.rs, directly affecting the same insurance accounting fields this PR updates.

Poem

🐇 Hop hop, the insurance was stuck in a vault so deep,
After bind-and-burn, the flushed tokens fell asleep.
Now tag 57 wakes them, a PDA signs the way,
total_returned climbs higher, the LPs saved the day!
No rotate-back required — this rabbit fixed the trap. 🌟

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/171-recover-flushed-insurance

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant