Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
- Added `AccountComponent::has_procedure(root)` helper ([#2974](https://github.com/0xMiden/protocol/pull/2974)).
- Optimized protocol MASM stack-cleaning sequences, saving 1 cycle per occurrence across 9 single-element-extraction procedures ([#3041](https://github.com/0xMiden/protocol/pull/3041)).
- [BREAKING] Refactored `TokenPolicyManager` by adding `invoke_send_policy` / `invoke_receive_policy` wrappers (stored in the protocol reserved asset callback slots) that read the active policy root from the new `active_send_policy_proc_root` / `active_receive_policy_proc_root` storage slots ([#3047](https://github.com/0xMiden/protocol/pull/3047)).
- Added regression tests ensuring a `TokenPolicyManager` with only reserved send/receive policies installs the protocol reserved asset callback slots, so `has_callbacks` is correct from creation and minted assets carry the callback flag ([#3091](https://github.com/0xMiden/protocol/pull/3091)).
- Added a definition of the Miden operator on the architecture overview page and linked it from the note lifecycle ([#3017](https://github.com/0xMiden/protocol/pull/3017)).
- Clarified Miden's operational roles on the architecture overview page and linked them from the note lifecycle ([#3017](https://github.com/0xMiden/protocol/pull/3017)).
- [BREAKING] Replaced `AccountInterface::build_send_notes_script` with a standalone `SendNotesTransactionScript` built against `AccountCodeInterface` ([#3055](https://github.com/0xMiden/protocol/pull/3055)).
Expand Down
72 changes: 66 additions & 6 deletions crates/miden-standards/src/account/policies/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,12 +197,21 @@ struct PolicyConfig {
///
/// Construct via [`Self::builder`]. The builder requires the active mint and burn policy
/// ([`TokenPolicyManagerBuilder::active_mint_policy`] /
/// [`TokenPolicyManagerBuilder::active_burn_policy`]). Active send / receive policies
/// ([`TokenPolicyManagerBuilder::active_send_policy`] /
/// [`TokenPolicyManagerBuilder::active_receive_policy`]) are optional — when omitted, the
/// protocol-reserved asset-callback slots are not installed, so every minted asset carries
/// [`AssetCallbackFlag::Disabled`][miden_protocol::asset::AssetCallbackFlag::Disabled] and is
/// permanently exempt from any transfer policy installed later.
/// [`TokenPolicyManagerBuilder::active_burn_policy`]). Send / receive policies are optional and may
/// be registered as active ([`TokenPolicyManagerBuilder::active_send_policy`] /
/// [`TokenPolicyManagerBuilder::active_receive_policy`]) and/or as reserved alternatives
/// ([`TokenPolicyManagerBuilder::allowed_send_policy`] /
/// [`TokenPolicyManagerBuilder::allowed_receive_policy`]) for runtime switching. The
/// protocol-reserved asset-callback slots (see the storage layout below) are installed whenever at
/// least one send or receive policy of either kind is registered - active or reserved - so every
/// minted asset carries
/// [`AssetCallbackFlag::Enabled`][miden_protocol::asset::AssetCallbackFlag::Enabled] from creation,
/// even when only reserved policies exist and no active root is set yet. This keeps `has_callbacks`
/// true for the faucet's entire lifetime, so promoting a reserved policy later via
/// `set_send_policy` / `set_receive_policy` enforces it against the whole circulating supply rather
/// than only assets minted after the switch. The slots are omitted only when no send or receive
/// policy of any kind is registered, in which case minted assets carry
/// [`AssetCallbackFlag::Disabled`][miden_protocol::asset::AssetCallbackFlag::Disabled].
///
/// ## Storage layout
///
Expand Down Expand Up @@ -760,6 +769,57 @@ mod tests {
assert_eq!(active_receive_slot.value(), allow_all_root);
}

/// Checks that a manager whose send / receive policies are registered only as reserved
/// alternatives (no active transfer policy yet) still installs the protocol-reserved callback
/// slots with the fixed `invoke_*_policy` wrapper roots, so `has_callbacks` is true from
/// creation.
#[test]
fn reserved_only_transfer_policy_registers_protocol_callback_slots() {
let manager = TokenPolicyManager::builder()
.active_mint_policy(MintPolicy::allow_all())
.active_burn_policy(BurnPolicy::allow_all())
.allowed_send_policy(TransferPolicy::allow_all())
.allowed_receive_policy(TransferPolicy::allow_all())
.build();

let manager_component = manager.to_manager_component();

// Both callback slots are installed even though no transfer policy is active yet.
let on_account_slot =
find_slot(&manager_component, AssetCallbacks::on_before_asset_added_to_account_slot())
.expect(
"reserved receive policy must register the on_before_asset_added_to_account \
protocol callback slot",
);
let on_note_slot =
find_slot(&manager_component, AssetCallbacks::on_before_asset_added_to_note_slot())
.expect(
"reserved send policy must register the on_before_asset_added_to_note protocol \
callback slot",
);

// They hold the fixed wrapper roots, identical to the active-policy case.
assert_eq!(
on_account_slot.value(),
TokenPolicyManager::invoke_receive_policy_root().as_word()
);
assert_eq!(on_note_slot.value(), TokenPolicyManager::invoke_send_policy_root().as_word());

// No policy has been activated yet, so the active-policy slots still hold the empty word.
let active_send_slot =
find_slot(&manager_component, TokenPolicyManager::active_send_policy_slot())
.expect("active send policy slot must be registered");
let active_receive_slot =
find_slot(&manager_component, TokenPolicyManager::active_receive_policy_slot())
.expect("active receive policy slot must be registered");
assert_eq!(active_send_slot.value(), Word::empty());
assert_eq!(active_receive_slot.value(), Word::empty());

// The reserved roots are recorded in the allowed-roots maps so they can be promoted later.
assert!(manager.allowed_send_policies().contains(&TransferPolicy::allow_all().root()));
assert!(manager.allowed_receive_policies().contains(&TransferPolicy::allow_all().root()));
}

/// A manager configured without send / receive policies must NOT register the
/// protocol callback slots — otherwise it would always needlessly mint assets with
/// callbacks enabled.
Expand Down
69 changes: 69 additions & 0 deletions crates/miden-testing/tests/scripts/faucet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,42 @@ fn build_network_faucet_with_burn_switching(
builder.add_account_from_builder(Auth::IncrNonce, account_builder, AccountState::Exists)
}

/// Builds an existing public fungible faucet whose send and receive policies are registered only
/// as reserved alternatives, with no active transfer policy. Used to exercise minting on a faucet
/// that has reserved-but-inactive transfer policies.
fn build_existing_faucet_with_reserved_only_transfer_policy(
builder: &mut MockChainBuilder,
token_symbol: &str,
max_supply: u64,
owner: AccountId,
) -> anyhow::Result<Account> {
let name = TokenName::new(token_symbol)?;
let symbol = TokenSymbol::new(token_symbol)?;
let max_supply = AssetAmount::new(max_supply)?;
let faucet = FungibleFaucet::builder()
.name(name)
.symbol(symbol)
.decimals(10)
.max_supply(max_supply)
.build()?;

let token_policy_manager = TokenPolicyManager::builder()
.active_mint_policy(MintPolicy::allow_all())
.active_burn_policy(BurnPolicy::allow_all())
.allowed_send_policy(TransferPolicy::allow_all())
.allowed_receive_policy(TransferPolicy::allow_all())
.build();

let account_builder = AccountBuilder::new(builder.rng_mut().random())
.account_type(AccountType::Public)
.with_component(faucet)
.with_component(Ownable2Step::new(owner))
.with_component(Authority::OwnerControlled)
.with_components(token_policy_manager);

builder.add_account_from_builder(Auth::IncrNonce, account_builder, AccountState::Exists)
}

/// Builds a network fungible faucet whose active burn policy is `min_burn_amount`, configured
/// with the given threshold. The faucet installs an owner-controlled [`Authority`] so the
/// owner-gated `set_min_burn_amount` setter can be exercised, plus the standard transfer
Expand Down Expand Up @@ -335,6 +371,39 @@ async fn minting_fungible_asset_on_existing_faucet_succeeds() -> anyhow::Result<
Ok(())
}

/// Checks that minting on a faucet whose transfer policies are registered only as reserved
/// alternatives still produces assets carrying `AssetCallbackFlag::Enabled`. The mint succeeds and
/// the output asset is enabled only if `has_callbacks` is true from creation.
#[tokio::test]
async fn minting_on_reserved_only_transfer_policy_faucet_enables_callbacks() -> anyhow::Result<()> {
let mut builder = MockChain::builder();

let owner_account_id =
AccountId::dummy([1; 15], AccountIdVersion::Version1, AccountType::Private);

let faucet = build_existing_faucet_with_reserved_only_transfer_policy(
&mut builder,
"RSV",
1000,
owner_account_id,
)?;
let mut mock_chain = builder.build()?;

let params = FaucetTestParams {
recipient: Word::from([0, 1, 2, 3u32]),
tag: NoteTag::default(),
note_type: NoteType::Private,
amount: Felt::new_unchecked(100),
};

let executed_transaction =
execute_mint_transaction(&mut mock_chain, faucet.clone(), &params).await?;
// `verify_minted_output_note` asserts the minted asset carries `AssetCallbackFlag::Enabled`.
verify_minted_output_note(&executed_transaction, &faucet, &params)?;

Ok(())
}

/// Tests that mint fails when the minted amount would exceed the max supply.
#[tokio::test]
async fn faucet_contract_mint_fungible_asset_fails_exceeds_max_supply() -> anyhow::Result<()> {
Expand Down
Loading