Skip to content

Conversation

@imetandy
Copy link

Key Changes

  • Implemented a close_tree instruction to close the merkle tree and reclaim the rent. We also handle closing the config account.
  • Ran scripts to generate clients for js and rust.

Potential Improvements

  • Backwards compatibility for spl-account-compression if this is something that needs to be supported, but I didn't feel it was relevant.
  • This will only run if the merkle tree is empty (i.e. all cNFTs are burned). For some use cases, implementing some cNFT expiry might make sense for burn delegation, only enabled after a certain time.

Motivation

  • Designing NFT use cases for short lived ownership or access control (i.e. betting slips, gig tickets) where it is likely that all cNFTs eventually become unused / worthless.

Testing

  • No tests currently implemented or ran, consider this an initial draft for discussion in MIP 50.

…+ rust SDK. Generated clients through Kinobi.
@vercel
Copy link

vercel bot commented Nov 17, 2025

@imetandy is attempting to deploy a commit to the Metaplex Foundation Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 17, 2025

Summary by CodeRabbit

Release Notes

  • New Features
    • Added CloseTree instruction enabling users to close empty merkle trees and reclaim associated rent through authorized accounts.

Walkthrough

A new CloseTree instruction is added to the Bubblegum program to close an empty Merkle tree and its config account PDA, reclaiming rent. The implementation includes account validation, schema version checks, authority verification, and a CPI call to mpl_account_compression.

Changes

Cohort / File(s) Change Summary
IDL Definition
idls/bubblegum.json
Added CloseTree instruction with accounts: treeAuthority, authority, merkleTree, recipient, compressionProgram, logWrapper, systemProgram. Added CloseTree variant to InstructionName enum.
Processor Implementation
programs/bubblegum/program/src/processor/close_tree.rs
New file with CloseTree accounts struct and close_tree function. Validates schema version (V2), authority (tree_creator or tree_delegate), recipient, performs CPI to close_empty_tree, and reclaims rent by closing tree config PDA.
Core Library
programs/bubblegum/program/src/lib.rs
Added CloseTree variant to InstructionName enum, mapped discriminator [9, 124, 164, 131, 238, 218, 148, 212] to CloseTree in get_instruction_type, added public close_tree RPC handler.
Module Export
programs/bubblegum/program/src/processor/mod.rs
Added private module declaration mod close_tree and public re-export pub(crate) use close_tree::*.

Sequence Diagram

sequenceDiagram
    participant User
    participant Bubblegum
    participant Processor as Processor: close_tree
    participant MplCompression as MplAccountCompression
    participant System
    
    User->>Bubblegum: close_tree instruction
    Bubblegum->>Processor: dispatch CloseTree context
    
    rect rgb(200, 220, 255)
        Note over Processor: Validations
        Processor->>Processor: Verify schema version = V2
        Processor->>Processor: Check authority is tree_creator or tree_delegate
        Processor->>Processor: Check recipient is tree_creator or tree_delegate
    end
    
    rect rgb(220, 255, 220)
        Note over Processor: Close empty tree via CPI
        Processor->>MplCompression: cpi::close_empty_tree<br/>(tree_authority as signer)
        MplCompression->>System: close merkle_tree account
    end
    
    rect rgb(255, 240, 220)
        Note over Processor: Reclaim rent
        Processor->>System: transfer TreeConfig lamports to recipient
        Processor->>System: close tree_authority PDA
    end
    
    Processor-->>User: Result<()>
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Authority and recipient validation logic: Two separate account ownership checks that must both succeed. Review the validation conditions to ensure they correctly identify tree_creator and tree_delegate scenarios.
  • CPI invocation to mpl_account_compression: Verify that the CloseTree context properly constructs the signer seeds and bump for the tree_authority PDA, and that the CPI call passes the correct parameters.
  • PDA closure and rent reclaim: Confirm the lamport transfer correctly reclaims rent from the tree config account and that the PDA is properly closed.
  • Schema version check: Validate the V2 schema version constraint is appropriate for this instruction.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title accurately describes the main change: implementing a close_tree instruction and exposing it in SDKs, which aligns with the primary modifications across IDL, lib.rs, processor, and generated clients.
Description check ✅ Passed The pull request description is directly related to the changeset, detailing the close_tree instruction implementation, rent reclamation, config account closure, and SDK generation, which match the code changes.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

📝 Customizable high-level summaries are now available in beta!

You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.


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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dccb28b and 6e37a98.

