Skip to content

fix(nft): portfolio provenance + RepairExtraMetas recovery, with indentation/seed/tests fixed (supersedes #171) - #172

Merged
dcccrypto merged 3 commits into
mainfrom
fix/171-rebased
Aug 17, 2026
Merged

fix(nft): portfolio provenance + RepairExtraMetas recovery, with indentation/seed/tests fixed (supersedes #171)#172
dcccrypto merged 3 commits into
mainfrom
fix/171-rebased

Conversation

@dcccrypto

Copy link
Copy Markdown
Owner

Supersedes #171 by @Bayyan16. Their commit is preserved as-is; my corrections are a follow-up commit on top.

The three provenance checks are sound and break no live NFT — the wrapper's only production initializer sets portfolio_account_id = portfolio_ai.key (percolator-prog v16_program.rs:6558-6565), MintPositionNft at the deployed commit f18da24 already enforces the same equality (processor.rs:304-307), and all live devnet portfolios carry bytes[48..80] == their own address. The problems were in how it shipped.

1. Indentation. The new ownership gate and the entire resize/recovery block were written at column 0 inside the function body, so a security-critical account-ownership check read as though it were top-level. Re-indented to the surrounding block. (Only these regions — main does not pass cargo fmt --check and no workflow enforces it, so a repo-wide reformat would be unrelated churn.)

2. Hardcoded seed literal. The recovery path derived its signer seeds from b"extra-account-metas" while the mint path (:631) uses EXTRA_METAS_SEED, already imported at :28. Both now use the constant.

3. Zero tests. The PR changed four on-chain behaviours and shipped no tests; CI exercised none of the new lines. Added both arms of verify_portfolio_account_id plus a zeroed-provenance case. Negative control: stubbing the comparison to false fails two of the three.

4. Justified the System-owned recovery branch rather than leaving it unexplained. It is the never-created case: a nonexistent account is presented as System-owned with no data, so the old owner != program_id -> reject gate meant RepairExtraMetas could only fix a wrong-sized metas account, never a missing one — the case that actually bricks transfers. It is not the post-burn state: close_extra_metas (:747) zeroes lamports and data but never assigns back to System, and a burn drains nft_pda too, so a burned NFT cannot reach the checks. Safety rests on the address being pinned to our derivation, the account being required empty, and nft_pda being independently validated.

SEVERITY CORRECTION for the linked issues: #169/#170 are graded "High", but the precondition — a wrapper-owned, decodable portfolio whose provenance_header.portfolio_account_id differs from its own address — cannot be produced by percolator-prog. These checks are defence-in-depth against a future initializer, not fixes for a reachable exploit, and the issues should be re-graded before anyone merges on that basis.

cargo build clean; 47 lib tests pass (44 before, +3 new).

Bayyan16 and others added 2 commits August 16, 2026 11:10
…171

Follow-up to the provenance/repair commit. The three provenance checks are sound
and break no live NFT — the wrapper's only production initializer sets
portfolio_account_id = portfolio_ai.key, MintPositionNft at the deployed commit
f18da24 already enforces the same equality, and all live devnet portfolios carry
bytes[48..80] == their own address. The problems were in how it shipped.

1. Indentation. The new ownership gate and the entire resize/recovery block were
   written at column 0 inside the function body, so a security-critical
   account-ownership check read as though it were top-level. Re-indented to the
   surrounding block. (Only these regions — `main` does not pass
   `cargo fmt --check` and no workflow enforces it, so a repo-wide reformat would
   be unrelated churn.)

2. Hardcoded seed literal. The recovery path derived its signer seeds from
   b"extra-account-metas" while the mint path (:631) uses EXTRA_METAS_SEED, which
   is already imported at :28. Now both use the constant, so a seed change cannot
   desynchronise them.

3. Zero tests. The PR changed four on-chain behaviours and shipped no tests; CI
   exercised none of the new lines. Added the two arms of
   verify_portfolio_account_id plus a zeroed-provenance case (an all-zero header
   must not pass by accident). Negative control: stubbing the comparison to
   `false` fails two of the three.

4. Justified the System-owned recovery branch rather than leaving it unexplained.
   It is the never-created case: a nonexistent account is presented as
   System-owned with no data, so the old `owner != program_id -> reject` gate
   meant RepairExtraMetas could only fix a wrong-SIZED metas account, never a
   MISSING one — the case that actually bricks transfers. It is not the post-burn
   state: close_extra_metas (:747) zeroes lamports and data but never assigns back
   to System, and a burn drains nft_pda too, so a burned NFT cannot reach the
   checks. Safety rests on the address being pinned to our derivation, the account
   being required empty, and nft_pda being independently validated.

SEVERITY CORRECTION for the linked issues: #169/#170 are graded "High", but the
precondition — a wrapper-owned, decodable portfolio whose
provenance_header.portfolio_account_id differs from its own address — cannot be
produced by percolator-prog. These checks are defence-in-depth against a future
initializer, not fixes for a reachable exploit, and the issues should be re-graded
before anyone merges on that basis.

cargo build clean; 47 lib tests pass (44 before, +3 new).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown

Important

Review available on request

  • 🔍 Trigger review

Reviews should be triggered manually for repositories with fewer than 10 stars. Select Trigger review above or comment @coderabbitai review to review the latest changes. For a full review, comment @coderabbitai full review.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 26276f76-188c-48ec-a28d-a0abfba3a12a


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.

Second-pass review findings. Two are substantive.

1. UNDISCLOSED DESTRUCTIVE CAPABILITY. The resize guard was changed from
   `data.len() < EXTRA_METAS_ACCOUNT_LEN` (main) to `!=`, which also SHRANK an
   oversized program-owned extra_metas back to 261 bytes — through a
   PERMISSIONLESS instruction, and with no mention in the PR. Reverted to
   grow-only. The recovery case is unaffected: a never-created account has len 0,
   which is already `<` the target, so `<` covers it. This also removes the now
   -redundant needs_grow branch.

2. FALSE PREMISE COMMITTED TO ON-CHAIN SOURCE. The comment asserted "this is NOT
   the post-burn state: close_extra_metas zeroes lamports and data but never
   assigns the account back to the System Program". The runtime reaps
   zero-lamport accounts at end of transaction, so on any LATER transaction a
   burned NFT's extra_metas DOES load as System-owned and empty and DOES reach
   this branch. The branch is still safe — but via the next gate, not this one:
   the same burn drains nft_pda, so `nft_pda.owner != program_id` rejects before
   anything is written. Corrected, with the old claim flagged as wrong so a future
   reader does not re-derive it.

3. INDENTATION FIX WAS INCOMPLETE — the irony of a commit titled "repair the
   indentation". The PR's own added line at transfer_hook.rs:474 sat at 4-space
   inside an 8-space block, with a stray blank line splitting it from the
   statement it depends on. Both fixed.

cargo build clean; 47 lib tests pass, full suite green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dcccrypto
dcccrypto merged commit 86a282b into main Aug 17, 2026
2 checks passed
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.

2 participants