Skip to content

feat(standards): add flexible Burn Policies for Regulated Fungible Tokens#2664

Open
onurinanc wants to merge 10 commits into0xMiden:nextfrom
onurinanc:burn-policies
Open

feat(standards): add flexible Burn Policies for Regulated Fungible Tokens#2664
onurinanc wants to merge 10 commits into0xMiden:nextfrom
onurinanc:burn-policies

Conversation

@onurinanc
Copy link
Copy Markdown
Collaborator

Related Issue: #2638, OpenZeppelin/miden-confidential-contracts#88

We currently have MintPolicies in place (without having the MintPolicyManager as a separate account component), and in this PR we introduce BurnPolicies while preserving the same overall structure.

Follow-up PR: Similar to MintPolicyManager for the mint policies, we will include BurnPolicyManager in the follow-up PR when introducing role-based access control.

Copy link
Copy Markdown
Contributor

@PhilippGackstatter PhilippGackstatter left a comment

Choose a reason for hiding this comment

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

Looks good!

I have a few main comments:

  • I think the burn policy should not return the asset it takes as input, since it can't do anything useful with it, but correct me if I'm wrong.
  • There is a lot of duplication:
    • We should be able to deduplicate the greatest amount of Rust code, i.e. OwnerControlled and AuthControlled.
    • I didn't find a good opportunity to deduplicate the mint and burn MASM policy managers, but if there is one, we should probably do it.
  • May be worth double-checking that all procedures in the mint and burn policies modules that have Invocation: dynexec or Invocation: exec do not have pad comments, so we don't accidentally start overwriting these in the future (since the pad elements aren't actually garbage).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It looks like this is basically a copy of the mint policies OwnerControlled and similarly for AuthControlled, with the main difference being that it uses a different slot name? If there is no meaningful difference between the two, I wouldn't duplicate these but try to come up with a structure that accounts for the shared code. Maybe something like this:

// We should be able to make this type purely a private helper type now.
pub(super) struct OwnerControlled {
    initial_policy_root: Word,
}

pub struct MintOwnerControlled(OwnerControlled);
pub struct BurnOwnerControlled(OwnerControlled);

impl MintOwnerControlled {
    pub const NAME: &'static str =
        "miden::standards::components::mint_policies::owner_controlled";
}

impl BurnOwnerControlled {
    pub const NAME: &'static str =
        "miden::standards::components::burn_policies::owner_controlled";
}

impl From<MintOwnerControlled> for AccountComponent {
    fn from(mint_owner_controlled: MintOwnerControlled) -> Self {
        // The encoding logic lives in OwnerControlled, and we just pass different slot names
        // here.
        let storage_slots = mint_owner_controlled.0.to_storage_slots(
            MintOwnerControlled::allowed_policy_proc_roots_slot(),
            MintOwnerControlled::active_policy_proc_root_slot(),
        );

        todo!()
    }
}

impl From<BurnOwnerControlled> for AccountComponent {
    fn from(burn_owner_controlled: BurnOwnerControlled) -> Self {
        let storage_slots = mint_owner_controlled.0.to_storage_slots(
            BurnOwnerControlled::allowed_policy_proc_roots_slot(),
            BurnOwnerControlled::active_policy_proc_root_slot(),
        );

        todo!()
    }
}

This is just meant to illustrate the overall structure. Iiuc, the same applies to AuthControlled.

For the *PolicyAuthority, it might be fine to leave this duplicated since it's fairly little code.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yes, but I do think that this deduplication makes the things more complicated. I'm not sure this is the ideal way to do it, I've tried to deduplicate but it doesn't seem good. I would prefer to address this again after we add RoleBasedAccessControlled for mint and burn policies. What do you think?

Comment on lines +9 to +11
pub proc allow_all
# Dummy predicate, no checks yet.
push.0 drop
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: "No checks yet" sounds like we'll add some meaningful checks in the future, but this policy allows everything, so is this accurate? If we change it here, let's also change it in the mint policy one.