⛔ Files ignored due to path filters (4)
  • clients/js/src/generated/instructions/closeTree.ts is excluded by !**/generated/**
  • clients/js/src/generated/instructions/index.ts is excluded by !**/generated/**
  • clients/rust/src/generated/instructions/close_tree.rs is excluded by !**/generated/**
  • clients/rust/src/generated/instructions/mod.rs is excluded by !**/generated/**
📒 Files selected for processing (4)
  • idls/bubblegum.json (2 hunks)
  • programs/bubblegum/program/src/lib.rs (3 hunks)
  • programs/bubblegum/program/src/processor/close_tree.rs (1 hunks)
  • programs/bubblegum/program/src/processor/mod.rs (2 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: danenbm
Repo: metaplex-foundation/mpl-bubblegum PR: 134
File: clients/js/src/createTree.ts:85-88
Timestamp: 2025-05-07T21:05:10.245Z
Learning: For Bubblegum V2 trees, custom compression program IDs are not supported by design. The createTreeV2 function intentionally uses only the default mplAccountCompression program ID, unlike createTree which allows custom compression program IDs.
📚 Learning: 2025-05-29T09:26:11.104Z
Learnt from: danenbm
Repo: metaplex-foundation/mpl-bubblegum PR: 125
File: clients/rust/tests/setup/tree_manager.rs:102-110
Timestamp: 2025-05-29T09:26:11.104Z
Learning: In mpl-bubblegum Rust crate, mpl_account_compression::ID can be used without explicitly importing mpl_account_compression, likely because it's re-exported or available through other imports in the codebase.

Applied to files:

  • programs/bubblegum/program/src/processor/mod.rs
📚 Learning: 2025-05-07T21:05:10.245Z
Learnt from: danenbm
Repo: metaplex-foundation/mpl-bubblegum PR: 134
File: clients/js/src/createTree.ts:85-88
Timestamp: 2025-05-07T21:05:10.245Z
Learning: For Bubblegum V2 trees, custom compression program IDs are not supported by design. The createTreeV2 function intentionally uses only the default mplAccountCompression program ID, unlike createTree which allows custom compression program IDs.

Applied to files:

  • idls/bubblegum.json
  • programs/bubblegum/program/src/processor/close_tree.rs
🧬 Code graph analysis (3)
programs/bubblegum/program/src/processor/mod.rs (2)
programs/bubblegum/program/src/lib.rs (1)
  • close_tree (640-642)
programs/bubblegum/program/src/processor/close_tree.rs (1)
  • close_tree (31-77)
programs/bubblegum/program/src/processor/close_tree.rs (2)
clients/rust/src/generated/instructions/close_tree.rs (6)
  • merkle_tree (120-123)
  • merkle_tree (378-384)
  • authority (115-118)
  • authority (370-376)
  • recipient (127-130)
  • recipient (388-394)
programs/bubblegum/program/src/lib.rs (1)
  • close_tree (640-642)
programs/bubblegum/program/src/lib.rs (1)
programs/bubblegum/program/src/processor/close_tree.rs (1)
  • close_tree (31-77)
🔇 Additional comments (6)
idls/bubblegum.json (2)

3387-3437: IDL accounts for closeTree match the on-chain account struct

The account list (treeAuthority, authority, merkleTree, recipient, compressionProgram, logWrapper, systemProgram) and their mut/signer flags line up with CloseTree<'info> in Rust and the documented behavior (authority + creator/delegate recipient). I don’t see any schema issues here; the IDL looks consistent with the program implementation.


4133-4136: InstructionName::CloseTree enum variant is correctly added

Adding CloseTree at the end of the InstructionName enum keeps existing discriminants stable and aligns with the new instruction in the IDL and get_instruction_type wiring elsewhere.

programs/bubblegum/program/src/processor/mod.rs (1)

31-31: Module wiring for close_tree is correct

The new mod close_tree; declaration is paired with pub(crate) use close_tree::*;, making the CloseTree context and handler available to the crate without altering existing behavior.

programs/bubblegum/program/src/lib.rs (2)

22-60: Enum variant addition for CloseTree keeps InstructionName stable

Appending CloseTree to InstructionName preserves all existing variant ordinals and cleanly extends the internal instruction taxonomy to cover the new close-tree operation.


62-106: Ensure the CloseTree discriminator matches the generated Anchor ID

The new match arm mapping [9, 124, 164, 131, 238, 218, 148, 212] to InstructionName::CloseTree looks fine, but it’s critical that these bytes are exactly the Anchor-generated discriminator for the new close_tree instruction so tools and off-chain decoders stay in sync.

You can confirm this by regenerating the IDL/clients and comparing the discriminator, or by running a small script that computes the Anchor discriminator for global:close_tree and checking it equals [9, 124, 164, 131, 238, 218, 148, 212].

programs/bubblegum/program/src/processor/close_tree.rs (1)

31-77: close_tree logic correctly gates by version and authority and safely chains CPIs

The core flow looks solid:

  • Restricting to Version::V2 trees ensures this only applies to V2 configs (and, by design, V2 trees that were created with the canonical compression program).
  • Requiring the signer authority to be either tree_creator or tree_delegate, and separately requiring recipient to be one of those two, prevents a delegate from redirecting rent to arbitrary accounts while still allowing creator/delegate to choose where reclaimed lamports go.
  • Using the TreeConfig PDA as the signer for mpl_account_compression::cpi::close_empty_tree via seeds [merkle_tree.key().as_ref(), bump] matches the account derivation on the Bubblegum side and ensures only the correct authority PDA can close the Merkle tree.
  • The order “CPI to close_empty_tree, then close tree_authority” guarantees the config PDA is only closed after the underlying tree is successfully closed, so failures leave state intact.

The only thing I’d suggest double-checking is that these seeds exactly match the seeds used when creating TreeConfig in create_tree_v2, so the PDA signer derivation can never succeed for a mismatched (tree, config) pair.

You can verify the seed consistency by inspecting the TreeConfig PDA derivation in create_tree_v2 and ensuring it also uses [merkle_tree.key().as_ref()] as seeds with the same bump.

Comment on lines +639 to +642
/// Closes an empty tree and its config PDA to reclaim rent.
pub fn close_tree(ctx: Context<CloseTree>) -> Result<()> {
processor::close_tree(ctx)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

close_tree RPC wiring is correct; consider clarifying V2-only behavior in docs

The new close_tree(ctx: Context<CloseTree>) entrypoint correctly delegates to processor::close_tree and matches the IDL. Since the processor enforces Version::V2, you might call out “V2 trees only” explicitly in the doc comment to reduce surprises for integrators trying this on legacy trees.

🤖 Prompt for AI Agents
programs/bubblegum/program/src/lib.rs lines 639-642: the doc comment for
close_tree says "Closes an empty tree and its config PDA to reclaim rent." but
doesn't state this operation is V2-only; update the doc comment to explicitly
state that this RPC only works for Version::V2 trees (processor enforces
Version::V2) so integrators aren’t surprised when calling it on legacy trees,
and optionally add a short note about expected failure behavior for non-V2
trees.

Comment on lines +8 to +29
#[derive(Accounts)]
pub struct CloseTree<'info> {
#[account(
mut,
seeds = [merkle_tree.key().as_ref()],
bump,
)]
pub tree_authority: Account<'info, TreeConfig>,
/// Tree creator or delegate.
pub authority: Signer<'info>,
/// CHECK: This account is modified in the downstream program.
#[account(mut, owner = mpl_account_compression::ID)]
pub merkle_tree: UncheckedAccount<'info>,
/// Recipient for reclaimed lamports (tree + config PDA). Must be the creator
/// or the delegate.
/// CHECK: This account is validated in the instruction.
#[account(mut)]
pub recipient: UncheckedAccount<'info>,
pub compression_program: Program<'info, MplAccountCompression>,
pub log_wrapper: Program<'info, MplNoop>,
pub system_program: Program<'info, System>,
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Accounts definition for CloseTree is consistent and safe, with minor hardening opportunities

The CloseTree<'info> accounts look well-structured:

  • tree_authority as a PDA over merkle_tree.key().as_ref() with bump matches the usual TreeConfig derivation pattern.
  • merkle_tree is constrained to owner = mpl_account_compression::ID, ensuring only genuine compression trees can be targeted.
  • recipient is kept unchecked but is constrained later in the handler to be creator or delegate, which is appropriate for lamport reclamation.
  • compression_program, log_wrapper and system_program are correctly typed to the expected programs.

If you want to align fully with other Bubblegum instructions and tighten invariants further, you could also add explicit runtime checks that:

  • compression_program.key() == mpl_account_compression::ID (using the existing InvalidCompressionProgram error), and
  • log_wrapper matches the canonical noop program (using InvalidLogWrapper),

even though the types already strongly suggest the intended IDs.

Based on learnings

🤖 Prompt for AI Agents
In programs/bubblegum/program/src/processor/close_tree.rs around lines 8 to 29,
add explicit runtime checks in the instruction handler to validate that
compression_program.key() == mpl_account_compression::ID and log_wrapper.key()
== mpl_noop::ID (or the canonical noop program ID used elsewhere), returning the
existing InvalidCompressionProgram and InvalidLogWrapper errors respectively;
place these checks early in the handler before any CPI or downstream calls so
the instruction fails fast if the provided program IDs are incorrect.

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