feat(stake,#171): PDA-signed RecoverFlushedInsurance — unstrand flushed insurance post-burn - #249
Conversation
…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>
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughAdds a permissionless ChangesRecoverFlushedInsurance permissionless recovery instruction
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
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):
pool.vault == vault.keychecked 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.recover_flushed_insurance_after_burn).amount <= total_flushed − total_returnedkeepsreturned <= 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
Tests