Comment on lines +8 to +15
#! Inputs: [ASSET_KEY, ASSET_VALUE, pad(16)]
#! Outputs: [ASSET_KEY, ASSET_VALUE, pad(16)]
#!
#! Panics if:
#! - note sender is not owner.
#!
#! Invocation: dynexec
pub proc owner_only
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I didn't notice this in the last PR, but since this procedure is exec-uted, we should remove the pad comments because there will actually be meaningful values on the stack that we shouldn't drop.

Applies to all burn and mint policies.

# => [ASSET_KEY, ASSET_VALUE, pad(16)]

dupw loc_storew_le.BURN_POLICY_ASSET_KEY_PTR dropw
dupw.1 loc_storew_le.BURN_POLICY_ASSET_VALUE_PTR dropw
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Question: Why is the burn policy expected to return the asset if we require it to be identical? This means we could pass a copy of the asset to the policy instead and have it not return anything.

Also, loading the asset from the active note works, but incurs a lot of extra work (piping data from the advice provider through memory), and it would be easy to pass the asset instead, since this procedure is called from a trusted context.

So, ideally we can have this procedure be:

#! Executes active burn policy for the provided asset by dynamic execution.
#!
#! Inputs:  [ASSET_KEY, ASSET_VALUE]
#! Outputs: []
#!
#! Panics if:
#! - burn policy root is invalid.
#! - active burn policy validation fails.
#! - active burn policy mutates the asset key or value.
#!
#! Invocation: exec
@locals(4)
pub proc execute_burn_policy
    exec.get_burn_policy_root
    # => [BURN_POLICY_ROOT, ASSET_KEY, ASSET_VALUE]

    exec.assert_existing_policy_root
    # => [BURN_POLICY_ROOT, ASSET_KEY, ASSET_VALUE]

    exec.assert_allowed_policy_root
    # => [BURN_POLICY_ROOT, ASSET_KEY, ASSET_VALUE]

    loc_storew_le.BURN_POLICY_PROC_ROOT_PTR dropw
    # => [ASSET_KEY, ASSET_VALUE]

    locaddr.BURN_POLICY_PROC_ROOT_PTR
    # => [policy_root_ptr, ASSET_KEY, ASSET_VALUE]

    dynexec
    # => []
end

Note also that I've removed the pad comments, because this procedure is executed and should not drop any additional elements from the stack. We have 3-4 more procedures in this module and in the mint policy manager that have Invocation: exec but have pad comments. To avoid future mistakes due to these pad comments, it'd be good to remove these.

@onurinanc
Copy link
Copy Markdown
Collaborator Author

onurinanc commented Mar 27, 2026

Hi @PhilippGackstatter, could you review this when you have a chance?

I’m wondering whether the deduplication related to the comment: #2664 (comment) is actually better now, or if it ended up making things more complex. Right now, it feels like the storage slots don’t have a clear structure anymore.

Copy link
Copy Markdown
Contributor

@PhilippGackstatter PhilippGackstatter left a comment

Choose a reason for hiding this comment

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

Looks good to me!

I agree the deduplication brought only minor benefits, but maybe this will change again with role-based access control? In any case, I left a few small comments, feel free to apply what you think makes sense.

Self(AuthControlled { initial_policy_root })
}

/// Creates a new [`BurnAuthControlled`] component with `allow_all` policy as default.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// Creates a new [`BurnAuthControlled`] component with `allow_all` policy as default.
/// Creates a new [`BurnAuthControlled`] component with `allow_all` policy.

}

