fix(nft): ReconcileBurnedNft reclaims the mint and ExtraAccountMetaList rent (#182) - #183
Conversation
…st rent dcccrypto#102 stopped this leak for BurnPositionNft and EmergencyBurn, which both close the NFT mint and call close_extra_metas. ReconcileBurnedNft was added later (dcccrypto#138) with seven accounts and no slot for either, so every reconciled NFT abandoned 7,676,880 lamports (~0.0077 SOL): 2,707,440 for the 261-byte metas PDA and 4,969,440 for the 586-byte mint. The loss is unrecoverable, and earlier than it first appears. BurnPositionNft and EmergencyBurn are the only instructions that close either account, and both require the holder ATA to hold amount == 1 — already 0 after the out-of-band burn that Reconcile exists to handle. RepairExtraMetas only ever pays rent in. So both are dead from the burn onward, and Reconcile is the only instruction that can still legally touch them. Add extra_metas and the Token-2022 program as accounts 7 and 8, closing both to last_holder_ai — the same recipient the PDA rent already goes to, and the one both burn paths already pay these rents to. The accounts are REQUIRED, not optional. Reconcile is permissionless, irreversible and runs at most once, so an opt-in could be lost permanently to whoever called first: a stale client, a helpful third party, or a griefer paying one transaction fee. The ABI break costs nothing — tag 7 had no account list in instruction.rs, no row in the README table, and a search across this repo and both sibling trees finds no callers. Grants no new authority: close_extra_metas re-derives extra_metas from nft_mint, which is itself pinned to nft_state.nft_mint; the mint-close CPI hardcodes the Token-2022 program id; and last_holder_ai is pinned to the recorded last_holder. All pins precede the new code. Also documents tag 7's account list in instruction.rs and adds its missing row to the README instruction table, and corrects the handler's account doc, which described seven accounts and marked nft_mint read-only. Closes dcccrypto#182 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughChangesReconcileBurnedNft rent recovery
Estimated code review effort: 3 (Moderate) | ~25 minutes Merge Risk: 🔵 Low · up to Reconciliation now requires the mint and metadata accounts and returns their rent to the recorded holder. The identities and authorities are tightly constrained; the remaining merge-readiness risk is that failures after portfolio release rely on transaction atomicity, so rollback behavior should be explicitly confirmed. Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
Full details: Linked Issues checkExplanation The PR closes the NFT mint and ExtraAccountMetaList PDA and returns their rent to last_holder. However, it rejects seven-account callers, so it does not preserve the backward compatibility required by issue Full details: Docstring CoverageExplanation Docstring coverage is 46.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 15 functions across 3 files. (1 skipped: 1 unsupported.)
✨ Finishing Touches🧪 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 |
Correction: there IS a first-party caller of tag 7 —
|
The SDK companion is ready, and the safe merge order is SDK-firstFollowing up on my note that this should not merge alone. The companion is up, and checking the deployed handler settled the ordering — in the opposite direction to what I assumed. dcccrypto/percolator-sdk#378 mirrors this change ( Merge the SDK first, then this, then release. The reason it is safe in that order and not the other:
So the SDK change is forward-compatible and this one is not backward-compatible, which fixes the order. The npm publish is the real gate rather than either merge — the SDK's CI publishes only on a Nothing is urgent: seven repos depend on the SDK, all pinned by exact version or git SHA rather than a floating range, and none of them calls Reconcile at all. |
Closes #182.
Problem
#102stopped this leak forBurnPositionNftandEmergencyBurn, which both close the NFT mint and callclose_extra_metas.ReconcileBurnedNftwas added later (#138) with seven accounts and no slot for either, so every reconciled NFT abandoned 7,676,880 lamports (~0.0077 SOL) — 2,707,440 for the 261-byte metas PDA and 4,969,440 for the 586-byte mint.The loss is unrecoverable, and earlier than it first looks.
BurnPositionNftandEmergencyBurnare the only instructions that close either account, and both require the holder ATA to holdamount == 1— already 0 after the out-of-band burn that Reconcile exists to handle.RepairExtraMetasonly ever pays rent in. So both are dead from the burn onward, and Reconcile is the only instruction that can still legally touch them, exercised at most once.Fix
extra_metasand the Token-2022 program become accounts 7 and 8, and both are closed tolast_holder_ai— the same recipient the PDA rent already goes to, and the one both burn paths already pay these rents to.They are required, not optional. I originally proposed optional trailing accounts to avoid an ABI break, and changed it: Reconcile is permissionless, irreversible and one-shot, so an opt-in could be lost permanently to whoever called first — a stale client, a helpful third party, or a griefer paying a single 5,000-lamport fee to destroy 7.68M of someone else's rent. The compatibility that would have bought protects an empty set: tag 7 had no account list in
instruction.rs, no row in the README instruction table, and a search across this repo and both sibling trees finds no callers.Verification
51 pre-existing tests unchanged, plus 5 new ones.
cargo clippy --all-targets -- -D warningsclean;cargo build-sbf --tools-version v1.52succeeds.The load-bearing test is
control_emergency_burn_reclaims_both_when_the_pda_is_alive— identical accounts with Reconcile simply not run, showingEmergencyBurncloses the metas PDA and returns its rent. That isolates "Reconcile omitted them" from "these accounts are inherently unclosable".the_short_seven_account_form_is_rejectedpins the required-accounts decision and asserts the short call destroys nothing on its way out.Honest limits of the suite: the mint close is a Token-2022 CPI, which no-ops under host
SyscallStubs, so its lamports do not move in tests. What is proven is the metas rent (moved in-program) landing on the recordedlast_holder, plus conservation on that address. The mint close rests on the identical pattern already exercised byBurnPositionNft.Security review
No new authority. Every pin precedes the new code:
close_extra_metasre-derivesextra_metasfromnft_mint, which is itself pinned tonft_state.nft_mint;token2022::close_accounthardcodes the Token-2022 program id, so account 8 cannot redirect the CPI;mint_authis the canonical PDA; andlast_holder_aiis pinned to the recordedlast_holder. Substituted metas, substituted mint, substituted mint authority, substituted token program and a substituted rent recipient were each attacked and each fails closed.Caveats, documented in the code
MintCloseAuthorityon the mint. Every mint this program creates has it, added beforeReconcileBurnedNftexisted, so all reconcilable mints carry it. A mint predating that would make the CPI — and therefore the whole reconcile, including the escrow release — revert. Worth knowing before any historical deploy is considered.Also included
instruction.rs— it was the only tag without one.nft_mintread-only while the code now requires it writable.Adjacent, not fixed here
The
+ 128slack inmint_space + metadata_tlv_size + 128at mint creation is dead weight — nothing ever reallocs the mint, so those bytes are funded and never used, over-funding every NFT by 890,880 lamports. It comes back to the holder on the normal burn paths, so it is a capital-lockup issue rather than a loss, but it is exactly the amount that made my original estimate 13% low. Worth its own issue.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Documentation