impl OwnerControlled {
pub(crate) fn mint_initial_storage_slots(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I agree the deduplication didn't bring many benefits and since this and mint_initial_storage_slots do not share any logic, I don't think we need to have these here. I think we can move these as private helper functions to MintOwnerControlled and BurnOwnerControlled.

Comment on lines +17 to +24
impl AuthControlled {
/// Builds the three storage slots for an auth-controlled policy manager component.
///
/// `active_slot` / `allowed_slot` are storage **names**. `allow_all_procedure_root` is the
/// MAST root of the built-in `allow_all` procedure (map key when seeding `allowed_slot`), not
/// a slot.
pub(crate) fn initial_storage_slots(
&self,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If this is the only shared function, we can also consider making it a pub(crate) helper function that both use and remove the AuthControlled type. We can also leave it as is if we think we'll add more shared functionality in the future (as part of the role-based access control work, for example).

Comment on lines +5 to +8
pub use ::miden::standards::burn_policies::auth_controlled::allow_all
pub use ::miden::standards::burn_policies::owner_controlled::owner_only
pub use ::miden::standards::burn_policies::policy_manager::set_burn_policy
pub use ::miden::standards::burn_policies::policy_manager::get_burn_policy
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It looks odd that we export auth_controlled::allow_all from owner_controlled. I guess the reason is because we want to make allow_all available for use in OwnerControlled, so the owner can switch between allow_all or owner_only without having to change components? If so, would be good to add a comment here because this doesn't seem obvious.

I think the approach makes sense, but it made me realize that allow_all is generic and doesn't really have anything to do with "auth controlled". So I think we could export this instead as:

pub use ::miden::standards::burn_policies::allow_all

Which we should be able to do by defining that procedure in crates/miden-standards/asm/standards/burn_policies/mod.masm. And the auth controlled component would happen to use that.

The same probably applies to the mint policy one to keep things consistent, even if we don't re-export it from mint_policies::owner_controlled.

Comment on lines +29 to +36
static ACTIVE_BURN_POLICY_PROC_ROOT_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("miden::standards::burn_policy_manager::active_policy_proc_root")
.expect("storage slot name should be valid")
});
static ALLOWED_BURN_POLICY_PROC_ROOTS_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("miden::standards::burn_policy_manager::allowed_policy_proc_roots")
.expect("storage slot name should be valid")
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: These are re-defined in auth and owner controlled, could we define these just once as private helpers?

Comment on lines +145 to +157
/// Returns the storage slot schema for policy authority mode.
pub fn policy_authority_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
(
Self::policy_authority_slot().clone(),
StorageSlotSchema::value(
"Policy authority mode (AuthControlled = tx auth, OwnerControlled = external owner)",
[
FeltSchema::u8("policy_authority"),
FeltSchema::new_void(),
FeltSchema::new_void(),
FeltSchema::new_void(),
],
),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: If we move the storage slots to a shared location, it would also be nice to only define the schema once next to the slots.

Comment thread crates/miden-standards/src/account/burn_policies/owner_controlled.rs Outdated
fn create_set_burn_policy_note_script(policy_root: Word) -> String {
format!(
r#"
use miden::standards::burn_policies::policy_manager->policy_manager
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
use miden::standards::burn_policies::policy_manager->policy_manager
use miden::standards::burn_policies::policy_manager

Nit: This seems redundant

Comment on lines +163 to +164
repeat.12 push.0 end
push.{policy_root}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
repeat.12 push.0 end
push.{policy_root}
padw padw padw
push.{policy_root}

Nit

@PhilippGackstatter
Copy link
Copy Markdown
Contributor

One other scenario I was wondering about is adding a new policy component and use its exported policy. To use it, we'd also have to update the map of allowed procedure policy roots, but I think we don't have an API for this? This may be something to add in the future. If that's correct, would be great to open an issue to track that unless it will be covered in future PRs already. Thanks!

Copy link
Copy Markdown
Contributor

@PhilippGackstatter PhilippGackstatter left a comment

Choose a reason for hiding this comment

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

Looks good to me!

I'd remove the OwnerControlled type now that it doesn't have any functionality, and after that I think we can merge.

Comment on lines 43 to 46
#[derive(Debug, Clone, Copy)]
pub(crate) struct OwnerControlled {
pub(crate) initial_policy_root: Word,
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since we now ended up with only sharing the struct layout, but no functionality, I'd remove this type and inline the initial_policy_root: Word.

Comment on lines +244 to +245
.with_component(MintOwnerControlled::owner_only())
.with_component(BurnOwnerControlled::allow_all())
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Judging by this comment (#2724 (comment)), this may have to be BurnOwnerControlled::owner_only(). Maybe @mmagician can double check.

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