From 0979b41ac8db9d340d4120981aee1eb386b27884 Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Wed, 18 Feb 2026 15:05:35 -0300 Subject: [PATCH 01/10] Add support for unlimited and timed fungible faucets - Introduced `UnlimitedFungibleFaucet` for unrestricted token minting, suitable for testnets or inflationary models. - Implemented `TimedFungibleFaucet` with time-based constraints on token distribution, allowing minting until a specified block and optional burn-only mode post-distribution. - Updated account component interfaces to include new faucet types. - Enhanced documentation to cover new faucet functionalities and usage examples. --- CHANGELOG.md | 1 + .../faucets/timed_fungible_faucet.masm | 6 + .../faucets/unlimited_fungible_faucet.masm | 6 + .../asm/standards/faucets/fixed_fungible.masm | 207 ++++++++++++++ .../asm/standards/faucets/mod.masm | 258 ++++++++---------- .../asm/standards/faucets/timed_fungible.masm | 228 ++++++++++++++++ .../standards/faucets/unlimited_fungible.masm | 109 ++++++++ .../asm/standards/supply/fixed_supply.masm | 194 +++++++++++++ .../asm/standards/supply/flexible_supply.masm | 195 +++++++++++++ .../src/account/components/mod.rs | 40 +++ .../src/account/faucets/mod.rs | 4 + .../src/account/faucets/timed_fungible.rs | 215 +++++++++++++++ .../src/account/faucets/unlimited_fungible.rs | 160 +++++++++++ .../src/account/interface/component.rs | 60 ++++ .../src/account/interface/extension.rs | 11 + docs/src/account/faucets.md | 68 +++++ 16 files changed, 1618 insertions(+), 144 deletions(-) create mode 100644 crates/miden-standards/asm/account_components/faucets/timed_fungible_faucet.masm create mode 100644 crates/miden-standards/asm/account_components/faucets/unlimited_fungible_faucet.masm create mode 100644 crates/miden-standards/asm/standards/faucets/fixed_fungible.masm create mode 100644 crates/miden-standards/asm/standards/faucets/timed_fungible.masm create mode 100644 crates/miden-standards/asm/standards/faucets/unlimited_fungible.masm create mode 100644 crates/miden-standards/asm/standards/supply/fixed_supply.masm create mode 100644 crates/miden-standards/asm/standards/supply/flexible_supply.masm create mode 100644 crates/miden-standards/src/account/faucets/timed_fungible.rs create mode 100644 crates/miden-standards/src/account/faucets/unlimited_fungible.rs create mode 100644 docs/src/account/faucets.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 094361b13c..6b7712d439 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - Increased `TokenSymbol` max allowed length from 6 to 12 uppercase characters ([#2420](https://github.com/0xMiden/miden-base/pull/2420)). - Added `StandardNote::from_script_root()` and `StandardNote::name()` methods, and exposed `NoteType` `PUBLIC`/`PRIVATE` masks as public constants ([#2411](https://github.com/0xMiden/miden-base/pull/2411)). - Resolve standard note scripts directly in `TransactionExecutorHost` instead of querying the data store ([#2417](https://github.com/0xMiden/miden-base/pull/2417)). +- Added `UnlimitedFungibleFaucet` and `TimedFungibleFaucet` components, implementing flexible supply management strategies ([#2386](https://github.com/0xMiden/miden-base/pull/2386)). ### Changes diff --git a/crates/miden-standards/asm/account_components/faucets/timed_fungible_faucet.masm b/crates/miden-standards/asm/account_components/faucets/timed_fungible_faucet.masm new file mode 100644 index 0000000000..e0b22f876c --- /dev/null +++ b/crates/miden-standards/asm/account_components/faucets/timed_fungible_faucet.masm @@ -0,0 +1,6 @@ +# The MASM code of the Timed Fungible Faucet Account Component. +# +# See the `TimedFungibleFaucet` Rust type's documentation for more details. + +pub use ::miden::standards::faucets::timed_fungible::distribute +pub use ::miden::standards::faucets::timed_fungible::burn diff --git a/crates/miden-standards/asm/account_components/faucets/unlimited_fungible_faucet.masm b/crates/miden-standards/asm/account_components/faucets/unlimited_fungible_faucet.masm new file mode 100644 index 0000000000..9679d04707 --- /dev/null +++ b/crates/miden-standards/asm/account_components/faucets/unlimited_fungible_faucet.masm @@ -0,0 +1,6 @@ +# The MASM code of the Unlimited Fungible Faucet Account Component. +# +# See the `UnlimitedFungibleFaucet` Rust type's documentation for more details. + +pub use ::miden::standards::faucets::unlimited_fungible::distribute +pub use ::miden::standards::faucets::unlimited_fungible::burn diff --git a/crates/miden-standards/asm/standards/faucets/fixed_fungible.masm b/crates/miden-standards/asm/standards/faucets/fixed_fungible.masm new file mode 100644 index 0000000000..42f87f7f00 --- /dev/null +++ b/crates/miden-standards/asm/standards/faucets/fixed_fungible.masm @@ -0,0 +1,207 @@ +# FIXED SUPPLY FUNGIBLE FAUCET CONTRACT +# +# A fungible faucet with fixed supply management using the supply::fixed_supply component. +# This faucet enforces a maximum supply cap and tracks total minted tokens. +# ================================================================================================= + +use miden::protocol::active_account +use miden::protocol::active_note +use miden::protocol::faucet +use miden::protocol::native_account +use miden::protocol::output_note +use miden::standards::supply::fixed_supply + +# CONSTANTS +# ================================================================================================= + +const PRIVATE_NOTE=2 + +# ERRORS +# ================================================================================================= + +const ERR_BASIC_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS="burn requires exactly 1 note asset" + +# CONSTANTS +# ================================================================================================= + +# The local memory address at which the metadata slot content is stored. +const METADATA_SLOT_LOCAL=0 + +# The standard slot where fungible faucet metadata like token symbol or decimals are stored. +# Layout: [token_supply, max_supply, decimals, token_symbol] +# Note: This is kept for backward compatibility with existing metadata structure +const METADATA_SLOT=word("miden::standards::fungible_faucets::metadata") + +# PROCEDURES +# ================================================================================================= + +#! Distributes freshly minted fungible assets to the provided recipient by creating a note. +#! +#! This procedure uses the fixed_supply component to enforce supply limits. +#! +#! Inputs: [amount, tag, note_type, RECIPIENT] +#! Outputs: [note_idx] +#! +#! Where: +#! - amount is the amount to be minted and sent. +#! - tag is the tag to be included in the note. +#! - note_type is the type of the note that holds the asset. +#! - RECIPIENT is the recipient of the asset, i.e., +#! hash(hash(hash(serial_num, [0; 4]), script_root), storage_commitment). +#! - note_idx is the index of the created note. +#! +#! Panics if: +#! - the transaction is being executed against an account that is not a fungible asset faucet. +#! - the supply constraints are violated (checked by fixed_supply component). +#! +#! Invocation: exec +@locals(8) +pub proc distribute + # Store inputs to locals + # --------------------------------------------------------------------------------------------- + # Stack: [amount, tag, note_type, RECIPIENT] + + loc_store.4 + # => [tag, note_type, RECIPIENT] + + loc_store.5 + # => [note_type, RECIPIENT] + + loc_store.6 + # => [RECIPIENT] + + # Store RECIPIENT at index 0 (aligned) + # loc_storew_be reads the word but does not pop it + loc_storew_be.0 + # => [RECIPIENT] + + dropw + # => [] + + # Check supply limits using the fixed_supply component + # --------------------------------------------------------------------------------------------- + + loc_load.4 + # => [amount] + + # Prepare stack for check_supply_limit: [amount, pad(14)] + padw padw padw push.0 push.0 + # => [amount, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + + exec.fixed_supply::check_supply_limit + # => [max_mint_amount, token_supply, max_supply, pad(11)] + + # The check passed, clean up the stack + drop drop drop + repeat.11 + drop + end + # => [] + + # Mint the asset + # --------------------------------------------------------------------------------------------- + + exec.faucet::create_fungible_asset + # => [ASSET] + + exec.faucet::mint + # => [ASSET] + + # Update supply using fixed_supply component + # --------------------------------------------------------------------------------------------- + + # Extract amount from ASSET for the update (ASSET = [faucet_id_prefix, faucet_id_suffix, 0, amount]) + dup.3 + # => [amount, ASSET] + + # Prepare for update_supply_after_mint: [amount, pad(15)] + padw padw padw push.0 push.0 push.0 + # => [amount, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ASSET] + + exec.fixed_supply::update_supply_after_mint + # => [pad(16), ASSET] + + dropw dropw dropw dropw + # => [ASSET] + + # Create the note with the asset + # --------------------------------------------------------------------------------------------- + + # Restore RECIPIENT + padw loc_loadw_be.0 + # => [RECIPIENT, ASSET] + + # Restore note_type + loc_load.6 + # => [note_type, RECIPIENT, ASSET] + + # Restore tag + loc_load.5 + # => [tag, note_type, RECIPIENT, ASSET] + + # Create the note + exec.output_note::create + # => [note_idx, ASSET] + + # Add asset to the note + dup movdn.5 movdn.5 + # => [ASSET, note_idx, note_idx] + + exec.output_note::add_asset + # => [note_idx] +end + +#! Burns the fungible asset from the active note. +#! +#! Burning the asset removes it from circulation and reduces the token_supply by the asset's amount. +#! This procedure uses the fixed_supply component to update supply tracking. +#! +#! Inputs: [pad(16)] +#! Outputs: [pad(16)] +#! +#! Panics if: +#! - the procedure is not called from a note context. +#! - the note does not contain exactly one asset. +#! - the transaction is executed against an account which is not a fungible asset faucet. +#! - the transaction is executed against a faucet which is not the origin of the specified asset. +#! - the amount about to be burned is greater than the token_supply of the faucet. +#! +#! Invocation: call +pub proc burn + # Get the asset from the note. + # --------------------------------------------------------------------------------------------- + + # this will fail if not called from a note context. + push.0 exec.active_note::get_assets + # => [num_assets, dest_ptr, pad(16)] + + # Verify we have exactly one asset + assert.err=ERR_BASIC_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS + # => [dest_ptr, pad(16)] + + mem_loadw_be + # => [ASSET, pad(16)] + # => [[faucet_id_prefix, faucet_id_suffix, 0, amount], pad(16)] + + # Burn the asset from the transaction vault + # --------------------------------------------------------------------------------------------- + + dup.3 movdn.4 + # => [ASSET, amount, pad(16)] + + # burn the asset + exec.faucet::burn dropw + # => [amount, pad(16)] + + # Update supply using fixed_supply component + # --------------------------------------------------------------------------------------------- + + # Prepare stack for update_supply_after_burn: [amount, pad(15)] + # Stack is currently [amount, pad(16)] + # We need to drop one pad element to get pad(15) + movup.15 drop + # => [amount, pad(15)] + + exec.fixed_supply::update_supply_after_burn + # => [pad(16)] +end diff --git a/crates/miden-standards/asm/standards/faucets/mod.masm b/crates/miden-standards/asm/standards/faucets/mod.masm index 5451bb9655..99d909e3f5 100644 --- a/crates/miden-standards/asm/standards/faucets/mod.masm +++ b/crates/miden-standards/asm/standards/faucets/mod.masm @@ -4,6 +4,7 @@ use miden::protocol::faucet use miden::protocol::native_account use miden::protocol::output_note use ::miden::protocol::asset::FUNGIBLE_ASSET_MAX_AMOUNT +use miden::standards::supply::fixed_supply # CONSTANTS # ================================================================================================= @@ -13,12 +14,10 @@ const PRIVATE_NOTE=2 # ERRORS # ================================================================================================= +# Retained for backward compatibility of error constants in Rust const ERR_FUNGIBLE_ASSET_TOKEN_SUPPLY_EXCEEDS_MAX_SUPPLY="token supply exceeds max supply" - const ERR_FUNGIBLE_ASSET_MAX_SUPPLY_EXCEEDS_FUNGIBLE_ASSET_MAX_AMOUNT="max supply exceeds maximum representable fungible asset amount" - const ERR_FUNGIBLE_ASSET_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY="token_supply plus the amount passed to distribute would exceed the maximum supply" - const ERR_FAUCET_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY="asset amount to burn exceeds the existing token supply" const ERR_BASIC_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS="burn requires exactly 1 note asset" @@ -33,195 +32,166 @@ const METADATA_SLOT_LOCAL=0 # Layout: [token_supply, max_supply, decimals, token_symbol] const METADATA_SLOT=word("miden::standards::fungible_faucets::metadata") +# PROCEDURES +# ================================================================================================= + #! Distributes freshly minted fungible assets to the provided recipient by creating a note. #! +#! This procedure uses the fixed_supply component to enforce supply limits. +#! #! Inputs: [amount, tag, note_type, RECIPIENT] #! Outputs: [note_idx] #! -#! Where: -#! - amount is the amount to be minted and sent. -#! - tag is the tag to be included in the note. -#! - note_type is the type of the note that holds the asset. -#! - RECIPIENT is the recipient of the asset, i.e., -#! hash(hash(hash(serial_num, [0; 4]), script_root), storage_commitment). -#! - note_idx is the index of the created note. +#! ... #! #! Panics if: #! - the transaction is being executed against an account that is not a fungible asset faucet. -#! - the token supply exceeds the maximum supply. -#! - the maximum supply exceeds the maximum representable fungible asset amount. -#! - the token supply after minting is greater than the maximum allowed supply. +#! - the supply limits are exceeded. #! #! Invocation: exec -@locals(4) +@locals(8) pub proc distribute - # Get the configured max supply and the token supply (= current supply). + # Store inputs to locals # --------------------------------------------------------------------------------------------- - - push.METADATA_SLOT[0..2] exec.active_account::get_item - # => [token_symbol, decimals, max_supply, token_supply, amount, tag, note_type, RECIPIENT] - - # store a copy of the current slot content for the token_supply update later - loc_storew_be.METADATA_SLOT_LOCAL - drop drop - # => [max_supply, token_supply, amount, tag, note_type, RECIPIENT] - - # Assert that minting does not violate any supply constraints. - # - # To make sure we cannot mint more than intended, we need to check: - # 1) (max_supply - token_supply) <= max_supply, i.e. the subtraction does not wrap around - # 2) amount + token_supply does not exceed max_supply - # 3) amount + token_supply is less than FUNGIBLE_ASSET_MAX_AMOUNT - # - # This is done with the following concrete assertions: - # - assert token_supply <= max_supply which ensures 1) - # - assert max_supply <= FUNGIBLE_ASSET_MAX_AMOUNT to help ensure 3) - # - assert amount <= max_mint_amount to ensure 2) as well as 3) - # - this ensures 3) because token_supply + max_mint_amount at most ends up being equal to - # max_supply and we already asserted that max_supply does not exceed - # FUNGIBLE_ASSET_MAX_AMOUNT + # Stack: [amount, tag, note_type, RECIPIENT] + + loc_store.4 + # => [tag, note_type, RECIPIENT] + + loc_store.5 + # => [note_type, RECIPIENT] + + loc_store.6 + # => [RECIPIENT] + + # Store RECIPIENT at index 0 (aligned) + loc_storew_be.0 + # => [RECIPIENT] + + dropw + # => [] + + # Check supply limits using the fixed_supply component # --------------------------------------------------------------------------------------------- - - dup.1 dup.1 - # => [max_supply, token_supply, max_supply, token_supply, amount, tag, note_type, RECIPIENT] - - # assert that token_supply <= max_supply - lte assert.err=ERR_FUNGIBLE_ASSET_TOKEN_SUPPLY_EXCEEDS_MAX_SUPPLY - # => [max_supply, token_supply, amount, tag, note_type, RECIPIENT] - - # assert max_supply <= FUNGIBLE_ASSET_MAX_AMOUNT - dup lte.FUNGIBLE_ASSET_MAX_AMOUNT - assert.err=ERR_FUNGIBLE_ASSET_MAX_SUPPLY_EXCEEDS_FUNGIBLE_ASSET_MAX_AMOUNT - # => [max_supply, token_supply, amount, tag, note_type, RECIPIENT] - - dup.2 swap dup.2 - # => [token_supply, max_supply, amount, token_supply, amount, tag, note_type, RECIPIENT] - - # compute maximum amount that can be minted, max_mint_amount = max_supply - token_supply - sub - # => [max_mint_amount, amount, token_supply, amount, tag, note_type, RECIPIENT] - - # assert amount <= max_mint_amount - lte assert.err=ERR_FUNGIBLE_ASSET_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY - # => [token_supply, amount, tag, note_type, RECIPIENT] - - # Compute the new token_supply and update in storage. - # --------------------------------------------------------------------------------------------- - - dup.1 add - # => [new_token_supply, amount, tag, note_type, RECIPIENT] - - padw loc_loadw_be.METADATA_SLOT_LOCAL - # => [[token_symbol, decimals, max_supply, token_supply], new_token_supply, amount, tag, note_type, RECIPIENT] - - movup.3 drop - # => [[token_symbol, decimals, max_supply, new_token_supply], amount, tag, note_type, RECIPIENT] - - # update the metadata slot with the new supply - push.METADATA_SLOT[0..2] exec.native_account::set_item dropw - # => [amount, tag, note_type, RECIPIENT] - - # Mint the asset. + + loc_load.4 + # => [amount] + + # Prepare stack for check_supply_limit: [amount, pad(14)] + padw padw padw push.0 push.0 + # => [amount, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + + exec.fixed_supply::check_supply_limit + # => [max_mint_amount, token_supply, max_supply, pad(11)] + + # The check passed, clean up the stack + drop drop drop + repeat.12 + drop + end + # => [] + + # Mint the asset # --------------------------------------------------------------------------------------------- - - # creating the asset + + loc_load.4 exec.faucet::create_fungible_asset - # => [ASSET, tag, note_type, RECIPIENT] - - # mint the asset; this is needed to satisfy asset preservation logic. - # this ensures that the asset's faucet ID matches the native account's ID. - # this is ensured because create_fungible_asset creates the asset with the native account's ID + # => [ASSET] + exec.faucet::mint - # => [ASSET, tag, note_type, RECIPIENT] - - movdn.9 movdn.9 movdn.9 movdn.9 - # => [tag, note_type, RECIPIENT, ASSET] - - # Create a new note with the asset. + # => [ASSET] + + # Update supply using fixed_supply component # --------------------------------------------------------------------------------------------- - - # create a note + # Extract amount from ASSET for the update + # ASSET layout: [id_prefix, id_suffix, 0, amount] on stack + dup.3 + # => [amount, ASSET] + + # Prepare for update_supply_after_mint: [amount, pad(15)] + padw padw padw push.0 push.0 push.0 + # => [amount, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ASSET] + + exec.fixed_supply::update_supply_after_mint + # => [pad(16), ASSET] + + # Supply module now returns pad(16), so we drop it + dropw dropw dropw dropw + # => [ASSET] + + # Create the note with the asset + # --------------------------------------------------------------------------------------------- + + # Restore RECIPIENT + padw loc_loadw_be.0 + # => [RECIPIENT, ASSET] + + # Restore note_type + loc_load.6 + # => [note_type, RECIPIENT, ASSET] + + # Restore tag + loc_load.5 + # => [tag, note_type, RECIPIENT, ASSET] + + # Create the note exec.output_note::create # => [note_idx, ASSET] - - # load the ASSET and add it to the note + + # Add asset to the note dup movdn.5 movdn.5 # => [ASSET, note_idx, note_idx] - + exec.output_note::add_asset # => [note_idx] end #! Burns the fungible asset from the active note. #! -#! Burning the asset removes it from circulation and reduces the token_supply by the asset's amount. -#! -#! This procedure retrieves the asset from the active note and burns it. The note must contain -#! exactly one asset, which must be a fungible asset issued by this faucet. -#! -#! Inputs: [pad(16)] -#! Outputs: [pad(16)] -#! -#! Panics if: -#! - the procedure is not called from a note context (active_note::get_assets will fail). -#! - the note does not contain exactly one asset. -#! - the transaction is executed against an account which is not a fungible asset faucet. -#! - the transaction is executed against a faucet which is not the origin of the specified asset. -#! - the amount about to be burned is greater than the token_supply of the faucet. +#! ... #! #! Invocation: call pub proc burn # Get the asset from the note. # --------------------------------------------------------------------------------------------- - + # this will fail if not called from a note context. push.0 exec.active_note::get_assets # => [num_assets, dest_ptr, pad(16)] - + # Verify we have exactly one asset assert.err=ERR_BASIC_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS # => [dest_ptr, pad(16)] - + mem_loadw_be # => [ASSET, pad(16)] # => [[faucet_id_prefix, faucet_id_suffix, 0, amount], pad(16)] - + # Burn the asset from the transaction vault # --------------------------------------------------------------------------------------------- - + + # ASSET layout: [id_prefix, id_suffix, 0, amount] on stack dup.3 movdn.4 # => [ASSET, amount, pad(16)] - + # burn the asset - # this ensures we only burn assets that were issued by this faucet (which implies they are - # fungible) exec.faucet::burn dropw # => [amount, pad(16)] - - # Subtract burnt amount from current token_supply in storage. + + # Update supply using fixed_supply component # --------------------------------------------------------------------------------------------- - - push.METADATA_SLOT[0..2] exec.active_account::get_item - # => [token_symbol, decimals, max_supply, token_supply, amount, pad(16)] - - dup.4 dup.4 - # => [token_supply, amount, token_symbol, decimals, max_supply, token_supply, amount, pad(16)] - - # assert that amount <= token_supply - lte assert.err=ERR_FAUCET_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY - # => [token_symbol, decimals, max_supply, token_supply, amount, pad(16)] - - movup.3 movup.4 - # => [amount, token_supply, token_symbol, decimals, max_supply, pad(16)] - - # compute new_token_supply = token_supply - amount - sub - # => [new_token_supply, token_symbol, decimals, max_supply, pad(16)] - - movdn.3 - # => [token_symbol, decimals, max_supply, new_token_supply, pad(16)] - - # update the metadata slot with the new supply - push.METADATA_SLOT[0..2] exec.native_account::set_item dropw + + # Prepare stack for update_supply_after_burn: [amount, pad(15)] + # Current stack is [amount, pad(16)]. + + # Move one zero to top to drop it and get pad(15) + movup.16 drop + # => [amount, pad(15)] + + exec.fixed_supply::update_supply_after_burn # => [pad(16)] + + # Clean the stack + dropw dropw dropw dropw + # => [] end diff --git a/crates/miden-standards/asm/standards/faucets/timed_fungible.masm b/crates/miden-standards/asm/standards/faucets/timed_fungible.masm new file mode 100644 index 0000000000..e60c80313e --- /dev/null +++ b/crates/miden-standards/asm/standards/faucets/timed_fungible.masm @@ -0,0 +1,228 @@ +# TIMED SUPPLY FUNGIBLE FAUCET CONTRACT +# +# A fungible faucet with time-based supply management using the supply::flexible_supply component. +# This faucet enforces supply limits during a distribution period and optionally allows +# burn-only operations after the period ends. +# ================================================================================================= + +use miden::protocol::active_note +use miden::protocol::faucet +use miden::protocol::output_note +use miden::standards::supply::flexible_supply + +# CONSTANTS +# ================================================================================================= + +const PRIVATE_NOTE=2 + +# ERRORS +# ================================================================================================= + +const ERR_TIMED_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS="burn requires exactly 1 note asset" + +# PROCEDURES +# ================================================================================================= + +#! Distributes freshly minted fungible assets to the provided recipient by creating a note. +#! +#! This procedure uses the flexible_supply component to enforce time-based supply limits. +#! +#! Inputs: [amount, tag, note_type, RECIPIENT] +#! Outputs: [note_idx] +#! +#! Where: +#! - amount is the amount to be minted and sent. +#! - tag is the tag to be included in the note. +#! - note_type is the type of the note that holds the asset. +#! - RECIPIENT is the recipient of the asset, i.e., +#! hash(hash(hash(serial_num, [0; 4]), script_root), storage_commitment). +#! - note_idx is the index of the created note. +#! +#! Panics if: +#! - the transaction is being executed against an account that is not a fungible asset faucet. +#! - the distribution period has ended. +#! - the supply constraints are violated (checked by flexible_supply component). +#! +#! Invocation: exec +@locals(8) +pub proc distribute + # Store inputs to locals + # --------------------------------------------------------------------------------------------- + # Stack: [amount, tag, note_type, RECIPIENT] + + loc_store.4 + # => [tag, note_type, RECIPIENT] + + loc_store.5 + # => [note_type, RECIPIENT] + + loc_store.6 + # => [RECIPIENT] + + # Store RECIPIENT at index 0 (aligned) + # loc_storew_be reads the word but does not pop it + loc_storew_be.0 + # => [RECIPIENT] + + dropw + # => [] + + # Check if distribution is allowed (time + supply constraints) + # --------------------------------------------------------------------------------------------- + + loc_load.4 + # => [amount] + + # Prepare stack for check_can_distribute: [amount, pad(14)] + padw padw padw push.0.0 + # => [amount, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + + exec.flexible_supply::check_can_distribute + # => [max_mint_amount, token_supply, max_supply, pad(11)] + + # The check passed, clean up the stack + drop drop drop + repeat.12 + drop + end + # => [] + + # Mint the asset + # --------------------------------------------------------------------------------------------- + + loc_load.4 + exec.faucet::create_fungible_asset + # => [ASSET] + + exec.faucet::mint + # => [ASSET] + + # Update supply using flexible_supply component + # --------------------------------------------------------------------------------------------- + + # Extract amount from ASSET for the update + # ASSET layout: [id_prefix, id_suffix, 0, amount] on stack + dup.3 + # => [amount, ASSET] + + # Prepare for update_supply_after_mint: [amount, pad(15)] + padw padw padw push.0 push.0 push.0 + # => [amount, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ASSET] + + exec.flexible_supply::update_supply_after_mint + # => [pad(16), ASSET] + + dropw dropw dropw dropw + # => [ASSET] + + # Create the note with the asset + # --------------------------------------------------------------------------------------------- + + # Restore RECIPIENT + padw loc_loadw_be.0 + # => [RECIPIENT, ASSET] + + # Restore note_type + loc_load.6 + # => [note_type, RECIPIENT, ASSET] + + # Restore tag + loc_load.5 + # => [tag, note_type, RECIPIENT, ASSET] + + # Create the note + exec.output_note::create + # => [note_idx, ASSET] + + # Add asset to the note + dup movdn.5 movdn.5 + # => [ASSET, note_idx, note_idx] + + exec.output_note::add_asset + # => [note_idx] +end + +#! Burns the fungible asset from the active note. +#! +#! Burning the asset removes it from circulation and reduces the token_supply. +#! This procedure uses the flexible_supply component which may enforce time-based +#! restrictions (e.g., burn-only mode after distribution period). +#! +#! Inputs: [pad(16)] +#! Outputs: [pad(16)] +#! +#! Panics if: +#! - the procedure is not called from a note context. +#! - the note does not contain exactly one asset. +#! - the transaction is executed against an account which is not a fungible asset faucet. +#! - the transaction is executed against a faucet which is not the origin of the specified asset. +#! - burn is not allowed in the current period (if burn-only mode is enabled). +#! +#! Invocation: call +pub proc burn + # Get the asset from the note. + # --------------------------------------------------------------------------------------------- + + # this will fail if not called from a note context. + push.0 exec.active_note::get_assets + # => [num_assets, dest_ptr, pad(16)] + + # Verify we have exactly one asset + assert.err=ERR_TIMED_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS + # => [dest_ptr, pad(16)] + + mem_loadw_be + # => [ASSET, pad(16)] + # => [[faucet_id_prefix, faucet_id_suffix, 0, amount], pad(16)] + + # Duplicate amount and ASSET for the check + dup.3 dup.3 + # => [amount, amount, ASSET, pad(16)] + # Wait, dup.3 gets amount. dup.3 again gets amount? No, dup.3 of [amount, A, B, C] is B. + # Stack: [0, 1, 2, 3]. + # dup.3 -> index 3. + # [faucet_id_prefix, faucet_id_suffix, 0, amount]. + # dup.3 returns amount. + + # We need to preserve ASSET on stack bottom for later use. + # Stack after mem_loadw_be: [ASSET, pad(16)] + # ASSET = [prefix, suffix, 0, amount] + + dup.3 + # => [amount, ASSET, pad(16)] + + # Check if burn is allowed (time constraints) + # --------------------------------------------------------------------------------------------- + + # Prepare stack for check_can_burn: [amount, pad(15)] + padw padw padw push.0 push.0 push.0 + # => [amount, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ASSET, pad(16)] + + exec.flexible_supply::check_can_burn + # => [pad(16), ASSET, pad(16)] + + # Supply module returns pad(16) + dropw dropw dropw dropw + # => [ASSET, pad(16)] + + # Burn the asset from the transaction vault + # --------------------------------------------------------------------------------------------- + + # ASSET layout: [id_prefix, id_suffix, 0, amount] on stack + dup.3 movdn.4 + # => [ASSET, amount, pad(16)] + + # burn the asset + exec.faucet::burn dropw + # => [amount, pad(16)] + + # Update supply using flexible_supply component + # --------------------------------------------------------------------------------------------- + + # Prepare stack for update_supply_after_burn: [amount, pad(15)] + movup.16 drop + # => [amount, pad(15)] + + exec.flexible_supply::update_supply_after_burn + # => [pad(16)] +end diff --git a/crates/miden-standards/asm/standards/faucets/unlimited_fungible.masm b/crates/miden-standards/asm/standards/faucets/unlimited_fungible.masm new file mode 100644 index 0000000000..8e1281f1f9 --- /dev/null +++ b/crates/miden-standards/asm/standards/faucets/unlimited_fungible.masm @@ -0,0 +1,109 @@ +# UNLIMITED SUPPLY FUNGIBLE FAUCET CONTRACT +# +# A fungible faucet with no supply restrictions. +# This faucet allows unlimited minting and burning of tokens. +# ================================================================================================= + +use miden::protocol::active_note +use miden::protocol::faucet +use miden::protocol::output_note + +# CONSTANTS +# ================================================================================================= + +const PRIVATE_NOTE=2 + +# ERRORS +# ================================================================================================= + +const ERR_UNLIMITED_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS="burn requires exactly 1 note asset" + +# PROCEDURES +# ================================================================================================= + +#! Distributes freshly minted fungible assets to the provided recipient by creating a note. +#! +#! This procedure has no supply restrictions - it can mint unlimited amounts. +#! +#! Inputs: [amount, tag, note_type, RECIPIENT] +#! Outputs: [note_idx] +#! +#! Where: +#! - amount is the amount to be minted and sent. +#! - tag is the tag to be included in the note. +#! - note_type is the type of the note that holds the asset. +#! - RECIPIENT is the recipient of the asset, i.e., +#! hash(hash(hash(serial_num, [0; 4]), script_root), storage_commitment). +#! - note_idx is the index of the created note. +#! +#! Panics if: +#! - the transaction is being executed against an account that is not a fungible asset faucet. +#! +#! Invocation: exec +pub proc distribute + # Mint the asset + # --------------------------------------------------------------------------------------------- + + # Stack: [amount, tag, note_type, RECIPIENT] + # Create the fungible asset + exec.faucet::create_fungible_asset + # => [ASSET, tag, note_type, RECIPIENT] + + # Mint the asset + exec.faucet::mint + # => [ASSET, tag, note_type, RECIPIENT] + + # Rearrange stack for note creation + movdn.9 movdn.9 movdn.9 movdn.9 + # => [tag, note_type, RECIPIENT, ASSET] + + # Create a new note with the asset + # --------------------------------------------------------------------------------------------- + + exec.output_note::create + # => [note_idx, ASSET] + + # Add the asset to the note + dup movdn.5 movdn.5 + # => [ASSET, note_idx, note_idx] + + exec.output_note::add_asset + # => [note_idx] +end + +#! Burns the fungible asset from the active note. +#! +#! Burning the asset removes it from circulation. No supply tracking is performed. +#! +#! Inputs: [pad(16)] +#! Outputs: [pad(16)] +#! +#! Panics if: +#! - the procedure is not called from a note context. +#! - the note does not contain exactly one asset. +#! - the transaction is executed against an account which is not a fungible asset faucet. +#! - the transaction is executed against a faucet which is not the origin of the specified asset. +#! +#! Invocation: call +pub proc burn + # Get the asset from the note. + # --------------------------------------------------------------------------------------------- + + # this will fail if not called from a note context. + push.0 exec.active_note::get_assets + # => [num_assets, dest_ptr, pad(16)] + + # Verify we have exactly one asset + assert.err=ERR_UNLIMITED_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS + # => [dest_ptr, pad(16)] + + mem_loadw_be + # => [ASSET, pad(16)] + + # Burn the asset from the transaction vault + # --------------------------------------------------------------------------------------------- + + # burn the asset (no supply tracking needed) + exec.faucet::burn dropw + # => [pad(16)] +end diff --git a/crates/miden-standards/asm/standards/supply/fixed_supply.masm b/crates/miden-standards/asm/standards/supply/fixed_supply.masm new file mode 100644 index 0000000000..c6aa426b58 --- /dev/null +++ b/crates/miden-standards/asm/standards/supply/fixed_supply.masm @@ -0,0 +1,194 @@ +# miden::standards::supply::fixed_supply +# +# Provides fixed supply management functionality for faucets. +# This component enforces a maximum supply cap and tracks total minted tokens. + +use miden::protocol::active_account +use miden::protocol::native_account + +# CONSTANTS +# ================================================================================================ + +# The slot where the supply configuration is stored. +# Layout: [token_supply, max_supply, decimals, token_symbol] +const SUPPLY_CONFIG_SLOT = word("miden::standards::fungible_faucets::metadata") + +# ERRORS +# ================================================================================================ + +const ERR_SUPPLY_EXCEEDED = "token supply exceeds max supply" +const ERR_SUPPLY_CAP_INVALID = "max supply exceeds maximum representable fungible asset amount" +const ERR_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY = "token_supply plus the amount passed to distribute would exceed the maximum supply" +const ERR_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY = "asset amount to burn exceeds the existing token supply" + +# INTERNAL PROCEDURES +# ================================================================================================ + +#! Returns the current supply state and metadata from storage. +#! +#! Inputs: [] +#! Outputs: [max_supply, token_supply, decimals, token_symbol] +#! +#! Where: +#! - max_supply is the maximum allowed supply. +#! - token_supply is the current total supply of tokens minted. +#! - decimals is the number of decimals (preserved). +#! - token_symbol is the token symbol (preserved). +proc get_supply_state + push.SUPPLY_CONFIG_SLOT[0..2] exec.active_account::get_item + # => [token_symbol, decimals, max_supply, token_supply] + + # We want: [max_supply, token_supply, decimals, token_symbol] + movdn.3 movdn.2 swap + # => [max_supply, token_supply, decimals, token_symbol] +end + +# PUBLIC INTERFACE +# ================================================================================================ + +#! Checks if the given amount can be distributed without exceeding the max supply. +#! +#! Inputs: [amount, pad(14)] +#! Outputs: [max_mint_amount, token_supply, max_supply, pad(12)] +#! +#! Where: +#! - amount is the amount to be minted. +#! - max_mint_amount is the maximum amount that can be minted. +#! - token_supply is the current total supply. +#! - max_supply is the maximum allowed supply. +pub proc check_supply_limit + # Get current supply state + exec.get_supply_state + # => [max, token, dec, sym, amount, pad(10)] + + # Assert token_supply <= max_supply + dup dup.2 + # => [max, token, max, token, dec, sym, amount, pad(9)] + # lte [b, a] -> a <= b. + lte assert.err=ERR_SUPPLY_EXCEEDED + # => [max, token, dec, sym, amount, pad(10)] + + # Calculate max_mint_amount = max_supply - token_supply + dup dup.2 sub + # => [max_mint, max, token, dec, sym, amount, pad(9)] + + # Assert amount <= max_mint_amount + dup.5 dup + # => [max_mint, amount, max_mint, max, token, dec, sym, amount, pad(8)] + swap + # => [amount, max_mint, max_mint, max, token, dec, sym, amount, pad(8)] + lte assert.err=ERR_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY + # => [max_mint, max, token, dec, sym, amount, pad(9)] + + # Clean up and return [max_mint, token, max, pad(12)] + # Current indices: 0:max_mint, 1:max, 2:token, 3:dec, 4:sym, 5:amount + movup.5 drop # drop amount + movup.4 drop # drop sym + movup.3 drop # drop dec + # => [max_mint, max, token, pad(12)] + + # Rearrange to [max_mint, token, max] + movup.2 swap + # => [max_mint, token, max, pad(12)] +end + +#! Updates the supply after minting tokens. +#! +#! Inputs: [amount, pad(15)] +#! Outputs: [pad(16)] +pub proc update_supply_after_mint + # Get current supply state + exec.get_supply_state + # => [max, token, dec, sym, amount, pad(11)] + + # Calculate new_token_supply = token_supply + amount + dup.1 dup.5 add + # => [new_token, max, token, dec, sym, amount, pad(11)] + + # Construct word for storage: [new_token, max, dec, sym] + # Storage order: [token_supply, max_supply, decimals, token_symbol] + # We want [sym, dec, max, new] on stack for set_item VALUE + + # Remove old token + movdn.2 swap drop + # => [max, new_token, dec, sym, amount, pad(11)] + + # Reorder to [sym, dec, max, new] + # Current: 0:max, 1:new, 2:dec, 3:sym + movup.3 # sym + movup.3 # dec + swap + # => [sym, dec, max, new, amount, pad(11)] + + push.SUPPLY_CONFIG_SLOT[0..2] + # => [slot_pre, slot_suf, sym, dec, max, new, amount, pad(9)] + + # Move slot id deep + movdn.5 movdn.5 + # => [sym, dec, max, new, slot_pre, slot_suf, amount, pad(9)] + + exec.native_account::set_item + # => [OLD_WORD, amount, pad(9)] + + # Clean up and return pad(16) + dropw drop + repeat.9 drop end + padw padw padw padw +end + +#! Updates the supply after burning tokens. +#! +#! Inputs: [amount, pad(15)] +#! Outputs: [pad(16)] +pub proc update_supply_after_burn + # Get current supply state + exec.get_supply_state + # => [max, token, dec, sym, amount, pad(11)] + + # Assert amount <= token_supply + dup.1 dup.5 + # => [amount, token, max, token, dec, sym, amount, pad(10)] + lte assert.err=ERR_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY + # => [max, token, dec, sym, amount, pad(11)] + + # Calculate new_token_supply = token_supply - amount + dup.1 dup.5 sub + # => [new_token, max, token, dec, sym, amount, pad(11)] + + # Remove old token + movdn.2 swap drop + # => [max, new_token, dec, sym, amount, pad(11)] + + # Reorder to [sym, dec, max, new] + movup.3 # sym + movup.3 # dec + swap + # => [sym, dec, max, new, amount, pad(11)] + + push.SUPPLY_CONFIG_SLOT[0..2] + movdn.5 movdn.5 + + exec.native_account::set_item + # => [OLD_WORD, amount, pad(9)] + + # Clean up and return pad(16) + dropw drop + repeat.9 drop end + padw padw padw padw +end + +#! Returns the current supply state. +#! +#! Inputs: [pad(16)] +#! Outputs: [token_supply, max_supply, pad(14)] +pub proc get_supply + exec.get_supply_state + # => [max, token, dec, sym, pad(12)] + + # Drop metadata + movup.3 drop movup.2 drop + # => [max, token, pad(14)] + + # Output expected [token, max] + swap +end diff --git a/crates/miden-standards/asm/standards/supply/flexible_supply.masm b/crates/miden-standards/asm/standards/supply/flexible_supply.masm new file mode 100644 index 0000000000..a07c386a6c --- /dev/null +++ b/crates/miden-standards/asm/standards/supply/flexible_supply.masm @@ -0,0 +1,195 @@ +# miden::standards::supply::flexible_supply +# +# Provides time-based supply management functionality for faucets. +# This component enforces supply limits during a distribution period and optionally +# allows burn-only operations after the period ends. + +use miden::protocol::active_account +use miden::protocol::native_account +use miden::protocol::tx + +# CONSTANTS +# ================================================================================================ + +# The slot where the flexible supply configuration is stored. +# Layout: [token_supply, max_supply, distribution_end_block, burn_only_flag] +const FLEXIBLE_SUPPLY_CONFIG_SLOT = word("miden::standards::supply::flexible_supply::config") + +# ERRORS +# ================================================================================================ + +const ERR_SUPPLY_EXCEEDED = "token supply exceeds max supply" +const ERR_SUPPLY_CAP_INVALID = "max supply exceeds maximum representable fungible asset amount" +const ERR_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY = "token_supply plus the amount passed to distribute would exceed the maximum supply" +const ERR_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY = "asset amount to burn exceeds the existing token supply" +const ERR_DISTRIBUTION_PERIOD_ENDED = "distribution period has ended" +const ERR_BURN_NOT_ALLOWED = "burn operations are not allowed during distribution period" + +# INTERNAL PROCEDURES +# ================================================================================================ + +#! Returns the current flexible supply state from storage. +#! +#! Inputs: [] +#! Outputs: [token_supply, max_supply, distribution_end_block, burn_only_flag] +proc get_flexible_supply_state + push.FLEXIBLE_SUPPLY_CONFIG_SLOT[0..2] exec.active_account::get_item + # => [burn_only_flag, distribution_end_block, max_supply, token_supply] + + # Rearrange to [token_supply, max_supply, distribution_end_block, burn_only_flag] + movdn.3 movdn.2 swap +end + +#! Gets the current block height. +#! +#! Inputs: [] +#! Outputs: [block_height] +proc get_current_block_height + exec.tx::get_block_number +end + +# PUBLIC INTERFACE +# ================================================================================================ + +#! Checks if distribution is allowed based on time and supply constraints. +#! +#! Inputs: [amount, pad(14)] +#! Outputs: [max_mint_amount, token_supply, max_supply, pad(12)] +pub proc check_can_distribute + # Get current supply state + exec.get_flexible_supply_state + # => [token, max, end, burn, amount, pad(10)] + + # Check if distribution period has ended (if end > 0) + dup.2 + if.true + exec.get_current_block_height + dup.3 # end block + lt assert.err=ERR_DISTRIBUTION_PERIOD_ENDED + end + # => [token, max, end, burn, amount, pad(10)] + + # Assert token <= max + dup dup.2 lte assert.err=ERR_SUPPLY_EXCEEDED + # => [token, max, end, burn, amount, pad(10)] + + # Calculate max_mint = max - token + dup.1 dup.1 sub + # => [max_mint, token, max, end, burn, amount, pad(9)] + + # Assert amount <= max_mint + dup.5 dup lte assert.err=ERR_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY + # => [max_mint, token, max, end, burn, amount, pad(10)] + + # Clean up and return [max_mint, token, max, pad(12)] + movup.5 drop # amount + movup.4 drop # burn + movup.3 drop # end + # => [max_mint, token, max, pad(12)] +end + +#! Checks if burn is allowed based on time constraints. +#! +#! Inputs: [amount, pad(15)] +#! Outputs: [pad(16)] +pub proc check_can_burn + # Get current supply state + exec.get_flexible_supply_state + # => [token, max, end, burn, amount, pad(11)] + + # If burn_only_flag is 1, burns are only allowed AFTER the period + dup.3 + if.true + dup.2 # end + dup push.0 eq assert.err=ERR_BURN_NOT_ALLOWED # end block must be set for burn-only mode + + exec.get_current_block_height + swap gte assert.err=ERR_BURN_NOT_ALLOWED + end + # => [token, max, end, burn, amount, pad(11)] + + # Assert amount <= token + dup dup.5 lte assert.err=ERR_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY + # => [token, max, end, burn, amount, pad(11)] + + # Clean up and return pad(16) + drop drop drop drop drop + repeat.11 drop end + padw padw padw padw +end + +#! Updates the supply after minting tokens. +#! +#! Inputs: [amount, pad(15)] +#! Outputs: [pad(16)] +pub proc update_supply_after_mint + exec.get_flexible_supply_state + # => [token, max, end, burn, amount, pad(11)] + + # Calculate new_token = token + amount + dup dup.5 add + # => [new, token, max, end, burn, amount, pad(11)] + + # Remove old token and amount not needed for storage + movdn.4 swap drop + # => [max, end, burn, new, amount, pad(11)] + + # Reorder to [burn, end, max, new] + # Current: 0:max, 1:end, 2:burn, 3:new + movup.2 # burn + movup.2 # end + swap + # => [burn, end, max, new, amount, pad(11)] + + push.FLEXIBLE_SUPPLY_CONFIG_SLOT[0..2] + # => [pre, suf, burn, end, max, new, ... (18 items total)] + movdn.5 movdn.5 + # => [burn, end, max, new, pre, suf, amount, pad(11)] + + exec.native_account::set_item + # => [OLD, amount, pad(11)] + + dropw drop repeat.11 drop end + padw padw padw padw +end + +#! Updates the supply after burning tokens. +#! +#! Inputs: [amount, pad(15)] +#! Outputs: [pad(16)] +pub proc update_supply_after_burn + exec.get_flexible_supply_state + # => [token, max, end, burn, amount, pad(11)] + + # Calculate new_token = token - amount + dup dup.5 sub + # => [new, token, max, end, burn, amount, pad(11)] + + # Remove old token + movdn.4 swap drop + # => [max, end, burn, new, amount, pad(11)] + + # Reorder to [burn, end, max, new] + movup.2 # burn + movup.2 # end + swap + # => [burn, end, max, new, amount, pad(11)] + + push.FLEXIBLE_SUPPLY_CONFIG_SLOT[0..2] + movdn.5 movdn.5 + + exec.native_account::set_item + # => [OLD, amount, pad(11)] + + dropw drop repeat.11 drop end + padw padw padw padw +end + +#! Returns the current flexible supply state. +#! +#! Inputs: [pad(16)] +#! Outputs: [token_supply, max_supply, distribution_end_block, burn_only_flag, pad(12)] +pub proc get_supply + exec.get_flexible_supply_state + # => [token, max, end, burn, pad(12)] +end diff --git a/crates/miden-standards/src/account/components/mod.rs b/crates/miden-standards/src/account/components/mod.rs index 7a7df0a0d3..20acd9f54b 100644 --- a/crates/miden-standards/src/account/components/mod.rs +++ b/crates/miden-standards/src/account/components/mod.rs @@ -108,6 +108,24 @@ static NETWORK_FUNGIBLE_FAUCET_LIBRARY: LazyLock = LazyLock::new(|| { Library::read_from_bytes(bytes).expect("Shipped Network Fungible Faucet library is well-formed") }); +// Initialize the Unlimited Fungible Faucet library only once. +static UNLIMITED_FUNGIBLE_FAUCET_LIBRARY: LazyLock = LazyLock::new(|| { + let bytes = include_bytes!(concat!( + env!("OUT_DIR"), + "/assets/account_components/faucets/unlimited_fungible_faucet.masl" + )); + Library::read_from_bytes(bytes).expect("Shipped Unlimited Fungible Faucet library is well-formed") +}); + +// Initialize the Timed Fungible Faucet library only once. +static TIMED_FUNGIBLE_FAUCET_LIBRARY: LazyLock = LazyLock::new(|| { + let bytes = include_bytes!(concat!( + env!("OUT_DIR"), + "/assets/account_components/faucets/timed_fungible_faucet.masl" + )); + Library::read_from_bytes(bytes).expect("Shipped Timed Fungible Faucet library is well-formed") +}); + // METADATA LIBRARIES // ================================================================================================ @@ -130,6 +148,16 @@ pub fn basic_fungible_faucet_library() -> Library { BASIC_FUNGIBLE_FAUCET_LIBRARY.clone() } +/// Returns the Unlimited Fungible Faucet Library. +pub fn unlimited_fungible_faucet_library() -> Library { + UNLIMITED_FUNGIBLE_FAUCET_LIBRARY.clone() +} + +/// Returns the Timed Fungible Faucet Library. +pub fn timed_fungible_faucet_library() -> Library { + TIMED_FUNGIBLE_FAUCET_LIBRARY.clone() +} + /// Returns the Network Fungible Faucet Library. pub fn network_fungible_faucet_library() -> Library { NETWORK_FUNGIBLE_FAUCET_LIBRARY.clone() @@ -183,6 +211,8 @@ pub fn falcon_512_rpo_multisig_library() -> Library { pub enum StandardAccountComponent { BasicWallet, BasicFungibleFaucet, + UnlimitedFungibleFaucet, + TimedFungibleFaucet, NetworkFungibleFaucet, AuthEcdsaK256Keccak, AuthEcdsaK256KeccakAcl, @@ -199,6 +229,8 @@ impl StandardAccountComponent { let library = match self { Self::BasicWallet => BASIC_WALLET_LIBRARY.as_ref(), Self::BasicFungibleFaucet => BASIC_FUNGIBLE_FAUCET_LIBRARY.as_ref(), + Self::UnlimitedFungibleFaucet => UNLIMITED_FUNGIBLE_FAUCET_LIBRARY.as_ref(), + Self::TimedFungibleFaucet => TIMED_FUNGIBLE_FAUCET_LIBRARY.as_ref(), Self::NetworkFungibleFaucet => NETWORK_FUNGIBLE_FAUCET_LIBRARY.as_ref(), Self::AuthEcdsaK256Keccak => ECDSA_K256_KECCAK_LIBRARY.as_ref(), Self::AuthEcdsaK256KeccakAcl => ECDSA_K256_KECCAK_ACL_LIBRARY.as_ref(), @@ -246,6 +278,12 @@ impl StandardAccountComponent { Self::BasicFungibleFaucet => { component_interface_vec.push(AccountComponentInterface::BasicFungibleFaucet) }, + Self::UnlimitedFungibleFaucet => { + component_interface_vec.push(AccountComponentInterface::UnlimitedFungibleFaucet) + }, + Self::TimedFungibleFaucet => { + component_interface_vec.push(AccountComponentInterface::TimedFungibleFaucet) + }, Self::NetworkFungibleFaucet => { component_interface_vec.push(AccountComponentInterface::NetworkFungibleFaucet) }, @@ -280,6 +318,8 @@ impl StandardAccountComponent { ) { Self::BasicWallet.extract_component(procedures_set, component_interface_vec); Self::BasicFungibleFaucet.extract_component(procedures_set, component_interface_vec); + Self::UnlimitedFungibleFaucet.extract_component(procedures_set, component_interface_vec); + Self::TimedFungibleFaucet.extract_component(procedures_set, component_interface_vec); Self::NetworkFungibleFaucet.extract_component(procedures_set, component_interface_vec); Self::AuthEcdsaK256Keccak.extract_component(procedures_set, component_interface_vec); Self::AuthEcdsaK256KeccakAcl.extract_component(procedures_set, component_interface_vec); diff --git a/crates/miden-standards/src/account/faucets/mod.rs b/crates/miden-standards/src/account/faucets/mod.rs index 2b7eab4fef..42ee864a93 100644 --- a/crates/miden-standards/src/account/faucets/mod.rs +++ b/crates/miden-standards/src/account/faucets/mod.rs @@ -7,10 +7,14 @@ use thiserror::Error; mod basic_fungible; mod network_fungible; mod token_metadata; +mod timed_fungible; +mod unlimited_fungible; pub use basic_fungible::{BasicFungibleFaucet, create_basic_fungible_faucet}; pub use network_fungible::{NetworkFungibleFaucet, create_network_fungible_faucet}; pub use token_metadata::TokenMetadata; +pub use timed_fungible::{TimedFungibleFaucet, create_timed_fungible_faucet}; +pub use unlimited_fungible::{UnlimitedFungibleFaucet, create_unlimited_fungible_faucet}; // FUNGIBLE FAUCET ERROR // ================================================================================================ diff --git a/crates/miden-standards/src/account/faucets/timed_fungible.rs b/crates/miden-standards/src/account/faucets/timed_fungible.rs new file mode 100644 index 0000000000..e59697fa2f --- /dev/null +++ b/crates/miden-standards/src/account/faucets/timed_fungible.rs @@ -0,0 +1,215 @@ +use miden_protocol::account::component::{ + AccountComponentMetadata, + FeltSchema, + SchemaTypeId, + StorageSchema, + StorageSlotSchema, +}; +use miden_protocol::account::{ + Account, + AccountBuilder, + AccountComponent, + AccountStorageMode, + AccountType, + StorageSlot, + StorageSlotName, +}; +use miden_protocol::asset::TokenSymbol; +use miden_protocol::{Felt, FieldElement, Word}; +use miden_protocol::utils::sync::LazyLock; + +use super::{FungibleFaucetError, TokenMetadata}; +use crate::account::AuthScheme; +use crate::account::auth::{ + AuthEcdsaK256KeccakAcl, + AuthEcdsaK256KeccakAclConfig, + AuthFalcon512RpoAcl, + AuthFalcon512RpoAclConfig, +}; +use crate::account::components::timed_fungible_faucet_library; + +/// The schema type ID for token symbols. +const TOKEN_SYMBOL_TYPE_ID: &str = "miden::standards::fungible_faucets::metadata::token_symbol"; +/// The schema type ID for timed supply config. +const TIMED_SUPPLY_CONFIG_TYPE_ID: &str = "miden::standards::supply::flexible_supply::config"; + +use crate::procedure_digest; + +// SLOT NAMES +// ================================================================================================ + +static SUPPLY_CONFIG_SLOT: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::supply::flexible_supply::config") + .expect("storage slot name should be valid") +}); + +// TIMED FUNGIBLE FAUCET ACCOUNT COMPONENT +// ================================================================================================ + +procedure_digest!( + TIMED_FUNGIBLE_FAUCET_DISTRIBUTE, + TimedFungibleFaucet::DISTRIBUTE_PROC_NAME, + timed_fungible_faucet_library +); + +procedure_digest!( + TIMED_FUNGIBLE_FAUCET_BURN, + TimedFungibleFaucet::BURN_PROC_NAME, + timed_fungible_faucet_library +); + +pub struct TimedFungibleFaucet { + metadata: TokenMetadata, + distribution_end: u32, + burn_only: bool, +} + +impl TimedFungibleFaucet { + pub const NAME: &'static str = "miden::timed_fungible_faucet"; + pub const MAX_DECIMALS: u8 = TokenMetadata::MAX_DECIMALS; + + const DISTRIBUTE_PROC_NAME: &str = "timed_fungible_faucet::distribute"; + const BURN_PROC_NAME: &str = "timed_fungible_faucet::burn"; + + pub fn new( + symbol: TokenSymbol, + decimals: u8, + max_supply: Felt, + distribution_end: u32, + burn_only: bool, + ) -> Result { + let metadata = TokenMetadata::new(symbol, decimals, max_supply)?; + Ok(Self { + metadata, + distribution_end, + burn_only, + }) + } + + pub fn metadata(&self) -> &TokenMetadata { + &self.metadata + } + + pub fn metadata_slot() -> &'static StorageSlotName { + TokenMetadata::metadata_slot() + } + + pub fn supply_config_slot() -> &'static StorageSlotName { + &SUPPLY_CONFIG_SLOT + } + + pub fn distribute_digest() -> Word { + *TIMED_FUNGIBLE_FAUCET_DISTRIBUTE + } + + pub fn burn_digest() -> Word { + *TIMED_FUNGIBLE_FAUCET_BURN + } +} + +impl From for AccountComponent { + fn from(faucet: TimedFungibleFaucet) -> Self { + let metadata_slot: StorageSlot = faucet.metadata.into(); + + let config_val = [ + Felt::ZERO, // token_supply tracks supply here + faucet.metadata.max_supply(), + Felt::new(faucet.distribution_end as u64), + Felt::new(faucet.burn_only as u64) + ]; + + let config_slot = StorageSlot::with_value( + TimedFungibleFaucet::supply_config_slot().clone(), + Word::new(config_val) + ); + + let token_symbol_type = SchemaTypeId::new(TOKEN_SYMBOL_TYPE_ID).unwrap(); + let supply_config_type = SchemaTypeId::new(TIMED_SUPPLY_CONFIG_TYPE_ID).unwrap(); + + let metadata_slot_schema = StorageSlotSchema::value( + "Token metadata", + [ + FeltSchema::felt("token_supply").with_default(Felt::new(0)), + FeltSchema::felt("max_supply"), + FeltSchema::u8("decimals"), + FeltSchema::new_typed(token_symbol_type, "symbol"), + ], + ); + + // Custom schema for supply config: [token_supply, max_supply, distribution_end, burn_only] + let config_slot_schema = StorageSlotSchema::value( + "Supply Config", + [ + FeltSchema::felt("token_supply").with_default(Felt::new(0)), + FeltSchema::felt("max_supply"), + FeltSchema::u32("distribution_end"), + FeltSchema::new_typed(supply_config_type, "burn_only_flag"), + ], + ); + + // Use map or vec of tuples + let schema_entries = alloc::vec![ + (TimedFungibleFaucet::metadata_slot().clone(), metadata_slot_schema), + (TimedFungibleFaucet::supply_config_slot().clone(), config_slot_schema), + ]; + + let storage_schema = StorageSchema::new(schema_entries) + .expect("storage schema valid"); + + let metadata = AccountComponentMetadata::new(TimedFungibleFaucet::NAME) + .with_description("Timed fungible faucet component") + .with_supported_type(AccountType::FungibleFaucet) + .with_storage_schema(storage_schema); + + AccountComponent::new( + timed_fungible_faucet_library(), + vec![metadata_slot, config_slot], + metadata + ).expect("timed fungible faucet component valid") + } +} + +pub fn create_timed_fungible_faucet( + init_seed: [u8; 32], + symbol: TokenSymbol, + decimals: u8, + max_supply: Felt, + distribution_end: u32, + burn_only: bool, + storage_mode: AccountStorageMode, + auth_scheme: AuthScheme, +) -> Result { + let distribute_proc_root = TimedFungibleFaucet::distribute_digest(); + + let auth_component: AccountComponent = match auth_scheme { + AuthScheme::Falcon512Rpo { pub_key } => AuthFalcon512RpoAcl::new( + pub_key, + AuthFalcon512RpoAclConfig::new() + .with_auth_trigger_procedures(vec![distribute_proc_root]) + .with_allow_unauthorized_input_notes(true), + ) + .map_err(FungibleFaucetError::AccountError)? + .into(), + AuthScheme::EcdsaK256Keccak { pub_key } => AuthEcdsaK256KeccakAcl::new( + pub_key, + AuthEcdsaK256KeccakAclConfig::new() + .with_auth_trigger_procedures(vec![distribute_proc_root]) + .with_allow_unauthorized_input_notes(true), + ) + .map_err(FungibleFaucetError::AccountError)? + .into(), + _ => return Err(FungibleFaucetError::UnsupportedAuthScheme("Unsupported auth scheme".into())), + }; + + let faucet_component = TimedFungibleFaucet::new(symbol, decimals, max_supply, distribution_end, burn_only)?; + + let account = AccountBuilder::new(init_seed) + .account_type(AccountType::FungibleFaucet) + .storage_mode(storage_mode) + .with_auth_component(auth_component) + .with_component(faucet_component) + .build() + .map_err(FungibleFaucetError::AccountError)?; + + Ok(account) +} diff --git a/crates/miden-standards/src/account/faucets/unlimited_fungible.rs b/crates/miden-standards/src/account/faucets/unlimited_fungible.rs new file mode 100644 index 0000000000..4a4f89b1d9 --- /dev/null +++ b/crates/miden-standards/src/account/faucets/unlimited_fungible.rs @@ -0,0 +1,160 @@ +use miden_protocol::account::component::{ + AccountComponentMetadata, + FeltSchema, + SchemaTypeId, + StorageSchema, + StorageSlotSchema, +}; +use miden_protocol::account::{ + Account, + AccountBuilder, + AccountComponent, + AccountStorageMode, + AccountType, + StorageSlot, + StorageSlotName, +}; +use miden_protocol::asset::TokenSymbol; +use miden_protocol::{Felt, Word}; + +use super::{FungibleFaucetError, TokenMetadata}; +use crate::account::AuthScheme; +use crate::account::auth::{ + AuthEcdsaK256KeccakAcl, + AuthEcdsaK256KeccakAclConfig, + AuthFalcon512RpoAcl, + AuthFalcon512RpoAclConfig, +}; +use crate::account::components::unlimited_fungible_faucet_library; + +/// The schema type ID for token symbols. +const TOKEN_SYMBOL_TYPE_ID: &str = "miden::standards::fungible_faucets::metadata::token_symbol"; + +use crate::procedure_digest; + +// UNLIMITED FUNGIBLE FAUCET ACCOUNT COMPONENT +// ================================================================================================ + +procedure_digest!( + UNLIMITED_FUNGIBLE_FAUCET_DISTRIBUTE, + UnlimitedFungibleFaucet::DISTRIBUTE_PROC_NAME, + unlimited_fungible_faucet_library +); + +procedure_digest!( + UNLIMITED_FUNGIBLE_FAUCET_BURN, + UnlimitedFungibleFaucet::BURN_PROC_NAME, + unlimited_fungible_faucet_library +); + +pub struct UnlimitedFungibleFaucet { + metadata: TokenMetadata, +} + +impl UnlimitedFungibleFaucet { + pub const NAME: &'static str = "miden::unlimited_fungible_faucet"; + pub const MAX_DECIMALS: u8 = TokenMetadata::MAX_DECIMALS; + + const DISTRIBUTE_PROC_NAME: &str = "unlimited_fungible_faucet::distribute"; + const BURN_PROC_NAME: &str = "unlimited_fungible_faucet::burn"; + + pub fn new(symbol: TokenSymbol, decimals: u8) -> Result { + let max_supply = miden_protocol::asset::FungibleAsset::MAX_AMOUNT; + // We assume Unlimited Faucet doesn't track supply on-chain in metadata slot effectively, + // as logic doesn't update it. But we initialize it correctly. + let metadata = TokenMetadata::new(symbol, decimals, Felt::new(max_supply))?; + Ok(Self { metadata }) + } + + pub fn metadata(&self) -> &TokenMetadata { + &self.metadata + } + + pub fn metadata_slot() -> &'static StorageSlotName { + TokenMetadata::metadata_slot() + } + + pub fn distribute_digest() -> Word { + *UNLIMITED_FUNGIBLE_FAUCET_DISTRIBUTE + } + + pub fn burn_digest() -> Word { + *UNLIMITED_FUNGIBLE_FAUCET_BURN + } +} + +impl From for AccountComponent { + fn from(faucet: UnlimitedFungibleFaucet) -> Self { + let storage_slot: StorageSlot = faucet.metadata.into(); + + let token_symbol_type = SchemaTypeId::new(TOKEN_SYMBOL_TYPE_ID).unwrap(); + + let metadata_slot_schema = StorageSlotSchema::value( + "Token metadata", + [ + FeltSchema::felt("token_supply").with_default(Felt::new(0)), + FeltSchema::felt("max_supply"), + FeltSchema::u8("decimals"), + FeltSchema::new_typed(token_symbol_type, "symbol"), + ], + ); + + let schema_entry = ( + UnlimitedFungibleFaucet::metadata_slot().clone(), + metadata_slot_schema + ); + + let storage_schema = StorageSchema::new(alloc::vec![schema_entry]) + .expect("storage schema valid"); + + let metadata = AccountComponentMetadata::new(UnlimitedFungibleFaucet::NAME) + .with_description("Unlimited fungible faucet component for minting and burning tokens") + .with_supported_type(AccountType::FungibleFaucet) + .with_storage_schema(storage_schema); + + AccountComponent::new(unlimited_fungible_faucet_library(), vec![storage_slot], metadata) + .expect("unlimited fungible faucet component should satisfy the requirements of a valid account component") + } +} + +pub fn create_unlimited_fungible_faucet( + init_seed: [u8; 32], + symbol: TokenSymbol, + decimals: u8, + storage_mode: AccountStorageMode, + auth_scheme: AuthScheme, +) -> Result { + let distribute_proc_root = UnlimitedFungibleFaucet::distribute_digest(); + + let auth_component: AccountComponent = match auth_scheme { + AuthScheme::Falcon512Rpo { pub_key } => AuthFalcon512RpoAcl::new( + pub_key, + AuthFalcon512RpoAclConfig::new() + .with_auth_trigger_procedures(vec![distribute_proc_root]) + .with_allow_unauthorized_input_notes(true), + ) + .map_err(FungibleFaucetError::AccountError)? + .into(), + AuthScheme::EcdsaK256Keccak { pub_key } => AuthEcdsaK256KeccakAcl::new( + pub_key, + AuthEcdsaK256KeccakAclConfig::new() + .with_auth_trigger_procedures(vec![distribute_proc_root]) + .with_allow_unauthorized_input_notes(true), + ) + .map_err(FungibleFaucetError::AccountError)? + .into(), + _ => return Err(FungibleFaucetError::UnsupportedAuthScheme("Unsupported auth scheme".into())), + }; + + let faucet_component = UnlimitedFungibleFaucet::new(symbol, decimals)?; + + let account = AccountBuilder::new(init_seed) + .account_type(AccountType::FungibleFaucet) + .storage_mode(storage_mode) + .with_auth_component(auth_component) + .with_component(faucet_component) + .build() + .map_err(FungibleFaucetError::AccountError)?; + + Ok(account) +} diff --git a/crates/miden-standards/src/account/interface/component.rs b/crates/miden-standards/src/account/interface/component.rs index 110e23d1b3..dd3ee544d3 100644 --- a/crates/miden-standards/src/account/interface/component.rs +++ b/crates/miden-standards/src/account/interface/component.rs @@ -29,6 +29,12 @@ pub enum AccountComponentInterface { /// [`BasicFungibleFaucet`][crate::account::faucets::BasicFungibleFaucet] module. BasicFungibleFaucet, /// Exposes procedures from the + /// [`UnlimitedFungibleFaucet`][crate::account::faucets::UnlimitedFungibleFaucet] module. + UnlimitedFungibleFaucet, + /// Exposes procedures from the + /// [`TimedFungibleFaucet`][crate::account::faucets::TimedFungibleFaucet] module. + TimedFungibleFaucet, + /// Exposes procedures from the /// [`NetworkFungibleFaucet`][crate::account::faucets::NetworkFungibleFaucet] module. NetworkFungibleFaucet, /// Exposes procedures from the @@ -71,6 +77,10 @@ impl AccountComponentInterface { match self { AccountComponentInterface::BasicWallet => "Basic Wallet".to_string(), AccountComponentInterface::BasicFungibleFaucet => "Basic Fungible Faucet".to_string(), + AccountComponentInterface::UnlimitedFungibleFaucet => { + "Unlimited Fungible Faucet".to_string() + }, + AccountComponentInterface::TimedFungibleFaucet => "Timed Fungible Faucet".to_string(), AccountComponentInterface::NetworkFungibleFaucet => { "Network Fungible Faucet".to_string() }, @@ -264,6 +274,56 @@ impl AccountComponentInterface { amount = asset.unwrap_fungible().amount() )); }, + AccountComponentInterface::UnlimitedFungibleFaucet => { + if partial_note.assets().num_assets() != 1 { + return Err(AccountInterfaceError::FaucetNoteWithoutAsset); + } + + let asset = + partial_note.assets().iter().next().expect("note should contain an asset"); + + if asset.faucet_id_prefix() != sender_account_id.prefix() { + return Err(AccountInterfaceError::IssuanceFaucetMismatch( + asset.faucet_id_prefix(), + )); + } + + body.push_str(&format!( + " + push.{amount} + call.::miden::standards::faucets::unlimited_fungible::distribute + # => [note_idx, pad(25)] + swapdw dropw dropw swap drop + # => [note_idx, pad(16)]\n + ", + amount = asset.unwrap_fungible().amount() + )); + }, + AccountComponentInterface::TimedFungibleFaucet => { + if partial_note.assets().num_assets() != 1 { + return Err(AccountInterfaceError::FaucetNoteWithoutAsset); + } + + let asset = + partial_note.assets().iter().next().expect("note should contain an asset"); + + if asset.faucet_id_prefix() != sender_account_id.prefix() { + return Err(AccountInterfaceError::IssuanceFaucetMismatch( + asset.faucet_id_prefix(), + )); + } + + body.push_str(&format!( + " + push.{amount} + call.::miden::standards::faucets::timed_fungible::distribute + # => [note_idx, pad(25)] + swapdw dropw dropw swap drop + # => [note_idx, pad(16)]\n + ", + amount = asset.unwrap_fungible().amount() + )); + }, AccountComponentInterface::BasicWallet => { body.push_str( " diff --git a/crates/miden-standards/src/account/interface/extension.rs b/crates/miden-standards/src/account/interface/extension.rs index 9954d32632..6126186f6d 100644 --- a/crates/miden-standards/src/account/interface/extension.rs +++ b/crates/miden-standards/src/account/interface/extension.rs @@ -21,6 +21,8 @@ use crate::account::components::{ falcon_512_rpo_multisig_library, network_fungible_faucet_library, no_auth_library, + timed_fungible_faucet_library, + unlimited_fungible_faucet_library, }; use crate::account::interface::{ AccountComponentInterface, @@ -98,6 +100,15 @@ impl AccountInterfaceExt for AccountInterface { component_proc_digests .extend(basic_fungible_faucet_library().mast_forest().procedure_digests()); }, + AccountComponentInterface::UnlimitedFungibleFaucet => { + component_proc_digests.extend( + unlimited_fungible_faucet_library().mast_forest().procedure_digests(), + ); + }, + AccountComponentInterface::TimedFungibleFaucet => { + component_proc_digests + .extend(timed_fungible_faucet_library().mast_forest().procedure_digests()); + }, AccountComponentInterface::NetworkFungibleFaucet => { component_proc_digests.extend( network_fungible_faucet_library().mast_forest().procedure_digests(), diff --git a/docs/src/account/faucets.md b/docs/src/account/faucets.md new file mode 100644 index 0000000000..3e09b01601 --- /dev/null +++ b/docs/src/account/faucets.md @@ -0,0 +1,68 @@ +# Fungible Faucets + +Miden Standards provides several standard implementations for fungible faucets, offering different strategies for supply management and distribution. + +## Basic Fungible Faucet (Fixed Supply) + +The `BasicFungibleFaucet` account component implements a standard fixed-supply token model. + +- **Supply Strategy**: Fixed maximum supply. +- **Minting**: Authorized accounts can mint tokens up to the defined `max_supply`. Attempts to mint beyond this limit will fail. +- **Burning**: Token holders can burn tokens, reducing the circulating supply. +- **Storage**: Uses `TokenMetadata` stored in a single storage slot to track the current supply, max supply, decimals, and symbol. + +### Usage + +```rust +use miden_standards::account::faucets::{BasicFungibleFaucet, create_basic_fungible_faucet}; + +// Create a new faucet with a max supply of 1,000,000 +let faucet = BasicFungibleFaucet::new(symbol, decimals, max_supply)?; +``` + +## Unlimited Fungible Faucet + +The `UnlimitedFungibleFaucet` allows for unrestricted minting of tokens, suitable for testnets or inflationary models. + +- **Supply Strategy**: Unlimited (capped only by protocol limits, i.e., `FungibleAsset::MAX_AMOUNT`). +- **Minting**: Authorized accounts can mint an arbitrary amount of tokens. +- **Burning**: Supported. +- **Storage**: Stores `TokenMetadata`. Supply tracking is maintained but does not restrict further minting. + +### Usage + +```rust +use miden_standards::account::faucets::{UnlimitedFungibleFaucet, create_unlimited_fungible_faucet}; + +let faucet = UnlimitedFungibleFaucet::new(symbol, decimals)?; +``` + +## Timed Fungible Faucet + +The `TimedFungibleFaucet` introduces time-based constraints on the distribution of tokens. + +- **Supply Strategy**: Flexible supply with a time limit. +- **Distribution Period**: Minting is allowed only until a specified block number (`distribution_end`). +- **Post-Distribution Behavior**: After `distribution_end`, minting is disabled. +- **Burn Only Mode**: Can be configured to allow only burning of tokens (no further minting) after the distribution period. +- **Storage**: Uses two storage slots: + 1. `TokenMetadata`: Standard metadata. + 2. `SupplyConfig`: Tracks `token_supply`, `max_supply`, `distribution_end`, and `burn_only` flag. + +### Usage + +```rust +use miden_standards::account::faucets::{TimedFungibleFaucet, create_timed_fungible_faucet}; + +// Distribution ends at block 10,000 +let distribution_end = 10_000u32; +let burn_only = true; + +let faucet = TimedFungibleFaucet::new( + symbol, + decimals, + max_supply, + distribution_end, + burn_only +)?; +``` From f057b833ed608af3c9217b49e5af881e5e20069d Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Wed, 18 Feb 2026 15:25:17 -0300 Subject: [PATCH 02/10] feat: update procedures for timed and unlimited fungible faucets with enhanced error handling and metadata management --- .../asm/standards/faucets/mod.masm | 4 +- .../asm/standards/faucets/timed_fungible.masm | 2 +- .../src/account/faucets/mod.rs | 8 + .../src/account/faucets/timed_fungible.rs | 439 +++++++++++++++--- .../src/account/faucets/unlimited_fungible.rs | 338 ++++++++++++-- 5 files changed, 706 insertions(+), 85 deletions(-) diff --git a/crates/miden-standards/asm/standards/faucets/mod.masm b/crates/miden-standards/asm/standards/faucets/mod.masm index 99d909e3f5..7191108901 100644 --- a/crates/miden-standards/asm/standards/faucets/mod.masm +++ b/crates/miden-standards/asm/standards/faucets/mod.masm @@ -184,8 +184,8 @@ pub proc burn # Prepare stack for update_supply_after_burn: [amount, pad(15)] # Current stack is [amount, pad(16)]. - # Move one zero to top to drop it and get pad(15) - movup.16 drop + # Drop one zero to get pad(15) + swap drop # => [amount, pad(15)] exec.fixed_supply::update_supply_after_burn diff --git a/crates/miden-standards/asm/standards/faucets/timed_fungible.masm b/crates/miden-standards/asm/standards/faucets/timed_fungible.masm index e60c80313e..72a45c01c3 100644 --- a/crates/miden-standards/asm/standards/faucets/timed_fungible.masm +++ b/crates/miden-standards/asm/standards/faucets/timed_fungible.masm @@ -220,7 +220,7 @@ pub proc burn # --------------------------------------------------------------------------------------------- # Prepare stack for update_supply_after_burn: [amount, pad(15)] - movup.16 drop + swap drop # => [amount, pad(15)] exec.flexible_supply::update_supply_after_burn diff --git a/crates/miden-standards/src/account/faucets/mod.rs b/crates/miden-standards/src/account/faucets/mod.rs index 42ee864a93..bc32a9742d 100644 --- a/crates/miden-standards/src/account/faucets/mod.rs +++ b/crates/miden-standards/src/account/faucets/mod.rs @@ -36,6 +36,14 @@ pub enum FungibleFaucetError { "account interface does not have the procedures of the network fungible faucet component" )] MissingNetworkFungibleFaucetInterface, + #[error( + "account interface does not have the procedures of the timed fungible faucet component" + )] + MissingTimedFungibleFaucetInterface, + #[error( + "account interface does not have the procedures of the unlimited fungible faucet component" + )] + MissingUnlimitedFungibleFaucetInterface, #[error("failed to retrieve storage slot with name {slot_name}")] StorageLookupFailed { slot_name: StorageSlotName, diff --git a/crates/miden-standards/src/account/faucets/timed_fungible.rs b/crates/miden-standards/src/account/faucets/timed_fungible.rs index e59697fa2f..ff8626031f 100644 --- a/crates/miden-standards/src/account/faucets/timed_fungible.rs +++ b/crates/miden-standards/src/account/faucets/timed_fungible.rs @@ -9,14 +9,15 @@ use miden_protocol::account::{ Account, AccountBuilder, AccountComponent, + AccountStorage, AccountStorageMode, AccountType, StorageSlot, StorageSlotName, }; use miden_protocol::asset::TokenSymbol; -use miden_protocol::{Felt, FieldElement, Word}; use miden_protocol::utils::sync::LazyLock; +use miden_protocol::{Felt, FieldElement, Word}; use super::{FungibleFaucetError, TokenMetadata}; use crate::account::AuthScheme; @@ -27,13 +28,11 @@ use crate::account::auth::{ AuthFalcon512RpoAclConfig, }; use crate::account::components::timed_fungible_faucet_library; +use crate::account::interface::{AccountComponentInterface, AccountInterface, AccountInterfaceExt}; +use crate::procedure_digest; /// The schema type ID for token symbols. const TOKEN_SYMBOL_TYPE_ID: &str = "miden::standards::fungible_faucets::metadata::token_symbol"; -/// The schema type ID for timed supply config. -const TIMED_SUPPLY_CONFIG_TYPE_ID: &str = "miden::standards::supply::flexible_supply::config"; - -use crate::procedure_digest; // SLOT NAMES // ================================================================================================ @@ -58,6 +57,25 @@ procedure_digest!( timed_fungible_faucet_library ); +/// An [`AccountComponent`] implementing a timed fungible faucet. +/// +/// It reexports the procedures from `miden::standards::faucets::timed_fungible`. When linking +/// against this component, the `miden` library (i.e. +/// [`ProtocolLib`](miden_protocol::ProtocolLib)) must be available to the assembler which is the +/// case when using [`CodeBuilder`][builder]. The procedures of this component are: +/// - `distribute`, which mints assets and creates a note for the provided recipient within the +/// allowed time window. +/// - `burn`, which burns the provided asset (respects burn-only mode). +/// +/// This component supports accounts of type [`AccountType::FungibleFaucet`]. +/// +/// ## Storage Layout +/// +/// - [`Self::metadata_slot`]: Stores [`TokenMetadata`]. +/// - [`Self::supply_config_slot`]: Stores supply config +/// `[token_supply, max_supply, distribution_end, burn_only]`. +/// +/// [builder]: crate::code_builder::CodeBuilder pub struct TimedFungibleFaucet { metadata: TokenMetadata, distribution_end: u32, @@ -65,12 +83,30 @@ pub struct TimedFungibleFaucet { } impl TimedFungibleFaucet { + // CONSTANTS + // -------------------------------------------------------------------------------------------- + + /// The name of the component. pub const NAME: &'static str = "miden::timed_fungible_faucet"; + + /// The maximum number of decimals supported by the component. pub const MAX_DECIMALS: u8 = TokenMetadata::MAX_DECIMALS; const DISTRIBUTE_PROC_NAME: &str = "timed_fungible_faucet::distribute"; const BURN_PROC_NAME: &str = "timed_fungible_faucet::burn"; + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Creates a new [`TimedFungibleFaucet`] component from the given pieces of metadata and with + /// an initial token supply of zero. + /// + /// # Errors + /// + /// Returns an error if: + /// - the decimals parameter exceeds maximum value of [`Self::MAX_DECIMALS`]. + /// - the max supply parameter exceeds maximum possible amount for a fungible asset + /// ([`miden_protocol::asset::FungibleAsset::MAX_AMOUNT`]) pub fn new( symbol: TokenSymbol, decimals: u8, @@ -86,89 +122,229 @@ impl TimedFungibleFaucet { }) } - pub fn metadata(&self) -> &TokenMetadata { - &self.metadata + /// Creates a new [`TimedFungibleFaucet`] component from the given [`TokenMetadata`]. + pub fn from_metadata( + metadata: TokenMetadata, + distribution_end: u32, + burn_only: bool, + ) -> Self { + Self { metadata, distribution_end, burn_only } } + /// Attempts to create a new [`TimedFungibleFaucet`] component from the associated account + /// interface and storage. + /// + /// # Errors + /// + /// Returns an error if: + /// - the provided [`AccountInterface`] does not contain a + /// [`AccountComponentInterface::TimedFungibleFaucet`] component. + /// - the decimals parameter exceeds maximum value of [`Self::MAX_DECIMALS`]. + /// - the max supply value exceeds maximum possible amount for a fungible asset of + /// [`miden_protocol::asset::FungibleAsset::MAX_AMOUNT`]. + /// - the token supply exceeds the max supply. + /// - the token symbol encoded value exceeds the maximum value of + /// [`TokenSymbol::MAX_ENCODED_VALUE`]. + fn try_from_interface( + interface: AccountInterface, + storage: &AccountStorage, + ) -> Result { + if !interface.components().contains(&AccountComponentInterface::TimedFungibleFaucet) { + return Err(FungibleFaucetError::MissingTimedFungibleFaucetInterface); + } + + let metadata = TokenMetadata::try_from(storage)?; + + // Read supply config: [token_supply, max_supply, distribution_end, burn_only] + let config_word: Word = storage + .get_item(TimedFungibleFaucet::supply_config_slot()) + .map_err(|err| FungibleFaucetError::StorageLookupFailed { + slot_name: TimedFungibleFaucet::supply_config_slot().clone(), + source: err, + })?; + + let distribution_end = config_word[2].as_int() as u32; + let burn_only = config_word[3].as_int() != 0; + + Ok(Self { metadata, distribution_end, burn_only }) + } + + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the [`StorageSlotName`] where the [`TimedFungibleFaucet`]'s metadata is stored. pub fn metadata_slot() -> &'static StorageSlotName { TokenMetadata::metadata_slot() } - + + /// Returns the [`StorageSlotName`] where the supply configuration is stored. pub fn supply_config_slot() -> &'static StorageSlotName { &SUPPLY_CONFIG_SLOT } + /// Returns the storage slot schema for the metadata slot. + pub fn metadata_slot_schema() -> (StorageSlotName, StorageSlotSchema) { + let token_symbol_type = SchemaTypeId::new(TOKEN_SYMBOL_TYPE_ID).expect("valid type id"); + ( + Self::metadata_slot().clone(), + StorageSlotSchema::value( + "Token metadata", + [ + FeltSchema::felt("token_supply").with_default(Felt::new(0)), + FeltSchema::felt("max_supply"), + FeltSchema::u8("decimals"), + FeltSchema::new_typed(token_symbol_type, "symbol"), + ], + ), + ) + } + + /// Returns the storage slot schema for the supply config slot. + pub fn supply_config_slot_schema() -> (StorageSlotName, StorageSlotSchema) { + ( + Self::supply_config_slot().clone(), + StorageSlotSchema::value( + "Supply Config", + [ + FeltSchema::felt("token_supply").with_default(Felt::new(0)), + FeltSchema::felt("max_supply"), + FeltSchema::u32("distribution_end"), + FeltSchema::felt("burn_only_flag").with_default(Felt::new(0)), + ], + ), + ) + } + + /// Returns the token metadata. + pub fn metadata(&self) -> &TokenMetadata { + &self.metadata + } + + /// Returns the symbol of the faucet. + pub fn symbol(&self) -> TokenSymbol { + self.metadata.symbol() + } + + /// Returns the decimals of the faucet. + pub fn decimals(&self) -> u8 { + self.metadata.decimals() + } + + /// Returns the max supply (in base units) of the faucet. + pub fn max_supply(&self) -> Felt { + self.metadata.max_supply() + } + + /// Returns the token supply (in base units) of the faucet. + pub fn token_supply(&self) -> Felt { + self.metadata.token_supply() + } + + /// Returns the block number at which distribution ends. + pub fn distribution_end(&self) -> u32 { + self.distribution_end + } + + /// Returns whether the faucet is in burn-only mode after the distribution period. + pub fn burn_only(&self) -> bool { + self.burn_only + } + + /// Returns the digest of the `distribute` account procedure. pub fn distribute_digest() -> Word { *TIMED_FUNGIBLE_FAUCET_DISTRIBUTE } + /// Returns the digest of the `burn` account procedure. pub fn burn_digest() -> Word { *TIMED_FUNGIBLE_FAUCET_BURN } + + // MUTATORS + // -------------------------------------------------------------------------------------------- + + /// Sets the token_supply (in base units) of the timed fungible faucet. + /// + /// # Errors + /// + /// Returns an error if: + /// - the token supply exceeds the max supply. + pub fn with_token_supply(mut self, token_supply: Felt) -> Result { + self.metadata = self.metadata.with_token_supply(token_supply)?; + Ok(self) + } } impl From for AccountComponent { fn from(faucet: TimedFungibleFaucet) -> Self { let metadata_slot: StorageSlot = faucet.metadata.into(); - + let config_val = [ - Felt::ZERO, // token_supply tracks supply here + Felt::ZERO, // token_supply starts at zero faucet.metadata.max_supply(), Felt::new(faucet.distribution_end as u64), - Felt::new(faucet.burn_only as u64) + Felt::new(faucet.burn_only as u64), ]; - - let config_slot = StorageSlot::with_value( - TimedFungibleFaucet::supply_config_slot().clone(), - Word::new(config_val) - ); - let token_symbol_type = SchemaTypeId::new(TOKEN_SYMBOL_TYPE_ID).unwrap(); - let supply_config_type = SchemaTypeId::new(TIMED_SUPPLY_CONFIG_TYPE_ID).unwrap(); - - let metadata_slot_schema = StorageSlotSchema::value( - "Token metadata", - [ - FeltSchema::felt("token_supply").with_default(Felt::new(0)), - FeltSchema::felt("max_supply"), - FeltSchema::u8("decimals"), - FeltSchema::new_typed(token_symbol_type, "symbol"), - ], - ); - - // Custom schema for supply config: [token_supply, max_supply, distribution_end, burn_only] - let config_slot_schema = StorageSlotSchema::value( - "Supply Config", - [ - FeltSchema::felt("token_supply").with_default(Felt::new(0)), - FeltSchema::felt("max_supply"), - FeltSchema::u32("distribution_end"), - FeltSchema::new_typed(supply_config_type, "burn_only_flag"), - ], + let config_slot = StorageSlot::with_value( + TimedFungibleFaucet::supply_config_slot().clone(), + Word::new(config_val), ); - - // Use map or vec of tuples - let schema_entries = alloc::vec![ - (TimedFungibleFaucet::metadata_slot().clone(), metadata_slot_schema), - (TimedFungibleFaucet::supply_config_slot().clone(), config_slot_schema), - ]; - let storage_schema = StorageSchema::new(schema_entries) - .expect("storage schema valid"); + let storage_schema = StorageSchema::new([ + TimedFungibleFaucet::metadata_slot_schema(), + TimedFungibleFaucet::supply_config_slot_schema(), + ]) + .expect("storage schema should be valid"); let metadata = AccountComponentMetadata::new(TimedFungibleFaucet::NAME) - .with_description("Timed fungible faucet component") + .with_description( + "Timed fungible faucet component for time-bounded minting and burning tokens", + ) .with_supported_type(AccountType::FungibleFaucet) .with_storage_schema(storage_schema); AccountComponent::new( - timed_fungible_faucet_library(), - vec![metadata_slot, config_slot], - metadata - ).expect("timed fungible faucet component valid") + timed_fungible_faucet_library(), + vec![metadata_slot, config_slot], + metadata, + ) + .expect("timed fungible faucet component should satisfy the requirements of a valid account component") + } +} + +impl TryFrom for TimedFungibleFaucet { + type Error = FungibleFaucetError; + + fn try_from(account: Account) -> Result { + let account_interface = AccountInterface::from_account(&account); + + TimedFungibleFaucet::try_from_interface(account_interface, account.storage()) } } +impl TryFrom<&Account> for TimedFungibleFaucet { + type Error = FungibleFaucetError; + + fn try_from(account: &Account) -> Result { + let account_interface = AccountInterface::from_account(account); + + TimedFungibleFaucet::try_from_interface(account_interface, account.storage()) + } +} + +/// Creates a new faucet account with timed fungible faucet interface, +/// account storage type, specified authentication scheme, and provided metadata (token symbol, +/// decimals, max supply, distribution end block, burn-only flag). +/// +/// The timed faucet interface exposes two procedures: +/// - `distribute`, which mints assets and creates a note for the provided recipient within the +/// distribution time window. +/// - `burn`, which burns the provided asset (respects burn-only mode). +/// +/// The `distribute` procedure can be called from a transaction script and requires authentication +/// via the specified authentication scheme. The `burn` procedure can only be called from a note +/// script and requires the calling note to contain the asset to be burned. pub fn create_timed_fungible_faucet( init_seed: [u8; 32], symbol: TokenSymbol, @@ -198,11 +374,33 @@ pub fn create_timed_fungible_faucet( ) .map_err(FungibleFaucetError::AccountError)? .into(), - _ => return Err(FungibleFaucetError::UnsupportedAuthScheme("Unsupported auth scheme".into())), + AuthScheme::NoAuth => { + return Err(FungibleFaucetError::UnsupportedAuthScheme( + "timed fungible faucets cannot be created with NoAuth authentication scheme".into(), + )); + }, + AuthScheme::Falcon512RpoMultisig { threshold: _, pub_keys: _ } => { + return Err(FungibleFaucetError::UnsupportedAuthScheme( + "timed fungible faucets do not support multisig authentication".into(), + )); + }, + AuthScheme::Unknown => { + return Err(FungibleFaucetError::UnsupportedAuthScheme( + "timed fungible faucets cannot be created with Unknown authentication scheme" + .into(), + )); + }, + AuthScheme::EcdsaK256KeccakMultisig { threshold: _, pub_keys: _ } => { + return Err(FungibleFaucetError::UnsupportedAuthScheme( + "timed fungible faucets do not support EcdsaK256KeccakMultisig authentication" + .into(), + )); + }, }; - let faucet_component = TimedFungibleFaucet::new(symbol, decimals, max_supply, distribution_end, burn_only)?; - + let faucet_component = + TimedFungibleFaucet::new(symbol, decimals, max_supply, distribution_end, burn_only)?; + let account = AccountBuilder::new(init_seed) .account_type(AccountType::FungibleFaucet) .storage_mode(storage_mode) @@ -213,3 +411,136 @@ pub fn create_timed_fungible_faucet( Ok(account) } + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + use assert_matches::assert_matches; + use miden_protocol::account::auth::PublicKeyCommitment; + use miden_protocol::{FieldElement, ONE, Word}; + + use super::{ + AccountBuilder, + AccountStorageMode, + AccountType, + AuthScheme, + Felt, + FungibleFaucetError, + TimedFungibleFaucet, + TokenSymbol, + create_timed_fungible_faucet, + }; + use crate::account::auth::{AuthFalcon512Rpo, AuthFalcon512RpoAcl}; + use crate::account::wallets::BasicWallet; + + #[test] + fn timed_faucet_contract_creation() { + let pub_key_word = Word::new([ONE; 4]); + let auth_scheme: AuthScheme = AuthScheme::Falcon512Rpo { pub_key: pub_key_word.into() }; + + let init_seed: [u8; 32] = [ + 90, 110, 209, 94, 84, 105, 250, 242, 223, 203, 216, 124, 22, 159, 14, 132, 215, 85, + 183, 204, 149, 90, 166, 68, 100, 73, 106, 168, 125, 237, 138, 16, + ]; + + let max_supply = Felt::new(1_000_000); + let token_symbol = TokenSymbol::try_from("TMD").unwrap(); + let decimals = 6u8; + let distribution_end = 10_000u32; + let burn_only = true; + let storage_mode = AccountStorageMode::Private; + + let faucet_account = create_timed_fungible_faucet( + init_seed, + token_symbol, + decimals, + max_supply, + distribution_end, + burn_only, + storage_mode, + auth_scheme, + ) + .unwrap(); + + assert!(faucet_account.is_faucet()); + assert_eq!(faucet_account.account_type(), AccountType::FungibleFaucet); + + // Check metadata slot + assert_eq!( + faucet_account.storage().get_item(TimedFungibleFaucet::metadata_slot()).unwrap(), + [Felt::ZERO, max_supply, Felt::new(6), token_symbol.into()].into() + ); + + // Check supply config slot + assert_eq!( + faucet_account + .storage() + .get_item(TimedFungibleFaucet::supply_config_slot()) + .unwrap(), + [ + Felt::ZERO, + max_supply, + Felt::new(distribution_end as u64), + Felt::new(burn_only as u64) + ] + .into() + ); + + // Verify the faucet can be extracted via TryFrom + let faucet_component = TimedFungibleFaucet::try_from(faucet_account.clone()).unwrap(); + assert_eq!(faucet_component.symbol(), token_symbol); + assert_eq!(faucet_component.decimals(), decimals); + assert_eq!(faucet_component.max_supply(), max_supply); + assert_eq!(faucet_component.token_supply(), Felt::ZERO); + assert_eq!(faucet_component.distribution_end(), distribution_end); + assert_eq!(faucet_component.burn_only(), burn_only); + } + + #[test] + fn timed_faucet_create_from_account() { + let mock_word = Word::from([0, 1, 2, 3u32]); + let mock_public_key = PublicKeyCommitment::from(mock_word); + let mock_seed = mock_word.as_bytes(); + + let token_symbol = TokenSymbol::new("TMD").expect("invalid token symbol"); + let faucet_account = AccountBuilder::new(mock_seed) + .account_type(AccountType::FungibleFaucet) + .with_component( + TimedFungibleFaucet::new(token_symbol, 6, Felt::new(1_000_000), 10_000, true) + .expect("failed to create a timed fungible faucet component"), + ) + .with_auth_component(AuthFalcon512Rpo::new(mock_public_key)) + .build_existing() + .expect("failed to create faucet account"); + + let timed_ff = TimedFungibleFaucet::try_from(faucet_account) + .expect("timed fungible faucet creation failed"); + assert_eq!(timed_ff.symbol(), token_symbol); + assert_eq!(timed_ff.decimals(), 6); + assert_eq!(timed_ff.max_supply(), Felt::new(1_000_000)); + assert_eq!(timed_ff.token_supply(), Felt::ZERO); + assert_eq!(timed_ff.distribution_end(), 10_000); + assert!(timed_ff.burn_only()); + + // invalid account: timed fungible faucet component is missing + let invalid_faucet_account = AccountBuilder::new(mock_seed) + .account_type(AccountType::FungibleFaucet) + .with_auth_component(AuthFalcon512Rpo::new(mock_public_key)) + .with_component(BasicWallet) + .build_existing() + .expect("failed to create account"); + + let err = TimedFungibleFaucet::try_from(invalid_faucet_account) + .err() + .expect("timed fungible faucet creation should fail"); + assert_matches!(err, FungibleFaucetError::MissingTimedFungibleFaucetInterface); + } + + #[test] + fn get_timed_faucet_procedures() { + let _distribute_digest = TimedFungibleFaucet::distribute_digest(); + let _burn_digest = TimedFungibleFaucet::burn_digest(); + } +} diff --git a/crates/miden-standards/src/account/faucets/unlimited_fungible.rs b/crates/miden-standards/src/account/faucets/unlimited_fungible.rs index 4a4f89b1d9..b5cb10bf1f 100644 --- a/crates/miden-standards/src/account/faucets/unlimited_fungible.rs +++ b/crates/miden-standards/src/account/faucets/unlimited_fungible.rs @@ -9,6 +9,7 @@ use miden_protocol::account::{ Account, AccountBuilder, AccountComponent, + AccountStorage, AccountStorageMode, AccountType, StorageSlot, @@ -26,12 +27,12 @@ use crate::account::auth::{ AuthFalcon512RpoAclConfig, }; use crate::account::components::unlimited_fungible_faucet_library; +use crate::account::interface::{AccountComponentInterface, AccountInterface, AccountInterfaceExt}; +use crate::procedure_digest; /// The schema type ID for token symbols. const TOKEN_SYMBOL_TYPE_ID: &str = "miden::standards::fungible_faucets::metadata::token_symbol"; -use crate::procedure_digest; - // UNLIMITED FUNGIBLE FAUCET ACCOUNT COMPONENT // ================================================================================================ @@ -47,68 +48,180 @@ procedure_digest!( unlimited_fungible_faucet_library ); +/// An [`AccountComponent`] implementing an unlimited fungible faucet. +/// +/// It reexports the procedures from `miden::standards::faucets::unlimited_fungible`. When linking +/// against this component, the `miden` library (i.e. +/// [`ProtocolLib`](miden_protocol::ProtocolLib)) must be available to the assembler which is the +/// case when using [`CodeBuilder`][builder]. The procedures of this component are: +/// - `distribute`, which mints assets and creates a note for the provided recipient. +/// - `burn`, which burns the provided asset. +/// +/// No supply checks are enforced — minting is unrestricted up to protocol limits +/// ([`FungibleAsset::MAX_AMOUNT`](miden_protocol::asset::FungibleAsset::MAX_AMOUNT)). +/// +/// This component supports accounts of type [`AccountType::FungibleFaucet`]. +/// +/// ## Storage Layout +/// +/// - [`Self::metadata_slot`]: Stores [`TokenMetadata`]. +/// +/// [builder]: crate::code_builder::CodeBuilder pub struct UnlimitedFungibleFaucet { metadata: TokenMetadata, } impl UnlimitedFungibleFaucet { + // CONSTANTS + // -------------------------------------------------------------------------------------------- + + /// The name of the component. pub const NAME: &'static str = "miden::unlimited_fungible_faucet"; + + /// The maximum number of decimals supported by the component. pub const MAX_DECIMALS: u8 = TokenMetadata::MAX_DECIMALS; const DISTRIBUTE_PROC_NAME: &str = "unlimited_fungible_faucet::distribute"; const BURN_PROC_NAME: &str = "unlimited_fungible_faucet::burn"; + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Creates a new [`UnlimitedFungibleFaucet`] component. + /// + /// The max supply is set to [`FungibleAsset::MAX_AMOUNT`](miden_protocol::asset::FungibleAsset::MAX_AMOUNT) + /// since this faucet does not enforce supply limits at runtime. + /// + /// # Errors + /// + /// Returns an error if: + /// - the decimals parameter exceeds maximum value of [`Self::MAX_DECIMALS`]. pub fn new(symbol: TokenSymbol, decimals: u8) -> Result { let max_supply = miden_protocol::asset::FungibleAsset::MAX_AMOUNT; - // We assume Unlimited Faucet doesn't track supply on-chain in metadata slot effectively, - // as logic doesn't update it. But we initialize it correctly. let metadata = TokenMetadata::new(symbol, decimals, Felt::new(max_supply))?; Ok(Self { metadata }) } - pub fn metadata(&self) -> &TokenMetadata { - &self.metadata + /// Creates a new [`UnlimitedFungibleFaucet`] component from the given [`TokenMetadata`]. + pub fn from_metadata(metadata: TokenMetadata) -> Self { + Self { metadata } + } + + /// Attempts to create a new [`UnlimitedFungibleFaucet`] component from the associated account + /// interface and storage. + /// + /// # Errors + /// + /// Returns an error if: + /// - the provided [`AccountInterface`] does not contain a + /// [`AccountComponentInterface::UnlimitedFungibleFaucet`] component. + /// - the decimals parameter exceeds maximum value of [`Self::MAX_DECIMALS`]. + /// - the max supply value exceeds maximum possible amount for a fungible asset of + /// [`miden_protocol::asset::FungibleAsset::MAX_AMOUNT`]. + /// - the token supply exceeds the max supply. + /// - the token symbol encoded value exceeds the maximum value of + /// [`TokenSymbol::MAX_ENCODED_VALUE`]. + fn try_from_interface( + interface: AccountInterface, + storage: &AccountStorage, + ) -> Result { + if !interface + .components() + .contains(&AccountComponentInterface::UnlimitedFungibleFaucet) + { + return Err(FungibleFaucetError::MissingUnlimitedFungibleFaucetInterface); + } + + let metadata = TokenMetadata::try_from(storage)?; + Ok(Self { metadata }) } + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the [`StorageSlotName`] where the [`UnlimitedFungibleFaucet`]'s metadata is stored. pub fn metadata_slot() -> &'static StorageSlotName { TokenMetadata::metadata_slot() } + /// Returns the storage slot schema for the metadata slot. + pub fn metadata_slot_schema() -> (StorageSlotName, StorageSlotSchema) { + let token_symbol_type = SchemaTypeId::new(TOKEN_SYMBOL_TYPE_ID).expect("valid type id"); + ( + Self::metadata_slot().clone(), + StorageSlotSchema::value( + "Token metadata", + [ + FeltSchema::felt("token_supply").with_default(Felt::new(0)), + FeltSchema::felt("max_supply"), + FeltSchema::u8("decimals"), + FeltSchema::new_typed(token_symbol_type, "symbol"), + ], + ), + ) + } + + /// Returns the token metadata. + pub fn metadata(&self) -> &TokenMetadata { + &self.metadata + } + + /// Returns the symbol of the faucet. + pub fn symbol(&self) -> TokenSymbol { + self.metadata.symbol() + } + + /// Returns the decimals of the faucet. + pub fn decimals(&self) -> u8 { + self.metadata.decimals() + } + + /// Returns the max supply (in base units) of the faucet. + pub fn max_supply(&self) -> Felt { + self.metadata.max_supply() + } + + /// Returns the token supply (in base units) of the faucet. + pub fn token_supply(&self) -> Felt { + self.metadata.token_supply() + } + + /// Returns the digest of the `distribute` account procedure. pub fn distribute_digest() -> Word { *UNLIMITED_FUNGIBLE_FAUCET_DISTRIBUTE } + /// Returns the digest of the `burn` account procedure. pub fn burn_digest() -> Word { *UNLIMITED_FUNGIBLE_FAUCET_BURN } + + // MUTATORS + // -------------------------------------------------------------------------------------------- + + /// Sets the token_supply (in base units) of the unlimited fungible faucet. + /// + /// # Errors + /// + /// Returns an error if: + /// - the token supply exceeds the max supply. + pub fn with_token_supply(mut self, token_supply: Felt) -> Result { + self.metadata = self.metadata.with_token_supply(token_supply)?; + Ok(self) + } } impl From for AccountComponent { fn from(faucet: UnlimitedFungibleFaucet) -> Self { let storage_slot: StorageSlot = faucet.metadata.into(); - let token_symbol_type = SchemaTypeId::new(TOKEN_SYMBOL_TYPE_ID).unwrap(); - - let metadata_slot_schema = StorageSlotSchema::value( - "Token metadata", - [ - FeltSchema::felt("token_supply").with_default(Felt::new(0)), - FeltSchema::felt("max_supply"), - FeltSchema::u8("decimals"), - FeltSchema::new_typed(token_symbol_type, "symbol"), - ], - ); - - let schema_entry = ( - UnlimitedFungibleFaucet::metadata_slot().clone(), - metadata_slot_schema - ); - - let storage_schema = StorageSchema::new(alloc::vec![schema_entry]) - .expect("storage schema valid"); + let storage_schema = StorageSchema::new([UnlimitedFungibleFaucet::metadata_slot_schema()]) + .expect("storage schema should be valid"); let metadata = AccountComponentMetadata::new(UnlimitedFungibleFaucet::NAME) - .with_description("Unlimited fungible faucet component for minting and burning tokens") + .with_description( + "Unlimited fungible faucet component for minting and burning tokens", + ) .with_supported_type(AccountType::FungibleFaucet) .with_storage_schema(storage_schema); @@ -117,6 +230,35 @@ impl From for AccountComponent { } } +impl TryFrom for UnlimitedFungibleFaucet { + type Error = FungibleFaucetError; + + fn try_from(account: Account) -> Result { + let account_interface = AccountInterface::from_account(&account); + + UnlimitedFungibleFaucet::try_from_interface(account_interface, account.storage()) + } +} + +impl TryFrom<&Account> for UnlimitedFungibleFaucet { + type Error = FungibleFaucetError; + + fn try_from(account: &Account) -> Result { + let account_interface = AccountInterface::from_account(account); + + UnlimitedFungibleFaucet::try_from_interface(account_interface, account.storage()) + } +} + +/// Creates a new faucet account with unlimited fungible faucet interface, +/// account storage type, specified authentication scheme, and provided metadata (token symbol, +/// decimals). +/// +/// The unlimited faucet interface exposes two procedures: +/// - `distribute`, which mints assets and creates a note for the provided recipient. +/// - `burn`, which burns the provided asset. +/// +/// No supply checks are enforced at runtime. pub fn create_unlimited_fungible_faucet( init_seed: [u8; 32], symbol: TokenSymbol, @@ -143,11 +285,33 @@ pub fn create_unlimited_fungible_faucet( ) .map_err(FungibleFaucetError::AccountError)? .into(), - _ => return Err(FungibleFaucetError::UnsupportedAuthScheme("Unsupported auth scheme".into())), + AuthScheme::NoAuth => { + return Err(FungibleFaucetError::UnsupportedAuthScheme( + "unlimited fungible faucets cannot be created with NoAuth authentication scheme" + .into(), + )); + }, + AuthScheme::Falcon512RpoMultisig { threshold: _, pub_keys: _ } => { + return Err(FungibleFaucetError::UnsupportedAuthScheme( + "unlimited fungible faucets do not support multisig authentication".into(), + )); + }, + AuthScheme::Unknown => { + return Err(FungibleFaucetError::UnsupportedAuthScheme( + "unlimited fungible faucets cannot be created with Unknown authentication scheme" + .into(), + )); + }, + AuthScheme::EcdsaK256KeccakMultisig { threshold: _, pub_keys: _ } => { + return Err(FungibleFaucetError::UnsupportedAuthScheme( + "unlimited fungible faucets do not support EcdsaK256KeccakMultisig authentication" + .into(), + )); + }, }; let faucet_component = UnlimitedFungibleFaucet::new(symbol, decimals)?; - + let account = AccountBuilder::new(init_seed) .account_type(AccountType::FungibleFaucet) .storage_mode(storage_mode) @@ -158,3 +322,121 @@ pub fn create_unlimited_fungible_faucet( Ok(account) } + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + use assert_matches::assert_matches; + use miden_protocol::account::auth::PublicKeyCommitment; + use miden_protocol::asset::FungibleAsset; + use miden_protocol::{FieldElement, ONE, Word}; + + use super::{ + AccountBuilder, + AccountStorageMode, + AccountType, + AuthScheme, + Felt, + FungibleFaucetError, + TokenSymbol, + UnlimitedFungibleFaucet, + create_unlimited_fungible_faucet, + }; + use crate::account::auth::{AuthFalcon512Rpo, AuthFalcon512RpoAcl}; + use crate::account::wallets::BasicWallet; + + #[test] + fn unlimited_faucet_contract_creation() { + let pub_key_word = Word::new([ONE; 4]); + let auth_scheme: AuthScheme = AuthScheme::Falcon512Rpo { pub_key: pub_key_word.into() }; + + let init_seed: [u8; 32] = [ + 90, 110, 209, 94, 84, 105, 250, 242, 223, 203, 216, 124, 22, 159, 14, 132, 215, 85, + 183, 204, 149, 90, 166, 68, 100, 73, 106, 168, 125, 237, 138, 16, + ]; + + let token_symbol = TokenSymbol::try_from("UNL").unwrap(); + let decimals = 8u8; + let storage_mode = AccountStorageMode::Private; + + let faucet_account = create_unlimited_fungible_faucet( + init_seed, + token_symbol, + decimals, + storage_mode, + auth_scheme, + ) + .unwrap(); + + assert!(faucet_account.is_faucet()); + assert_eq!(faucet_account.account_type(), AccountType::FungibleFaucet); + + // Check metadata slot: max_supply should be FungibleAsset::MAX_AMOUNT + assert_eq!( + faucet_account + .storage() + .get_item(UnlimitedFungibleFaucet::metadata_slot()) + .unwrap(), + [ + Felt::ZERO, + Felt::new(FungibleAsset::MAX_AMOUNT), + Felt::new(8), + token_symbol.into() + ] + .into() + ); + + // Verify the faucet can be extracted via TryFrom + let faucet_component = + UnlimitedFungibleFaucet::try_from(faucet_account.clone()).unwrap(); + assert_eq!(faucet_component.symbol(), token_symbol); + assert_eq!(faucet_component.decimals(), decimals); + assert_eq!(faucet_component.max_supply(), Felt::new(FungibleAsset::MAX_AMOUNT)); + assert_eq!(faucet_component.token_supply(), Felt::ZERO); + } + + #[test] + fn unlimited_faucet_create_from_account() { + let mock_word = Word::from([0, 1, 2, 3u32]); + let mock_public_key = PublicKeyCommitment::from(mock_word); + let mock_seed = mock_word.as_bytes(); + + let token_symbol = TokenSymbol::new("UNL").expect("invalid token symbol"); + let faucet_account = AccountBuilder::new(mock_seed) + .account_type(AccountType::FungibleFaucet) + .with_component( + UnlimitedFungibleFaucet::new(token_symbol, 8) + .expect("failed to create an unlimited fungible faucet component"), + ) + .with_auth_component(AuthFalcon512Rpo::new(mock_public_key)) + .build_existing() + .expect("failed to create faucet account"); + + let unlimited_ff = UnlimitedFungibleFaucet::try_from(faucet_account) + .expect("unlimited fungible faucet creation failed"); + assert_eq!(unlimited_ff.symbol(), token_symbol); + assert_eq!(unlimited_ff.decimals(), 8); + assert_eq!(unlimited_ff.token_supply(), Felt::ZERO); + + // invalid account: unlimited fungible faucet component is missing + let invalid_faucet_account = AccountBuilder::new(mock_seed) + .account_type(AccountType::FungibleFaucet) + .with_auth_component(AuthFalcon512Rpo::new(mock_public_key)) + .with_component(BasicWallet) + .build_existing() + .expect("failed to create account"); + + let err = UnlimitedFungibleFaucet::try_from(invalid_faucet_account) + .err() + .expect("unlimited fungible faucet creation should fail"); + assert_matches!(err, FungibleFaucetError::MissingUnlimitedFungibleFaucetInterface); + } + + #[test] + fn get_unlimited_faucet_procedures() { + let _distribute_digest = UnlimitedFungibleFaucet::distribute_digest(); + let _burn_digest = UnlimitedFungibleFaucet::burn_digest(); + } +} From 98a0d09815172f2d10de7c020f1a6e47f2217690 Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Wed, 18 Feb 2026 16:06:36 -0300 Subject: [PATCH 03/10] feat: enhance supply management with improved error handling and metadata updates --- .../asm/standards/supply/fixed_supply.masm | 26 ++++----- .../asm/standards/supply/flexible_supply.masm | 56 ++++++++++--------- .../src/account/components/mod.rs | 3 +- .../src/account/faucets/mod.rs | 4 +- .../src/account/faucets/timed_fungible.rs | 18 ++---- .../src/account/faucets/unlimited_fungible.rs | 12 ++-- .../miden-standards/src/errors/standards.rs | 22 ++++++++ 7 files changed, 79 insertions(+), 62 deletions(-) diff --git a/crates/miden-standards/asm/standards/supply/fixed_supply.masm b/crates/miden-standards/asm/standards/supply/fixed_supply.masm index c6aa426b58..b40cd115cd 100644 --- a/crates/miden-standards/asm/standards/supply/fixed_supply.masm +++ b/crates/miden-standards/asm/standards/supply/fixed_supply.masm @@ -39,7 +39,7 @@ proc get_supply_state # => [token_symbol, decimals, max_supply, token_supply] # We want: [max_supply, token_supply, decimals, token_symbol] - movdn.3 movdn.2 swap + movdn.3 movdn.2 # => [max_supply, token_supply, decimals, token_symbol] end @@ -62,23 +62,22 @@ pub proc check_supply_limit # => [max, token, dec, sym, amount, pad(10)] # Assert token_supply <= max_supply - dup dup.2 - # => [max, token, max, token, dec, sym, amount, pad(9)] - # lte [b, a] -> a <= b. + # Need [max, token] on top for lte: a=token, b=max → token <= max + dup.1 dup.1 + # => [max, token, max, token, dec, sym, amount, ...] lte assert.err=ERR_SUPPLY_EXCEEDED - # => [max, token, dec, sym, amount, pad(10)] + # => [max, token, dec, sym, amount, ...] # Calculate max_mint_amount = max_supply - token_supply dup dup.2 sub # => [max_mint, max, token, dec, sym, amount, pad(9)] # Assert amount <= max_mint_amount - dup.5 dup - # => [max_mint, amount, max_mint, max, token, dec, sym, amount, pad(8)] - swap - # => [amount, max_mint, max_mint, max, token, dec, sym, amount, pad(8)] + # Need [max_mint, amount] on top for lte: a=amount, b=max_mint → amount <= max_mint + dup dup.6 swap + # => [max_mint, amount, max_mint, max, token, dec, sym, amount, ...] lte assert.err=ERR_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY - # => [max_mint, max, token, dec, sym, amount, pad(9)] + # => [max_mint, max, token, dec, sym, amount, ...] # Clean up and return [max_mint, token, max, pad(12)] # Current indices: 0:max_mint, 1:max, 2:token, 3:dec, 4:sym, 5:amount @@ -146,10 +145,11 @@ pub proc update_supply_after_burn # => [max, token, dec, sym, amount, pad(11)] # Assert amount <= token_supply - dup.1 dup.5 - # => [amount, token, max, token, dec, sym, amount, pad(10)] + # Need [token, amount] on top for lte: a=amount, b=token → amount <= token + dup.4 dup.2 + # => [token, amount, max, token, dec, sym, amount, ...] lte assert.err=ERR_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY - # => [max, token, dec, sym, amount, pad(11)] + # => [max, token, dec, sym, amount, ...] # Calculate new_token_supply = token_supply - amount dup.1 dup.5 sub diff --git a/crates/miden-standards/asm/standards/supply/flexible_supply.masm b/crates/miden-standards/asm/standards/supply/flexible_supply.masm index a07c386a6c..262e718cd7 100644 --- a/crates/miden-standards/asm/standards/supply/flexible_supply.masm +++ b/crates/miden-standards/asm/standards/supply/flexible_supply.masm @@ -78,8 +78,9 @@ pub proc check_can_distribute # => [max_mint, token, max, end, burn, amount, pad(9)] # Assert amount <= max_mint - dup.5 dup lte assert.err=ERR_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY - # => [max_mint, token, max, end, burn, amount, pad(10)] + # Need [max_mint, amount] on top for lte: a=amount, b=max_mint → amount <= max_mint + dup dup.6 swap lte assert.err=ERR_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY + # => [max_mint, token, max, end, burn, amount, ...] # Clean up and return [max_mint, token, max, pad(12)] movup.5 drop # amount @@ -101,16 +102,17 @@ pub proc check_can_burn dup.3 if.true dup.2 # end - dup push.0 eq assert.err=ERR_BURN_NOT_ALLOWED # end block must be set for burn-only mode + dup push.0 neq assert.err=ERR_BURN_NOT_ALLOWED # end block must be set for burn-only mode exec.get_current_block_height swap gte assert.err=ERR_BURN_NOT_ALLOWED end # => [token, max, end, burn, amount, pad(11)] - # Assert amount <= token - dup dup.5 lte assert.err=ERR_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY - # => [token, max, end, burn, amount, pad(11)] + # Assert amount <= token_supply + # Need [token, amount] on top for lte: a=amount, b=token → amount <= token + dup.4 dup.1 lte assert.err=ERR_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY + # => [token, max, end, burn, amount, ...] # Clean up and return pad(16) drop drop drop drop drop @@ -130,25 +132,25 @@ pub proc update_supply_after_mint dup dup.5 add # => [new, token, max, end, burn, amount, pad(11)] - # Remove old token and amount not needed for storage - movdn.4 swap drop - # => [max, end, burn, new, amount, pad(11)] - + # Remove old token + swap drop + # => [new, max, end, burn, amount, ...] + # Reorder to [burn, end, max, new] - # Current: 0:max, 1:end, 2:burn, 3:new + movdn.3 + # => [max, end, burn, new, amount, ...] movup.2 # burn movup.2 # end swap - # => [burn, end, max, new, amount, pad(11)] - + # => [burn, end, max, new, amount, ...] + push.FLEXIBLE_SUPPLY_CONFIG_SLOT[0..2] - # => [pre, suf, burn, end, max, new, ... (18 items total)] movdn.5 movdn.5 - # => [burn, end, max, new, pre, suf, amount, pad(11)] - + # => [burn, end, max, new, pre, suf, amount, ...] + exec.native_account::set_item - # => [OLD, amount, pad(11)] - + # => [OLD, amount, ...] + dropw drop repeat.11 drop end padw padw padw padw end @@ -159,21 +161,23 @@ end #! Outputs: [pad(16)] pub proc update_supply_after_burn exec.get_flexible_supply_state - # => [token, max, end, burn, amount, pad(11)] - + # => [token, max, end, burn, amount, ...] + # Calculate new_token = token - amount dup dup.5 sub - # => [new, token, max, end, burn, amount, pad(11)] - + # => [new, token, max, end, burn, amount, ...] + # Remove old token - movdn.4 swap drop - # => [max, end, burn, new, amount, pad(11)] - + swap drop + # => [new, max, end, burn, amount, ...] + # Reorder to [burn, end, max, new] + movdn.3 + # => [max, end, burn, new, amount, ...] movup.2 # burn movup.2 # end swap - # => [burn, end, max, new, amount, pad(11)] + # => [burn, end, max, new, amount, ...] push.FLEXIBLE_SUPPLY_CONFIG_SLOT[0..2] movdn.5 movdn.5 diff --git a/crates/miden-standards/src/account/components/mod.rs b/crates/miden-standards/src/account/components/mod.rs index 20acd9f54b..30b7467c91 100644 --- a/crates/miden-standards/src/account/components/mod.rs +++ b/crates/miden-standards/src/account/components/mod.rs @@ -114,7 +114,8 @@ static UNLIMITED_FUNGIBLE_FAUCET_LIBRARY: LazyLock = LazyLock::new(|| { env!("OUT_DIR"), "/assets/account_components/faucets/unlimited_fungible_faucet.masl" )); - Library::read_from_bytes(bytes).expect("Shipped Unlimited Fungible Faucet library is well-formed") + Library::read_from_bytes(bytes) + .expect("Shipped Unlimited Fungible Faucet library is well-formed") }); // Initialize the Timed Fungible Faucet library only once. diff --git a/crates/miden-standards/src/account/faucets/mod.rs b/crates/miden-standards/src/account/faucets/mod.rs index bc32a9742d..2b00d8ec5c 100644 --- a/crates/miden-standards/src/account/faucets/mod.rs +++ b/crates/miden-standards/src/account/faucets/mod.rs @@ -6,14 +6,14 @@ use thiserror::Error; mod basic_fungible; mod network_fungible; -mod token_metadata; mod timed_fungible; +mod token_metadata; mod unlimited_fungible; pub use basic_fungible::{BasicFungibleFaucet, create_basic_fungible_faucet}; pub use network_fungible::{NetworkFungibleFaucet, create_network_fungible_faucet}; -pub use token_metadata::TokenMetadata; pub use timed_fungible::{TimedFungibleFaucet, create_timed_fungible_faucet}; +pub use token_metadata::TokenMetadata; pub use unlimited_fungible::{UnlimitedFungibleFaucet, create_unlimited_fungible_faucet}; // FUNGIBLE FAUCET ERROR diff --git a/crates/miden-standards/src/account/faucets/timed_fungible.rs b/crates/miden-standards/src/account/faucets/timed_fungible.rs index ff8626031f..ba19ac982f 100644 --- a/crates/miden-standards/src/account/faucets/timed_fungible.rs +++ b/crates/miden-standards/src/account/faucets/timed_fungible.rs @@ -72,8 +72,8 @@ procedure_digest!( /// ## Storage Layout /// /// - [`Self::metadata_slot`]: Stores [`TokenMetadata`]. -/// - [`Self::supply_config_slot`]: Stores supply config -/// `[token_supply, max_supply, distribution_end, burn_only]`. +/// - [`Self::supply_config_slot`]: Stores supply config `[token_supply, max_supply, +/// distribution_end, burn_only]`. /// /// [builder]: crate::code_builder::CodeBuilder pub struct TimedFungibleFaucet { @@ -115,19 +115,11 @@ impl TimedFungibleFaucet { burn_only: bool, ) -> Result { let metadata = TokenMetadata::new(symbol, decimals, max_supply)?; - Ok(Self { - metadata, - distribution_end, - burn_only, - }) + Ok(Self { metadata, distribution_end, burn_only }) } /// Creates a new [`TimedFungibleFaucet`] component from the given [`TokenMetadata`]. - pub fn from_metadata( - metadata: TokenMetadata, - distribution_end: u32, - burn_only: bool, - ) -> Self { + pub fn from_metadata(metadata: TokenMetadata, distribution_end: u32, burn_only: bool) -> Self { Self { metadata, distribution_end, burn_only } } @@ -432,7 +424,7 @@ mod tests { TokenSymbol, create_timed_fungible_faucet, }; - use crate::account::auth::{AuthFalcon512Rpo, AuthFalcon512RpoAcl}; + use crate::account::auth::AuthFalcon512Rpo; use crate::account::wallets::BasicWallet; #[test] diff --git a/crates/miden-standards/src/account/faucets/unlimited_fungible.rs b/crates/miden-standards/src/account/faucets/unlimited_fungible.rs index b5cb10bf1f..375d6e8098 100644 --- a/crates/miden-standards/src/account/faucets/unlimited_fungible.rs +++ b/crates/miden-standards/src/account/faucets/unlimited_fungible.rs @@ -89,7 +89,8 @@ impl UnlimitedFungibleFaucet { /// Creates a new [`UnlimitedFungibleFaucet`] component. /// - /// The max supply is set to [`FungibleAsset::MAX_AMOUNT`](miden_protocol::asset::FungibleAsset::MAX_AMOUNT) + /// The max supply is set to + /// [`FungibleAsset::MAX_AMOUNT`](miden_protocol::asset::FungibleAsset::MAX_AMOUNT) /// since this faucet does not enforce supply limits at runtime. /// /// # Errors @@ -219,9 +220,7 @@ impl From for AccountComponent { .expect("storage schema should be valid"); let metadata = AccountComponentMetadata::new(UnlimitedFungibleFaucet::NAME) - .with_description( - "Unlimited fungible faucet component for minting and burning tokens", - ) + .with_description("Unlimited fungible faucet component for minting and burning tokens") .with_supported_type(AccountType::FungibleFaucet) .with_storage_schema(storage_schema); @@ -344,7 +343,7 @@ mod tests { UnlimitedFungibleFaucet, create_unlimited_fungible_faucet, }; - use crate::account::auth::{AuthFalcon512Rpo, AuthFalcon512RpoAcl}; + use crate::account::auth::AuthFalcon512Rpo; use crate::account::wallets::BasicWallet; #[test] @@ -389,8 +388,7 @@ mod tests { ); // Verify the faucet can be extracted via TryFrom - let faucet_component = - UnlimitedFungibleFaucet::try_from(faucet_account.clone()).unwrap(); + let faucet_component = UnlimitedFungibleFaucet::try_from(faucet_account.clone()).unwrap(); assert_eq!(faucet_component.symbol(), token_symbol); assert_eq!(faucet_component.decimals(), decimals); assert_eq!(faucet_component.max_supply(), Felt::new(FungibleAsset::MAX_AMOUNT)); diff --git a/crates/miden-standards/src/errors/standards.rs b/crates/miden-standards/src/errors/standards.rs index f16cc23ed4..76c6f4ab31 100644 --- a/crates/miden-standards/src/errors/standards.rs +++ b/crates/miden-standards/src/errors/standards.rs @@ -12,6 +12,17 @@ use miden_protocol::errors::MasmError; /// Error Message: "burn requires exactly 1 note asset" pub const ERR_BASIC_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS: MasmError = MasmError::from_static_str("burn requires exactly 1 note asset"); +/// Error Message: "asset amount to burn exceeds the existing token supply" +pub const ERR_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY: MasmError = MasmError::from_static_str("asset amount to burn exceeds the existing token supply"); +/// Error Message: "burn operations are not allowed during distribution period" +pub const ERR_BURN_NOT_ALLOWED: MasmError = MasmError::from_static_str("burn operations are not allowed during distribution period"); + +/// Error Message: "token_supply plus the amount passed to distribute would exceed the maximum supply" +pub const ERR_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY: MasmError = MasmError::from_static_str("token_supply plus the amount passed to distribute would exceed the maximum supply"); + +/// Error Message: "distribution period has ended" +pub const ERR_DISTRIBUTION_PERIOD_ENDED: MasmError = MasmError::from_static_str("distribution period has ended"); + /// Error Message: "asset amount to burn exceeds the existing token supply" pub const ERR_FAUCET_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY: MasmError = MasmError::from_static_str("asset amount to burn exceeds the existing token supply"); @@ -53,13 +64,24 @@ pub const ERR_P2ID_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS: MasmError = MasmError::fr /// Error Message: "note sender is not the owner" pub const ERR_SENDER_NOT_OWNER: MasmError = MasmError::from_static_str("note sender is not the owner"); +/// Error Message: "max supply exceeds maximum representable fungible asset amount" +pub const ERR_SUPPLY_CAP_INVALID: MasmError = MasmError::from_static_str("max supply exceeds maximum representable fungible asset amount"); +/// Error Message: "token supply exceeds max supply" +pub const ERR_SUPPLY_EXCEEDED: MasmError = MasmError::from_static_str("token supply exceeds max supply"); + /// Error Message: "SWAP script expects exactly 16 note storage items" pub const ERR_SWAP_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS: MasmError = MasmError::from_static_str("SWAP script expects exactly 16 note storage items"); /// Error Message: "SWAP script requires exactly 1 note asset" pub const ERR_SWAP_WRONG_NUMBER_OF_ASSETS: MasmError = MasmError::from_static_str("SWAP script requires exactly 1 note asset"); +/// Error Message: "burn requires exactly 1 note asset" +pub const ERR_TIMED_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS: MasmError = MasmError::from_static_str("burn requires exactly 1 note asset"); + /// Error Message: "failed to approve multisig transaction as it was already executed" pub const ERR_TX_ALREADY_EXECUTED: MasmError = MasmError::from_static_str("failed to approve multisig transaction as it was already executed"); +/// Error Message: "burn requires exactly 1 note asset" +pub const ERR_UNLIMITED_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS: MasmError = MasmError::from_static_str("burn requires exactly 1 note asset"); + /// Error Message: "number of approvers or threshold must not be zero" pub const ERR_ZERO_IN_MULTISIG_CONFIG: MasmError = MasmError::from_static_str("number of approvers or threshold must not be zero"); From 135d5524ce88ad966bc6fdf4ef87d5db60c727f2 Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Wed, 18 Feb 2026 21:21:28 -0300 Subject: [PATCH 04/10] Refactor supply management in fungible faucet contracts - Replaced the flexible_supply component with supply_limits in the timed_fungible faucet contract to unify supply management. - Removed the fixed_supply and flexible_supply components as they are no longer needed. - Updated the timed_fungible faucet implementation to utilize the new supply_limits component for minting and burning operations. - Adjusted error handling and supply checks to align with the new supply_limits logic. - Updated references to the supply configuration storage slot in the timed_fungible and unlimited_fungible faucet implementations. - Cleaned up unused error messages related to supply cap validation. --- .../asm/standards/faucets/fixed_fungible.masm | 207 -------------- .../asm/standards/faucets/mod.masm | 258 ++++++++++-------- .../asm/standards/faucets/timed_fungible.masm | 155 +++-------- .../asm/standards/supply/fixed_supply.masm | 194 ------------- .../asm/standards/supply/flexible_supply.masm | 199 -------------- .../asm/standards/supply/supply_limits.masm | 188 +++++++++++++ .../src/account/faucets/timed_fungible.rs | 6 +- .../src/account/faucets/token_metadata.rs | 3 + .../src/account/faucets/unlimited_fungible.rs | 4 +- .../miden-standards/src/errors/standards.rs | 2 - 10 files changed, 382 insertions(+), 834 deletions(-) delete mode 100644 crates/miden-standards/asm/standards/faucets/fixed_fungible.masm delete mode 100644 crates/miden-standards/asm/standards/supply/fixed_supply.masm delete mode 100644 crates/miden-standards/asm/standards/supply/flexible_supply.masm create mode 100644 crates/miden-standards/asm/standards/supply/supply_limits.masm diff --git a/crates/miden-standards/asm/standards/faucets/fixed_fungible.masm b/crates/miden-standards/asm/standards/faucets/fixed_fungible.masm deleted file mode 100644 index 42f87f7f00..0000000000 --- a/crates/miden-standards/asm/standards/faucets/fixed_fungible.masm +++ /dev/null @@ -1,207 +0,0 @@ -# FIXED SUPPLY FUNGIBLE FAUCET CONTRACT -# -# A fungible faucet with fixed supply management using the supply::fixed_supply component. -# This faucet enforces a maximum supply cap and tracks total minted tokens. -# ================================================================================================= - -use miden::protocol::active_account -use miden::protocol::active_note -use miden::protocol::faucet -use miden::protocol::native_account -use miden::protocol::output_note -use miden::standards::supply::fixed_supply - -# CONSTANTS -# ================================================================================================= - -const PRIVATE_NOTE=2 - -# ERRORS -# ================================================================================================= - -const ERR_BASIC_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS="burn requires exactly 1 note asset" - -# CONSTANTS -# ================================================================================================= - -# The local memory address at which the metadata slot content is stored. -const METADATA_SLOT_LOCAL=0 - -# The standard slot where fungible faucet metadata like token symbol or decimals are stored. -# Layout: [token_supply, max_supply, decimals, token_symbol] -# Note: This is kept for backward compatibility with existing metadata structure -const METADATA_SLOT=word("miden::standards::fungible_faucets::metadata") - -# PROCEDURES -# ================================================================================================= - -#! Distributes freshly minted fungible assets to the provided recipient by creating a note. -#! -#! This procedure uses the fixed_supply component to enforce supply limits. -#! -#! Inputs: [amount, tag, note_type, RECIPIENT] -#! Outputs: [note_idx] -#! -#! Where: -#! - amount is the amount to be minted and sent. -#! - tag is the tag to be included in the note. -#! - note_type is the type of the note that holds the asset. -#! - RECIPIENT is the recipient of the asset, i.e., -#! hash(hash(hash(serial_num, [0; 4]), script_root), storage_commitment). -#! - note_idx is the index of the created note. -#! -#! Panics if: -#! - the transaction is being executed against an account that is not a fungible asset faucet. -#! - the supply constraints are violated (checked by fixed_supply component). -#! -#! Invocation: exec -@locals(8) -pub proc distribute - # Store inputs to locals - # --------------------------------------------------------------------------------------------- - # Stack: [amount, tag, note_type, RECIPIENT] - - loc_store.4 - # => [tag, note_type, RECIPIENT] - - loc_store.5 - # => [note_type, RECIPIENT] - - loc_store.6 - # => [RECIPIENT] - - # Store RECIPIENT at index 0 (aligned) - # loc_storew_be reads the word but does not pop it - loc_storew_be.0 - # => [RECIPIENT] - - dropw - # => [] - - # Check supply limits using the fixed_supply component - # --------------------------------------------------------------------------------------------- - - loc_load.4 - # => [amount] - - # Prepare stack for check_supply_limit: [amount, pad(14)] - padw padw padw push.0 push.0 - # => [amount, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - - exec.fixed_supply::check_supply_limit - # => [max_mint_amount, token_supply, max_supply, pad(11)] - - # The check passed, clean up the stack - drop drop drop - repeat.11 - drop - end - # => [] - - # Mint the asset - # --------------------------------------------------------------------------------------------- - - exec.faucet::create_fungible_asset - # => [ASSET] - - exec.faucet::mint - # => [ASSET] - - # Update supply using fixed_supply component - # --------------------------------------------------------------------------------------------- - - # Extract amount from ASSET for the update (ASSET = [faucet_id_prefix, faucet_id_suffix, 0, amount]) - dup.3 - # => [amount, ASSET] - - # Prepare for update_supply_after_mint: [amount, pad(15)] - padw padw padw push.0 push.0 push.0 - # => [amount, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ASSET] - - exec.fixed_supply::update_supply_after_mint - # => [pad(16), ASSET] - - dropw dropw dropw dropw - # => [ASSET] - - # Create the note with the asset - # --------------------------------------------------------------------------------------------- - - # Restore RECIPIENT - padw loc_loadw_be.0 - # => [RECIPIENT, ASSET] - - # Restore note_type - loc_load.6 - # => [note_type, RECIPIENT, ASSET] - - # Restore tag - loc_load.5 - # => [tag, note_type, RECIPIENT, ASSET] - - # Create the note - exec.output_note::create - # => [note_idx, ASSET] - - # Add asset to the note - dup movdn.5 movdn.5 - # => [ASSET, note_idx, note_idx] - - exec.output_note::add_asset - # => [note_idx] -end - -#! Burns the fungible asset from the active note. -#! -#! Burning the asset removes it from circulation and reduces the token_supply by the asset's amount. -#! This procedure uses the fixed_supply component to update supply tracking. -#! -#! Inputs: [pad(16)] -#! Outputs: [pad(16)] -#! -#! Panics if: -#! - the procedure is not called from a note context. -#! - the note does not contain exactly one asset. -#! - the transaction is executed against an account which is not a fungible asset faucet. -#! - the transaction is executed against a faucet which is not the origin of the specified asset. -#! - the amount about to be burned is greater than the token_supply of the faucet. -#! -#! Invocation: call -pub proc burn - # Get the asset from the note. - # --------------------------------------------------------------------------------------------- - - # this will fail if not called from a note context. - push.0 exec.active_note::get_assets - # => [num_assets, dest_ptr, pad(16)] - - # Verify we have exactly one asset - assert.err=ERR_BASIC_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS - # => [dest_ptr, pad(16)] - - mem_loadw_be - # => [ASSET, pad(16)] - # => [[faucet_id_prefix, faucet_id_suffix, 0, amount], pad(16)] - - # Burn the asset from the transaction vault - # --------------------------------------------------------------------------------------------- - - dup.3 movdn.4 - # => [ASSET, amount, pad(16)] - - # burn the asset - exec.faucet::burn dropw - # => [amount, pad(16)] - - # Update supply using fixed_supply component - # --------------------------------------------------------------------------------------------- - - # Prepare stack for update_supply_after_burn: [amount, pad(15)] - # Stack is currently [amount, pad(16)] - # We need to drop one pad element to get pad(15) - movup.15 drop - # => [amount, pad(15)] - - exec.fixed_supply::update_supply_after_burn - # => [pad(16)] -end diff --git a/crates/miden-standards/asm/standards/faucets/mod.masm b/crates/miden-standards/asm/standards/faucets/mod.masm index 7191108901..5451bb9655 100644 --- a/crates/miden-standards/asm/standards/faucets/mod.masm +++ b/crates/miden-standards/asm/standards/faucets/mod.masm @@ -4,7 +4,6 @@ use miden::protocol::faucet use miden::protocol::native_account use miden::protocol::output_note use ::miden::protocol::asset::FUNGIBLE_ASSET_MAX_AMOUNT -use miden::standards::supply::fixed_supply # CONSTANTS # ================================================================================================= @@ -14,10 +13,12 @@ const PRIVATE_NOTE=2 # ERRORS # ================================================================================================= -# Retained for backward compatibility of error constants in Rust const ERR_FUNGIBLE_ASSET_TOKEN_SUPPLY_EXCEEDS_MAX_SUPPLY="token supply exceeds max supply" + const ERR_FUNGIBLE_ASSET_MAX_SUPPLY_EXCEEDS_FUNGIBLE_ASSET_MAX_AMOUNT="max supply exceeds maximum representable fungible asset amount" + const ERR_FUNGIBLE_ASSET_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY="token_supply plus the amount passed to distribute would exceed the maximum supply" + const ERR_FAUCET_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY="asset amount to burn exceeds the existing token supply" const ERR_BASIC_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS="burn requires exactly 1 note asset" @@ -32,166 +33,195 @@ const METADATA_SLOT_LOCAL=0 # Layout: [token_supply, max_supply, decimals, token_symbol] const METADATA_SLOT=word("miden::standards::fungible_faucets::metadata") -# PROCEDURES -# ================================================================================================= - #! Distributes freshly minted fungible assets to the provided recipient by creating a note. #! -#! This procedure uses the fixed_supply component to enforce supply limits. -#! #! Inputs: [amount, tag, note_type, RECIPIENT] #! Outputs: [note_idx] #! -#! ... +#! Where: +#! - amount is the amount to be minted and sent. +#! - tag is the tag to be included in the note. +#! - note_type is the type of the note that holds the asset. +#! - RECIPIENT is the recipient of the asset, i.e., +#! hash(hash(hash(serial_num, [0; 4]), script_root), storage_commitment). +#! - note_idx is the index of the created note. #! #! Panics if: #! - the transaction is being executed against an account that is not a fungible asset faucet. -#! - the supply limits are exceeded. +#! - the token supply exceeds the maximum supply. +#! - the maximum supply exceeds the maximum representable fungible asset amount. +#! - the token supply after minting is greater than the maximum allowed supply. #! #! Invocation: exec -@locals(8) +@locals(4) pub proc distribute - # Store inputs to locals + # Get the configured max supply and the token supply (= current supply). # --------------------------------------------------------------------------------------------- - # Stack: [amount, tag, note_type, RECIPIENT] - - loc_store.4 - # => [tag, note_type, RECIPIENT] - - loc_store.5 - # => [note_type, RECIPIENT] - - loc_store.6 - # => [RECIPIENT] - - # Store RECIPIENT at index 0 (aligned) - loc_storew_be.0 - # => [RECIPIENT] - - dropw - # => [] - - # Check supply limits using the fixed_supply component + + push.METADATA_SLOT[0..2] exec.active_account::get_item + # => [token_symbol, decimals, max_supply, token_supply, amount, tag, note_type, RECIPIENT] + + # store a copy of the current slot content for the token_supply update later + loc_storew_be.METADATA_SLOT_LOCAL + drop drop + # => [max_supply, token_supply, amount, tag, note_type, RECIPIENT] + + # Assert that minting does not violate any supply constraints. + # + # To make sure we cannot mint more than intended, we need to check: + # 1) (max_supply - token_supply) <= max_supply, i.e. the subtraction does not wrap around + # 2) amount + token_supply does not exceed max_supply + # 3) amount + token_supply is less than FUNGIBLE_ASSET_MAX_AMOUNT + # + # This is done with the following concrete assertions: + # - assert token_supply <= max_supply which ensures 1) + # - assert max_supply <= FUNGIBLE_ASSET_MAX_AMOUNT to help ensure 3) + # - assert amount <= max_mint_amount to ensure 2) as well as 3) + # - this ensures 3) because token_supply + max_mint_amount at most ends up being equal to + # max_supply and we already asserted that max_supply does not exceed + # FUNGIBLE_ASSET_MAX_AMOUNT # --------------------------------------------------------------------------------------------- - - loc_load.4 - # => [amount] - - # Prepare stack for check_supply_limit: [amount, pad(14)] - padw padw padw push.0 push.0 - # => [amount, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - - exec.fixed_supply::check_supply_limit - # => [max_mint_amount, token_supply, max_supply, pad(11)] - - # The check passed, clean up the stack - drop drop drop - repeat.12 - drop - end - # => [] - - # Mint the asset + + dup.1 dup.1 + # => [max_supply, token_supply, max_supply, token_supply, amount, tag, note_type, RECIPIENT] + + # assert that token_supply <= max_supply + lte assert.err=ERR_FUNGIBLE_ASSET_TOKEN_SUPPLY_EXCEEDS_MAX_SUPPLY + # => [max_supply, token_supply, amount, tag, note_type, RECIPIENT] + + # assert max_supply <= FUNGIBLE_ASSET_MAX_AMOUNT + dup lte.FUNGIBLE_ASSET_MAX_AMOUNT + assert.err=ERR_FUNGIBLE_ASSET_MAX_SUPPLY_EXCEEDS_FUNGIBLE_ASSET_MAX_AMOUNT + # => [max_supply, token_supply, amount, tag, note_type, RECIPIENT] + + dup.2 swap dup.2 + # => [token_supply, max_supply, amount, token_supply, amount, tag, note_type, RECIPIENT] + + # compute maximum amount that can be minted, max_mint_amount = max_supply - token_supply + sub + # => [max_mint_amount, amount, token_supply, amount, tag, note_type, RECIPIENT] + + # assert amount <= max_mint_amount + lte assert.err=ERR_FUNGIBLE_ASSET_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY + # => [token_supply, amount, tag, note_type, RECIPIENT] + + # Compute the new token_supply and update in storage. + # --------------------------------------------------------------------------------------------- + + dup.1 add + # => [new_token_supply, amount, tag, note_type, RECIPIENT] + + padw loc_loadw_be.METADATA_SLOT_LOCAL + # => [[token_symbol, decimals, max_supply, token_supply], new_token_supply, amount, tag, note_type, RECIPIENT] + + movup.3 drop + # => [[token_symbol, decimals, max_supply, new_token_supply], amount, tag, note_type, RECIPIENT] + + # update the metadata slot with the new supply + push.METADATA_SLOT[0..2] exec.native_account::set_item dropw + # => [amount, tag, note_type, RECIPIENT] + + # Mint the asset. # --------------------------------------------------------------------------------------------- - - loc_load.4 + + # creating the asset exec.faucet::create_fungible_asset - # => [ASSET] - + # => [ASSET, tag, note_type, RECIPIENT] + + # mint the asset; this is needed to satisfy asset preservation logic. + # this ensures that the asset's faucet ID matches the native account's ID. + # this is ensured because create_fungible_asset creates the asset with the native account's ID exec.faucet::mint - # => [ASSET] - - # Update supply using fixed_supply component - # --------------------------------------------------------------------------------------------- - # Extract amount from ASSET for the update - # ASSET layout: [id_prefix, id_suffix, 0, amount] on stack - dup.3 - # => [amount, ASSET] - - # Prepare for update_supply_after_mint: [amount, pad(15)] - padw padw padw push.0 push.0 push.0 - # => [amount, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ASSET] - - exec.fixed_supply::update_supply_after_mint - # => [pad(16), ASSET] - - # Supply module now returns pad(16), so we drop it - dropw dropw dropw dropw - # => [ASSET] - - # Create the note with the asset - # --------------------------------------------------------------------------------------------- - - # Restore RECIPIENT - padw loc_loadw_be.0 - # => [RECIPIENT, ASSET] - - # Restore note_type - loc_load.6 - # => [note_type, RECIPIENT, ASSET] - - # Restore tag - loc_load.5 + # => [ASSET, tag, note_type, RECIPIENT] + + movdn.9 movdn.9 movdn.9 movdn.9 # => [tag, note_type, RECIPIENT, ASSET] - - # Create the note + + # Create a new note with the asset. + # --------------------------------------------------------------------------------------------- + + # create a note exec.output_note::create # => [note_idx, ASSET] - - # Add asset to the note + + # load the ASSET and add it to the note dup movdn.5 movdn.5 # => [ASSET, note_idx, note_idx] - + exec.output_note::add_asset # => [note_idx] end #! Burns the fungible asset from the active note. #! -#! ... +#! Burning the asset removes it from circulation and reduces the token_supply by the asset's amount. +#! +#! This procedure retrieves the asset from the active note and burns it. The note must contain +#! exactly one asset, which must be a fungible asset issued by this faucet. +#! +#! Inputs: [pad(16)] +#! Outputs: [pad(16)] +#! +#! Panics if: +#! - the procedure is not called from a note context (active_note::get_assets will fail). +#! - the note does not contain exactly one asset. +#! - the transaction is executed against an account which is not a fungible asset faucet. +#! - the transaction is executed against a faucet which is not the origin of the specified asset. +#! - the amount about to be burned is greater than the token_supply of the faucet. #! #! Invocation: call pub proc burn # Get the asset from the note. # --------------------------------------------------------------------------------------------- - + # this will fail if not called from a note context. push.0 exec.active_note::get_assets # => [num_assets, dest_ptr, pad(16)] - + # Verify we have exactly one asset assert.err=ERR_BASIC_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS # => [dest_ptr, pad(16)] - + mem_loadw_be # => [ASSET, pad(16)] # => [[faucet_id_prefix, faucet_id_suffix, 0, amount], pad(16)] - + # Burn the asset from the transaction vault # --------------------------------------------------------------------------------------------- - - # ASSET layout: [id_prefix, id_suffix, 0, amount] on stack + dup.3 movdn.4 # => [ASSET, amount, pad(16)] - + # burn the asset + # this ensures we only burn assets that were issued by this faucet (which implies they are + # fungible) exec.faucet::burn dropw # => [amount, pad(16)] - - # Update supply using fixed_supply component + + # Subtract burnt amount from current token_supply in storage. # --------------------------------------------------------------------------------------------- - - # Prepare stack for update_supply_after_burn: [amount, pad(15)] - # Current stack is [amount, pad(16)]. - - # Drop one zero to get pad(15) - swap drop - # => [amount, pad(15)] - - exec.fixed_supply::update_supply_after_burn + + push.METADATA_SLOT[0..2] exec.active_account::get_item + # => [token_symbol, decimals, max_supply, token_supply, amount, pad(16)] + + dup.4 dup.4 + # => [token_supply, amount, token_symbol, decimals, max_supply, token_supply, amount, pad(16)] + + # assert that amount <= token_supply + lte assert.err=ERR_FAUCET_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY + # => [token_symbol, decimals, max_supply, token_supply, amount, pad(16)] + + movup.3 movup.4 + # => [amount, token_supply, token_symbol, decimals, max_supply, pad(16)] + + # compute new_token_supply = token_supply - amount + sub + # => [new_token_supply, token_symbol, decimals, max_supply, pad(16)] + + movdn.3 + # => [token_symbol, decimals, max_supply, new_token_supply, pad(16)] + + # update the metadata slot with the new supply + push.METADATA_SLOT[0..2] exec.native_account::set_item dropw # => [pad(16)] - - # Clean the stack - dropw dropw dropw dropw - # => [] end diff --git a/crates/miden-standards/asm/standards/faucets/timed_fungible.masm b/crates/miden-standards/asm/standards/faucets/timed_fungible.masm index 72a45c01c3..406c7bc2f8 100644 --- a/crates/miden-standards/asm/standards/faucets/timed_fungible.masm +++ b/crates/miden-standards/asm/standards/faucets/timed_fungible.masm @@ -1,6 +1,6 @@ # TIMED SUPPLY FUNGIBLE FAUCET CONTRACT # -# A fungible faucet with time-based supply management using the supply::flexible_supply component. +# A fungible faucet with time-based supply management using the supply::supply_limits component. # This faucet enforces supply limits during a distribution period and optionally allows # burn-only operations after the period ends. # ================================================================================================= @@ -8,7 +8,7 @@ use miden::protocol::active_note use miden::protocol::faucet use miden::protocol::output_note -use miden::standards::supply::flexible_supply +use miden::standards::supply::supply_limits # CONSTANTS # ================================================================================================= @@ -25,7 +25,8 @@ const ERR_TIMED_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS="burn requires exactly 1 no #! Distributes freshly minted fungible assets to the provided recipient by creating a note. #! -#! This procedure uses the flexible_supply component to enforce time-based supply limits. +#! This procedure uses the supply_limits component to enforce time-based supply limits +#! and update the supply counter in a single call. #! #! Inputs: [amount, tag, note_type, RECIPIENT] #! Outputs: [note_idx] @@ -41,7 +42,7 @@ const ERR_TIMED_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS="burn requires exactly 1 no #! Panics if: #! - the transaction is being executed against an account that is not a fungible asset faucet. #! - the distribution period has ended. -#! - the supply constraints are violated (checked by flexible_supply component). +#! - the supply constraints are violated. #! #! Invocation: exec @locals(8) @@ -49,95 +50,63 @@ pub proc distribute # Store inputs to locals # --------------------------------------------------------------------------------------------- # Stack: [amount, tag, note_type, RECIPIENT] - + loc_store.4 # => [tag, note_type, RECIPIENT] - + loc_store.5 # => [note_type, RECIPIENT] - + loc_store.6 # => [RECIPIENT] - - # Store RECIPIENT at index 0 (aligned) - # loc_storew_be reads the word but does not pop it + loc_storew_be.0 # => [RECIPIENT] - + dropw # => [] - - # Check if distribution is allowed (time + supply constraints) + + # Check supply constraints and update counter # --------------------------------------------------------------------------------------------- - + + padw padw padw push.0.0.0 loc_load.4 - # => [amount] - - # Prepare stack for check_can_distribute: [amount, pad(14)] - padw padw padw push.0.0 - # => [amount, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - - exec.flexible_supply::check_can_distribute - # => [max_mint_amount, token_supply, max_supply, pad(11)] - - # The check passed, clean up the stack - drop drop drop - repeat.12 - drop - end + # => [amount, pad(15)] + + exec.supply_limits::mint + # => [pad(16)] + + dropw dropw dropw dropw # => [] - + # Mint the asset # --------------------------------------------------------------------------------------------- - + loc_load.4 exec.faucet::create_fungible_asset # => [ASSET] - + exec.faucet::mint # => [ASSET] - - # Update supply using flexible_supply component - # --------------------------------------------------------------------------------------------- - - # Extract amount from ASSET for the update - # ASSET layout: [id_prefix, id_suffix, 0, amount] on stack - dup.3 - # => [amount, ASSET] - - # Prepare for update_supply_after_mint: [amount, pad(15)] - padw padw padw push.0 push.0 push.0 - # => [amount, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ASSET] - - exec.flexible_supply::update_supply_after_mint - # => [pad(16), ASSET] - - dropw dropw dropw dropw - # => [ASSET] - + # Create the note with the asset # --------------------------------------------------------------------------------------------- - - # Restore RECIPIENT + padw loc_loadw_be.0 # => [RECIPIENT, ASSET] - - # Restore note_type + loc_load.6 # => [note_type, RECIPIENT, ASSET] - - # Restore tag + loc_load.5 # => [tag, note_type, RECIPIENT, ASSET] - - # Create the note + exec.output_note::create # => [note_idx, ASSET] - - # Add asset to the note + dup movdn.5 movdn.5 # => [ASSET, note_idx, note_idx] - + exec.output_note::add_asset # => [note_idx] end @@ -145,7 +114,7 @@ end #! Burns the fungible asset from the active note. #! #! Burning the asset removes it from circulation and reduces the token_supply. -#! This procedure uses the flexible_supply component which may enforce time-based +#! This procedure uses the supply_limits component which may enforce time-based #! restrictions (e.g., burn-only mode after distribution period). #! #! Inputs: [pad(16)] @@ -160,69 +129,33 @@ end #! #! Invocation: call pub proc burn - # Get the asset from the note. + # Get the asset from the note # --------------------------------------------------------------------------------------------- - - # this will fail if not called from a note context. + push.0 exec.active_note::get_assets # => [num_assets, dest_ptr, pad(16)] - - # Verify we have exactly one asset + assert.err=ERR_TIMED_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS # => [dest_ptr, pad(16)] - + mem_loadw_be # => [ASSET, pad(16)] - # => [[faucet_id_prefix, faucet_id_suffix, 0, amount], pad(16)] - - # Duplicate amount and ASSET for the check - dup.3 dup.3 - # => [amount, amount, ASSET, pad(16)] - # Wait, dup.3 gets amount. dup.3 again gets amount? No, dup.3 of [amount, A, B, C] is B. - # Stack: [0, 1, 2, 3]. - # dup.3 -> index 3. - # [faucet_id_prefix, faucet_id_suffix, 0, amount]. - # dup.3 returns amount. - - # We need to preserve ASSET on stack bottom for later use. - # Stack after mem_loadw_be: [ASSET, pad(16)] - # ASSET = [prefix, suffix, 0, amount] - - dup.3 - # => [amount, ASSET, pad(16)] - - # Check if burn is allowed (time constraints) - # --------------------------------------------------------------------------------------------- - - # Prepare stack for check_can_burn: [amount, pad(15)] - padw padw padw push.0 push.0 push.0 - # => [amount, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ASSET, pad(16)] - - exec.flexible_supply::check_can_burn - # => [pad(16), ASSET, pad(16)] - - # Supply module returns pad(16) - dropw dropw dropw dropw - # => [ASSET, pad(16)] - - # Burn the asset from the transaction vault + + # Save amount for supply update, then burn the asset # --------------------------------------------------------------------------------------------- - - # ASSET layout: [id_prefix, id_suffix, 0, amount] on stack + dup.3 movdn.4 # => [ASSET, amount, pad(16)] - - # burn the asset + exec.faucet::burn dropw # => [amount, pad(16)] - - # Update supply using flexible_supply component + + # Check burn constraints and update supply counter # --------------------------------------------------------------------------------------------- - - # Prepare stack for update_supply_after_burn: [amount, pad(15)] + swap drop # => [amount, pad(15)] - - exec.flexible_supply::update_supply_after_burn + + exec.supply_limits::burn # => [pad(16)] end diff --git a/crates/miden-standards/asm/standards/supply/fixed_supply.masm b/crates/miden-standards/asm/standards/supply/fixed_supply.masm deleted file mode 100644 index b40cd115cd..0000000000 --- a/crates/miden-standards/asm/standards/supply/fixed_supply.masm +++ /dev/null @@ -1,194 +0,0 @@ -# miden::standards::supply::fixed_supply -# -# Provides fixed supply management functionality for faucets. -# This component enforces a maximum supply cap and tracks total minted tokens. - -use miden::protocol::active_account -use miden::protocol::native_account - -# CONSTANTS -# ================================================================================================ - -# The slot where the supply configuration is stored. -# Layout: [token_supply, max_supply, decimals, token_symbol] -const SUPPLY_CONFIG_SLOT = word("miden::standards::fungible_faucets::metadata") - -# ERRORS -# ================================================================================================ - -const ERR_SUPPLY_EXCEEDED = "token supply exceeds max supply" -const ERR_SUPPLY_CAP_INVALID = "max supply exceeds maximum representable fungible asset amount" -const ERR_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY = "token_supply plus the amount passed to distribute would exceed the maximum supply" -const ERR_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY = "asset amount to burn exceeds the existing token supply" - -# INTERNAL PROCEDURES -# ================================================================================================ - -#! Returns the current supply state and metadata from storage. -#! -#! Inputs: [] -#! Outputs: [max_supply, token_supply, decimals, token_symbol] -#! -#! Where: -#! - max_supply is the maximum allowed supply. -#! - token_supply is the current total supply of tokens minted. -#! - decimals is the number of decimals (preserved). -#! - token_symbol is the token symbol (preserved). -proc get_supply_state - push.SUPPLY_CONFIG_SLOT[0..2] exec.active_account::get_item - # => [token_symbol, decimals, max_supply, token_supply] - - # We want: [max_supply, token_supply, decimals, token_symbol] - movdn.3 movdn.2 - # => [max_supply, token_supply, decimals, token_symbol] -end - -# PUBLIC INTERFACE -# ================================================================================================ - -#! Checks if the given amount can be distributed without exceeding the max supply. -#! -#! Inputs: [amount, pad(14)] -#! Outputs: [max_mint_amount, token_supply, max_supply, pad(12)] -#! -#! Where: -#! - amount is the amount to be minted. -#! - max_mint_amount is the maximum amount that can be minted. -#! - token_supply is the current total supply. -#! - max_supply is the maximum allowed supply. -pub proc check_supply_limit - # Get current supply state - exec.get_supply_state - # => [max, token, dec, sym, amount, pad(10)] - - # Assert token_supply <= max_supply - # Need [max, token] on top for lte: a=token, b=max → token <= max - dup.1 dup.1 - # => [max, token, max, token, dec, sym, amount, ...] - lte assert.err=ERR_SUPPLY_EXCEEDED - # => [max, token, dec, sym, amount, ...] - - # Calculate max_mint_amount = max_supply - token_supply - dup dup.2 sub - # => [max_mint, max, token, dec, sym, amount, pad(9)] - - # Assert amount <= max_mint_amount - # Need [max_mint, amount] on top for lte: a=amount, b=max_mint → amount <= max_mint - dup dup.6 swap - # => [max_mint, amount, max_mint, max, token, dec, sym, amount, ...] - lte assert.err=ERR_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY - # => [max_mint, max, token, dec, sym, amount, ...] - - # Clean up and return [max_mint, token, max, pad(12)] - # Current indices: 0:max_mint, 1:max, 2:token, 3:dec, 4:sym, 5:amount - movup.5 drop # drop amount - movup.4 drop # drop sym - movup.3 drop # drop dec - # => [max_mint, max, token, pad(12)] - - # Rearrange to [max_mint, token, max] - movup.2 swap - # => [max_mint, token, max, pad(12)] -end - -#! Updates the supply after minting tokens. -#! -#! Inputs: [amount, pad(15)] -#! Outputs: [pad(16)] -pub proc update_supply_after_mint - # Get current supply state - exec.get_supply_state - # => [max, token, dec, sym, amount, pad(11)] - - # Calculate new_token_supply = token_supply + amount - dup.1 dup.5 add - # => [new_token, max, token, dec, sym, amount, pad(11)] - - # Construct word for storage: [new_token, max, dec, sym] - # Storage order: [token_supply, max_supply, decimals, token_symbol] - # We want [sym, dec, max, new] on stack for set_item VALUE - - # Remove old token - movdn.2 swap drop - # => [max, new_token, dec, sym, amount, pad(11)] - - # Reorder to [sym, dec, max, new] - # Current: 0:max, 1:new, 2:dec, 3:sym - movup.3 # sym - movup.3 # dec - swap - # => [sym, dec, max, new, amount, pad(11)] - - push.SUPPLY_CONFIG_SLOT[0..2] - # => [slot_pre, slot_suf, sym, dec, max, new, amount, pad(9)] - - # Move slot id deep - movdn.5 movdn.5 - # => [sym, dec, max, new, slot_pre, slot_suf, amount, pad(9)] - - exec.native_account::set_item - # => [OLD_WORD, amount, pad(9)] - - # Clean up and return pad(16) - dropw drop - repeat.9 drop end - padw padw padw padw -end - -#! Updates the supply after burning tokens. -#! -#! Inputs: [amount, pad(15)] -#! Outputs: [pad(16)] -pub proc update_supply_after_burn - # Get current supply state - exec.get_supply_state - # => [max, token, dec, sym, amount, pad(11)] - - # Assert amount <= token_supply - # Need [token, amount] on top for lte: a=amount, b=token → amount <= token - dup.4 dup.2 - # => [token, amount, max, token, dec, sym, amount, ...] - lte assert.err=ERR_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY - # => [max, token, dec, sym, amount, ...] - - # Calculate new_token_supply = token_supply - amount - dup.1 dup.5 sub - # => [new_token, max, token, dec, sym, amount, pad(11)] - - # Remove old token - movdn.2 swap drop - # => [max, new_token, dec, sym, amount, pad(11)] - - # Reorder to [sym, dec, max, new] - movup.3 # sym - movup.3 # dec - swap - # => [sym, dec, max, new, amount, pad(11)] - - push.SUPPLY_CONFIG_SLOT[0..2] - movdn.5 movdn.5 - - exec.native_account::set_item - # => [OLD_WORD, amount, pad(9)] - - # Clean up and return pad(16) - dropw drop - repeat.9 drop end - padw padw padw padw -end - -#! Returns the current supply state. -#! -#! Inputs: [pad(16)] -#! Outputs: [token_supply, max_supply, pad(14)] -pub proc get_supply - exec.get_supply_state - # => [max, token, dec, sym, pad(12)] - - # Drop metadata - movup.3 drop movup.2 drop - # => [max, token, pad(14)] - - # Output expected [token, max] - swap -end diff --git a/crates/miden-standards/asm/standards/supply/flexible_supply.masm b/crates/miden-standards/asm/standards/supply/flexible_supply.masm deleted file mode 100644 index 262e718cd7..0000000000 --- a/crates/miden-standards/asm/standards/supply/flexible_supply.masm +++ /dev/null @@ -1,199 +0,0 @@ -# miden::standards::supply::flexible_supply -# -# Provides time-based supply management functionality for faucets. -# This component enforces supply limits during a distribution period and optionally -# allows burn-only operations after the period ends. - -use miden::protocol::active_account -use miden::protocol::native_account -use miden::protocol::tx - -# CONSTANTS -# ================================================================================================ - -# The slot where the flexible supply configuration is stored. -# Layout: [token_supply, max_supply, distribution_end_block, burn_only_flag] -const FLEXIBLE_SUPPLY_CONFIG_SLOT = word("miden::standards::supply::flexible_supply::config") - -# ERRORS -# ================================================================================================ - -const ERR_SUPPLY_EXCEEDED = "token supply exceeds max supply" -const ERR_SUPPLY_CAP_INVALID = "max supply exceeds maximum representable fungible asset amount" -const ERR_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY = "token_supply plus the amount passed to distribute would exceed the maximum supply" -const ERR_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY = "asset amount to burn exceeds the existing token supply" -const ERR_DISTRIBUTION_PERIOD_ENDED = "distribution period has ended" -const ERR_BURN_NOT_ALLOWED = "burn operations are not allowed during distribution period" - -# INTERNAL PROCEDURES -# ================================================================================================ - -#! Returns the current flexible supply state from storage. -#! -#! Inputs: [] -#! Outputs: [token_supply, max_supply, distribution_end_block, burn_only_flag] -proc get_flexible_supply_state - push.FLEXIBLE_SUPPLY_CONFIG_SLOT[0..2] exec.active_account::get_item - # => [burn_only_flag, distribution_end_block, max_supply, token_supply] - - # Rearrange to [token_supply, max_supply, distribution_end_block, burn_only_flag] - movdn.3 movdn.2 swap -end - -#! Gets the current block height. -#! -#! Inputs: [] -#! Outputs: [block_height] -proc get_current_block_height - exec.tx::get_block_number -end - -# PUBLIC INTERFACE -# ================================================================================================ - -#! Checks if distribution is allowed based on time and supply constraints. -#! -#! Inputs: [amount, pad(14)] -#! Outputs: [max_mint_amount, token_supply, max_supply, pad(12)] -pub proc check_can_distribute - # Get current supply state - exec.get_flexible_supply_state - # => [token, max, end, burn, amount, pad(10)] - - # Check if distribution period has ended (if end > 0) - dup.2 - if.true - exec.get_current_block_height - dup.3 # end block - lt assert.err=ERR_DISTRIBUTION_PERIOD_ENDED - end - # => [token, max, end, burn, amount, pad(10)] - - # Assert token <= max - dup dup.2 lte assert.err=ERR_SUPPLY_EXCEEDED - # => [token, max, end, burn, amount, pad(10)] - - # Calculate max_mint = max - token - dup.1 dup.1 sub - # => [max_mint, token, max, end, burn, amount, pad(9)] - - # Assert amount <= max_mint - # Need [max_mint, amount] on top for lte: a=amount, b=max_mint → amount <= max_mint - dup dup.6 swap lte assert.err=ERR_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY - # => [max_mint, token, max, end, burn, amount, ...] - - # Clean up and return [max_mint, token, max, pad(12)] - movup.5 drop # amount - movup.4 drop # burn - movup.3 drop # end - # => [max_mint, token, max, pad(12)] -end - -#! Checks if burn is allowed based on time constraints. -#! -#! Inputs: [amount, pad(15)] -#! Outputs: [pad(16)] -pub proc check_can_burn - # Get current supply state - exec.get_flexible_supply_state - # => [token, max, end, burn, amount, pad(11)] - - # If burn_only_flag is 1, burns are only allowed AFTER the period - dup.3 - if.true - dup.2 # end - dup push.0 neq assert.err=ERR_BURN_NOT_ALLOWED # end block must be set for burn-only mode - - exec.get_current_block_height - swap gte assert.err=ERR_BURN_NOT_ALLOWED - end - # => [token, max, end, burn, amount, pad(11)] - - # Assert amount <= token_supply - # Need [token, amount] on top for lte: a=amount, b=token → amount <= token - dup.4 dup.1 lte assert.err=ERR_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY - # => [token, max, end, burn, amount, ...] - - # Clean up and return pad(16) - drop drop drop drop drop - repeat.11 drop end - padw padw padw padw -end - -#! Updates the supply after minting tokens. -#! -#! Inputs: [amount, pad(15)] -#! Outputs: [pad(16)] -pub proc update_supply_after_mint - exec.get_flexible_supply_state - # => [token, max, end, burn, amount, pad(11)] - - # Calculate new_token = token + amount - dup dup.5 add - # => [new, token, max, end, burn, amount, pad(11)] - - # Remove old token - swap drop - # => [new, max, end, burn, amount, ...] - - # Reorder to [burn, end, max, new] - movdn.3 - # => [max, end, burn, new, amount, ...] - movup.2 # burn - movup.2 # end - swap - # => [burn, end, max, new, amount, ...] - - push.FLEXIBLE_SUPPLY_CONFIG_SLOT[0..2] - movdn.5 movdn.5 - # => [burn, end, max, new, pre, suf, amount, ...] - - exec.native_account::set_item - # => [OLD, amount, ...] - - dropw drop repeat.11 drop end - padw padw padw padw -end - -#! Updates the supply after burning tokens. -#! -#! Inputs: [amount, pad(15)] -#! Outputs: [pad(16)] -pub proc update_supply_after_burn - exec.get_flexible_supply_state - # => [token, max, end, burn, amount, ...] - - # Calculate new_token = token - amount - dup dup.5 sub - # => [new, token, max, end, burn, amount, ...] - - # Remove old token - swap drop - # => [new, max, end, burn, amount, ...] - - # Reorder to [burn, end, max, new] - movdn.3 - # => [max, end, burn, new, amount, ...] - movup.2 # burn - movup.2 # end - swap - # => [burn, end, max, new, amount, ...] - - push.FLEXIBLE_SUPPLY_CONFIG_SLOT[0..2] - movdn.5 movdn.5 - - exec.native_account::set_item - # => [OLD, amount, pad(11)] - - dropw drop repeat.11 drop end - padw padw padw padw -end - -#! Returns the current flexible supply state. -#! -#! Inputs: [pad(16)] -#! Outputs: [token_supply, max_supply, distribution_end_block, burn_only_flag, pad(12)] -pub proc get_supply - exec.get_flexible_supply_state - # => [token, max, end, burn, pad(12)] -end diff --git a/crates/miden-standards/asm/standards/supply/supply_limits.masm b/crates/miden-standards/asm/standards/supply/supply_limits.masm new file mode 100644 index 0000000000..73bb669e36 --- /dev/null +++ b/crates/miden-standards/asm/standards/supply/supply_limits.masm @@ -0,0 +1,188 @@ +# miden::standards::supply::supply_limits +# +# Unified supply management for faucets. +# Enforces supply caps with optional time-based distribution windows +# and burn restrictions. +# +# Storage layout: [token_supply, max_supply, distribution_end_block, burn_only_flag] +# +# For fixed-supply faucets: set distribution_end_block=0 and burn_only_flag=0. +# For timed faucets: set distribution_end_block and burn_only_flag as needed. + +use miden::protocol::active_account +use miden::protocol::native_account +use miden::protocol::tx + +# CONSTANTS +# ================================================================================================ + +# The slot where the supply limits configuration is stored. +# Layout: [token_supply, max_supply, distribution_end_block, burn_only_flag] +const SUPPLY_LIMITS_SLOT = word("miden::standards::supply::supply_limits::config") + +# ERRORS +# ================================================================================================ + +const ERR_SUPPLY_EXCEEDED = "token supply exceeds max supply" +const ERR_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY = "token_supply plus the amount passed to distribute would exceed the maximum supply" +const ERR_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY = "asset amount to burn exceeds the existing token supply" +const ERR_DISTRIBUTION_PERIOD_ENDED = "distribution period has ended" +const ERR_BURN_NOT_ALLOWED = "burn operations are not allowed during distribution period" + +# INTERNAL PROCEDURES +# ================================================================================================ + +#! Returns the current supply state from storage. +#! +#! Inputs: [] +#! Outputs: [token_supply, max_supply, distribution_end_block, burn_only_flag] +proc get_supply_state + push.SUPPLY_LIMITS_SLOT[0..2] exec.active_account::get_item + # => [burn_only_flag, distribution_end_block, max_supply, token_supply] + + # Rearrange to [token_supply, max_supply, distribution_end_block, burn_only_flag] + movdn.3 movdn.2 swap +end + +#! Gets the current block height. +#! +#! Inputs: [] +#! Outputs: [block_height] +proc get_current_block_height + exec.tx::get_block_number +end + +# PUBLIC INTERFACE +# ================================================================================================ + +#! Checks supply constraints and updates the supply counter after minting. +#! +#! This merges the check and update into a single procedure to avoid +#! double storage reads. +#! +#! Inputs: [amount, pad(15)] +#! Outputs: [pad(16)] +#! +#! Panics if: +#! - the distribution period has ended (when distribution_end_block > 0). +#! - token_supply exceeds max_supply (invariant check). +#! - amount would exceed remaining mintable supply. +pub proc mint + exec.get_supply_state + # => [token, max, end, burn_only, amount, pad(15)] + + # Check if distribution period has ended (if end > 0) + dup.2 + if.true + exec.get_current_block_height + dup.3 # end block + lt assert.err=ERR_DISTRIBUTION_PERIOD_ENDED + end + # => [token, max, end, burn_only, amount, pad(15)] + + # Assert token <= max (invariant) + dup dup.2 lte assert.err=ERR_SUPPLY_EXCEEDED + # => [token, max, end, burn_only, amount, pad(15)] + + # Calculate max_mint = max - token + dup.1 dup.1 sub + # => [max_mint, token, max, end, burn_only, amount, pad(15)] + + # Assert amount <= max_mint + dup dup.6 swap lte assert.err=ERR_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY + # => [max_mint, token, max, end, burn_only, amount, pad(15)] + + # Drop max_mint, no longer needed + drop + # => [token, max, end, burn_only, amount, pad(15)] + + # Calculate new_token = token + amount + dup dup.5 add + # => [new_token, token, max, end, burn_only, amount, pad(15)] + + # Remove old token + swap drop + # => [new_token, max, end, burn_only, amount, pad(15)] + + # Reorder to [burn_only, end, max, new_token] for storage write + movdn.3 + # => [max, end, burn_only, new_token, amount, pad(15)] + movup.2 # burn_only + movup.2 # end + swap + # => [burn_only, end, max, new_token, amount, pad(15)] + + push.SUPPLY_LIMITS_SLOT[0..2] + movdn.5 movdn.5 + # => [burn_only, end, max, new_token, pre, suf, amount, pad(15)] + + exec.native_account::set_item + # => [OLD(4), amount, pad(15)] + + dropw drop repeat.15 drop end + padw padw padw padw +end + +#! Checks burn constraints and updates the supply counter after burning. +#! +#! This merges the check and update into a single procedure to avoid +#! double storage reads. +#! +#! Inputs: [amount, pad(15)] +#! Outputs: [pad(16)] +#! +#! Panics if: +#! - burn_only_flag is set and distribution period has not ended yet. +#! - amount exceeds the current token_supply. +pub proc burn + exec.get_supply_state + # => [token, max, end, burn_only, amount, pad(15)] + + # If burn_only_flag is 1, burns are only allowed AFTER the distribution period + dup.3 + if.true + dup.2 # end + dup push.0 neq assert.err=ERR_BURN_NOT_ALLOWED # end block must be set + exec.get_current_block_height + swap gte assert.err=ERR_BURN_NOT_ALLOWED + end + # => [token, max, end, burn_only, amount, pad(15)] + + # Assert amount <= token_supply + dup.4 dup.1 lte assert.err=ERR_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY + # => [token, max, end, burn_only, amount, pad(15)] + + # Calculate new_token = token - amount + dup dup.5 sub + # => [new_token, token, max, end, burn_only, amount, pad(15)] + + # Remove old token + swap drop + # => [new_token, max, end, burn_only, amount, pad(15)] + + # Reorder to [burn_only, end, max, new_token] for storage write + movdn.3 + # => [max, end, burn_only, new_token, amount, pad(15)] + movup.2 # burn_only + movup.2 # end + swap + # => [burn_only, end, max, new_token, amount, pad(15)] + + push.SUPPLY_LIMITS_SLOT[0..2] + movdn.5 movdn.5 + + exec.native_account::set_item + # => [OLD(4), amount, pad(15)] + + dropw drop repeat.15 drop end + padw padw padw padw +end + +#! Returns the current supply state. +#! +#! Inputs: [pad(16)] +#! Outputs: [token_supply, max_supply, distribution_end_block, burn_only_flag, pad(12)] +pub proc get_supply + exec.get_supply_state + # => [token, max, end, burn_only, pad(12)] +end diff --git a/crates/miden-standards/src/account/faucets/timed_fungible.rs b/crates/miden-standards/src/account/faucets/timed_fungible.rs index ba19ac982f..6970c1bc74 100644 --- a/crates/miden-standards/src/account/faucets/timed_fungible.rs +++ b/crates/miden-standards/src/account/faucets/timed_fungible.rs @@ -19,6 +19,7 @@ use miden_protocol::asset::TokenSymbol; use miden_protocol::utils::sync::LazyLock; use miden_protocol::{Felt, FieldElement, Word}; +use super::token_metadata::TOKEN_SYMBOL_TYPE_ID; use super::{FungibleFaucetError, TokenMetadata}; use crate::account::AuthScheme; use crate::account::auth::{ @@ -31,14 +32,11 @@ use crate::account::components::timed_fungible_faucet_library; use crate::account::interface::{AccountComponentInterface, AccountInterface, AccountInterfaceExt}; use crate::procedure_digest; -/// The schema type ID for token symbols. -const TOKEN_SYMBOL_TYPE_ID: &str = "miden::standards::fungible_faucets::metadata::token_symbol"; - // SLOT NAMES // ================================================================================================ static SUPPLY_CONFIG_SLOT: LazyLock = LazyLock::new(|| { - StorageSlotName::new("miden::standards::supply::flexible_supply::config") + StorageSlotName::new("miden::standards::supply::supply_limits::config") .expect("storage slot name should be valid") }); diff --git a/crates/miden-standards/src/account/faucets/token_metadata.rs b/crates/miden-standards/src/account/faucets/token_metadata.rs index 37fb74c562..fbea6ad086 100644 --- a/crates/miden-standards/src/account/faucets/token_metadata.rs +++ b/crates/miden-standards/src/account/faucets/token_metadata.rs @@ -8,6 +8,9 @@ use super::FungibleFaucetError; // CONSTANTS // ================================================================================================ +/// Schema type ID for the token symbol field in token metadata storage schema. +pub const TOKEN_SYMBOL_TYPE_ID: &str = "miden::standards::fungible_faucets::metadata::token_symbol"; + static METADATA_SLOT_NAME: LazyLock = LazyLock::new(|| { StorageSlotName::new("miden::standards::fungible_faucets::metadata") .expect("storage slot name should be valid") diff --git a/crates/miden-standards/src/account/faucets/unlimited_fungible.rs b/crates/miden-standards/src/account/faucets/unlimited_fungible.rs index 375d6e8098..cb19f4ea62 100644 --- a/crates/miden-standards/src/account/faucets/unlimited_fungible.rs +++ b/crates/miden-standards/src/account/faucets/unlimited_fungible.rs @@ -18,6 +18,7 @@ use miden_protocol::account::{ use miden_protocol::asset::TokenSymbol; use miden_protocol::{Felt, Word}; +use super::token_metadata::TOKEN_SYMBOL_TYPE_ID; use super::{FungibleFaucetError, TokenMetadata}; use crate::account::AuthScheme; use crate::account::auth::{ @@ -30,9 +31,6 @@ use crate::account::components::unlimited_fungible_faucet_library; use crate::account::interface::{AccountComponentInterface, AccountInterface, AccountInterfaceExt}; use crate::procedure_digest; -/// The schema type ID for token symbols. -const TOKEN_SYMBOL_TYPE_ID: &str = "miden::standards::fungible_faucets::metadata::token_symbol"; - // UNLIMITED FUNGIBLE FAUCET ACCOUNT COMPONENT // ================================================================================================ diff --git a/crates/miden-standards/src/errors/standards.rs b/crates/miden-standards/src/errors/standards.rs index 76c6f4ab31..bea02711a4 100644 --- a/crates/miden-standards/src/errors/standards.rs +++ b/crates/miden-standards/src/errors/standards.rs @@ -64,8 +64,6 @@ pub const ERR_P2ID_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS: MasmError = MasmError::fr /// Error Message: "note sender is not the owner" pub const ERR_SENDER_NOT_OWNER: MasmError = MasmError::from_static_str("note sender is not the owner"); -/// Error Message: "max supply exceeds maximum representable fungible asset amount" -pub const ERR_SUPPLY_CAP_INVALID: MasmError = MasmError::from_static_str("max supply exceeds maximum representable fungible asset amount"); /// Error Message: "token supply exceeds max supply" pub const ERR_SUPPLY_EXCEEDED: MasmError = MasmError::from_static_str("token supply exceeds max supply"); From 7386db3b8a61749b8a457dec7c62a88bb7539359 Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Thu, 19 Feb 2026 10:21:10 -0300 Subject: [PATCH 05/10] Refactor supply management for fungible faucets - Simplified supply limits management in `supply_limits.masm` by removing unnecessary checks and consolidating procedures for minting and burning tokens. - Updated `TimedFungibleFaucet` and `UnlimitedFungibleFaucet` to include owner account management, allowing for ownership checks and ownership transfer functionality. - Removed deprecated error messages related to supply checks and burn restrictions, replacing them with more relevant error handling for timed distribution periods. - Adjusted tests to reflect changes in ownership management and updated assertions accordingly. --- .../faucets/timed_fungible_faucet.masm | 2 + .../faucets/unlimited_fungible_faucet.masm | 2 + .../asm/standards/faucets/timed_fungible.masm | 141 +++------- .../standards/faucets/unlimited_fungible.masm | 71 +---- .../asm/standards/supply/supply_limits.masm | 209 +++----------- .../src/account/faucets/timed_fungible.rs | 261 ++++++++++-------- .../src/account/faucets/unlimited_fungible.rs | 194 ++++++++----- .../miden-standards/src/errors/standards.rs | 23 +- 8 files changed, 374 insertions(+), 529 deletions(-) diff --git a/crates/miden-standards/asm/account_components/faucets/timed_fungible_faucet.masm b/crates/miden-standards/asm/account_components/faucets/timed_fungible_faucet.masm index e0b22f876c..f7b0c63437 100644 --- a/crates/miden-standards/asm/account_components/faucets/timed_fungible_faucet.masm +++ b/crates/miden-standards/asm/account_components/faucets/timed_fungible_faucet.masm @@ -4,3 +4,5 @@ pub use ::miden::standards::faucets::timed_fungible::distribute pub use ::miden::standards::faucets::timed_fungible::burn +pub use ::miden::standards::faucets::timed_fungible::transfer_ownership +pub use ::miden::standards::faucets::timed_fungible::renounce_ownership diff --git a/crates/miden-standards/asm/account_components/faucets/unlimited_fungible_faucet.masm b/crates/miden-standards/asm/account_components/faucets/unlimited_fungible_faucet.masm index 9679d04707..a700390731 100644 --- a/crates/miden-standards/asm/account_components/faucets/unlimited_fungible_faucet.masm +++ b/crates/miden-standards/asm/account_components/faucets/unlimited_fungible_faucet.masm @@ -4,3 +4,5 @@ pub use ::miden::standards::faucets::unlimited_fungible::distribute pub use ::miden::standards::faucets::unlimited_fungible::burn +pub use ::miden::standards::faucets::unlimited_fungible::transfer_ownership +pub use ::miden::standards::faucets::unlimited_fungible::renounce_ownership diff --git a/crates/miden-standards/asm/standards/faucets/timed_fungible.masm b/crates/miden-standards/asm/standards/faucets/timed_fungible.masm index 406c7bc2f8..6ee279ed15 100644 --- a/crates/miden-standards/asm/standards/faucets/timed_fungible.masm +++ b/crates/miden-standards/asm/standards/faucets/timed_fungible.masm @@ -1,32 +1,39 @@ # TIMED SUPPLY FUNGIBLE FAUCET CONTRACT # -# A fungible faucet with time-based supply management using the supply::supply_limits component. -# This faucet enforces supply limits during a distribution period and optionally allows -# burn-only operations after the period ends. +# A fungible faucet with time-based distribution controls. +# This faucet enforces a distribution period for minting. +# Burns are always allowed. Supply tracking is handled by faucets::distribute/burn. # ================================================================================================= -use miden::protocol::active_note -use miden::protocol::faucet -use miden::protocol::output_note -use miden::standards::supply::supply_limits +use miden::protocol::active_account +use miden::protocol::tx +use miden::standards::faucets +use miden::standards::access::ownable # CONSTANTS # ================================================================================================= -const PRIVATE_NOTE=2 +const TIMED_CONFIG_SLOT = word("miden::standards::faucets::timed_fungible::config") # ERRORS # ================================================================================================= -const ERR_TIMED_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS="burn requires exactly 1 note asset" +const ERR_TIMED_FUNGIBLE_DISTRIBUTION_PERIOD_ENDED="distribution period has ended" + +# OWNER MANAGEMENT +# ================================================================================================= + +pub use ownable::get_owner +pub use ownable::transfer_ownership +pub use ownable::renounce_ownership # PROCEDURES # ================================================================================================= #! Distributes freshly minted fungible assets to the provided recipient by creating a note. #! -#! This procedure uses the supply_limits component to enforce time-based supply limits -#! and update the supply counter in a single call. +#! This procedure checks the distribution period before delegating to the shared distribute +#! procedure which handles supply tracking in the metadata slot. #! #! Inputs: [amount, tag, note_type, RECIPIENT] #! Outputs: [note_idx] @@ -41,81 +48,37 @@ const ERR_TIMED_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS="burn requires exactly 1 no #! #! Panics if: #! - the transaction is being executed against an account that is not a fungible asset faucet. +#! - the note sender is not the owner of this faucet. #! - the distribution period has ended. -#! - the supply constraints are violated. #! #! Invocation: exec -@locals(8) pub proc distribute - # Store inputs to locals - # --------------------------------------------------------------------------------------------- - # Stack: [amount, tag, note_type, RECIPIENT] - - loc_store.4 - # => [tag, note_type, RECIPIENT] + exec.ownable::verify_owner - loc_store.5 - # => [note_type, RECIPIENT] + # Check distribution period hasn't ended + push.TIMED_CONFIG_SLOT[0..2] exec.active_account::get_item + # => [distribution_end, 0, 0, 0, amount, tag, note_type, RECIPIENT] - loc_store.6 - # => [RECIPIENT] - - loc_storew_be.0 - # => [RECIPIENT] + dup + if.true + exec.tx::get_block_number + dup.1 + # => [distribution_end, current_block, distribution_end, 0, 0, 0, ...] + lt assert.err=ERR_TIMED_FUNGIBLE_DISTRIBUTION_PERIOD_ENDED + # => [distribution_end, 0, 0, 0, ...] + end dropw - # => [] - - # Check supply constraints and update counter - # --------------------------------------------------------------------------------------------- - - padw padw padw push.0.0.0 - loc_load.4 - # => [amount, pad(15)] - - exec.supply_limits::mint - # => [pad(16)] - - dropw dropw dropw dropw - # => [] - - # Mint the asset - # --------------------------------------------------------------------------------------------- - - loc_load.4 - exec.faucet::create_fungible_asset - # => [ASSET] - - exec.faucet::mint - # => [ASSET] - - # Create the note with the asset - # --------------------------------------------------------------------------------------------- - - padw loc_loadw_be.0 - # => [RECIPIENT, ASSET] + # => [amount, tag, note_type, RECIPIENT] - loc_load.6 - # => [note_type, RECIPIENT, ASSET] - - loc_load.5 - # => [tag, note_type, RECIPIENT, ASSET] - - exec.output_note::create - # => [note_idx, ASSET] - - dup movdn.5 movdn.5 - # => [ASSET, note_idx, note_idx] - - exec.output_note::add_asset + exec.faucets::distribute # => [note_idx] end #! Burns the fungible asset from the active note. #! -#! Burning the asset removes it from circulation and reduces the token_supply. -#! This procedure uses the supply_limits component which may enforce time-based -#! restrictions (e.g., burn-only mode after distribution period). +#! Burns are always allowed — no time or ownership restrictions. +#! Delegates to the shared burn procedure which handles supply tracking. #! #! Inputs: [pad(16)] #! Outputs: [pad(16)] @@ -125,37 +88,7 @@ end #! - the note does not contain exactly one asset. #! - the transaction is executed against an account which is not a fungible asset faucet. #! - the transaction is executed against a faucet which is not the origin of the specified asset. -#! - burn is not allowed in the current period (if burn-only mode is enabled). +#! - the amount to burn exceeds the current token supply. #! #! Invocation: call -pub proc burn - # Get the asset from the note - # --------------------------------------------------------------------------------------------- - - push.0 exec.active_note::get_assets - # => [num_assets, dest_ptr, pad(16)] - - assert.err=ERR_TIMED_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS - # => [dest_ptr, pad(16)] - - mem_loadw_be - # => [ASSET, pad(16)] - - # Save amount for supply update, then burn the asset - # --------------------------------------------------------------------------------------------- - - dup.3 movdn.4 - # => [ASSET, amount, pad(16)] - - exec.faucet::burn dropw - # => [amount, pad(16)] - - # Check burn constraints and update supply counter - # --------------------------------------------------------------------------------------------- - - swap drop - # => [amount, pad(15)] - - exec.supply_limits::burn - # => [pad(16)] -end +pub use faucets::burn diff --git a/crates/miden-standards/asm/standards/faucets/unlimited_fungible.masm b/crates/miden-standards/asm/standards/faucets/unlimited_fungible.masm index 8e1281f1f9..1fe5215c75 100644 --- a/crates/miden-standards/asm/standards/faucets/unlimited_fungible.masm +++ b/crates/miden-standards/asm/standards/faucets/unlimited_fungible.masm @@ -4,19 +4,15 @@ # This faucet allows unlimited minting and burning of tokens. # ================================================================================================= -use miden::protocol::active_note -use miden::protocol::faucet -use miden::protocol::output_note +use miden::standards::faucets +use miden::standards::access::ownable -# CONSTANTS +# OWNER MANAGEMENT # ================================================================================================= -const PRIVATE_NOTE=2 - -# ERRORS -# ================================================================================================= - -const ERR_UNLIMITED_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS="burn requires exactly 1 note asset" +pub use ownable::get_owner +pub use ownable::transfer_ownership +pub use ownable::renounce_ownership # PROCEDURES # ================================================================================================= @@ -38,36 +34,14 @@ const ERR_UNLIMITED_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS="burn requires exactly #! #! Panics if: #! - the transaction is being executed against an account that is not a fungible asset faucet. +#! - the note sender is not the owner of this faucet. #! #! Invocation: exec pub proc distribute - # Mint the asset - # --------------------------------------------------------------------------------------------- - - # Stack: [amount, tag, note_type, RECIPIENT] - # Create the fungible asset - exec.faucet::create_fungible_asset - # => [ASSET, tag, note_type, RECIPIENT] - - # Mint the asset - exec.faucet::mint - # => [ASSET, tag, note_type, RECIPIENT] - - # Rearrange stack for note creation - movdn.9 movdn.9 movdn.9 movdn.9 - # => [tag, note_type, RECIPIENT, ASSET] - - # Create a new note with the asset - # --------------------------------------------------------------------------------------------- - - exec.output_note::create - # => [note_idx, ASSET] - - # Add the asset to the note - dup movdn.5 movdn.5 - # => [ASSET, note_idx, note_idx] - - exec.output_note::add_asset + exec.ownable::verify_owner + # => [amount, tag, note_type, RECIPIENT] + + exec.faucets::distribute # => [note_idx] end @@ -85,25 +59,4 @@ end #! - the transaction is executed against a faucet which is not the origin of the specified asset. #! #! Invocation: call -pub proc burn - # Get the asset from the note. - # --------------------------------------------------------------------------------------------- - - # this will fail if not called from a note context. - push.0 exec.active_note::get_assets - # => [num_assets, dest_ptr, pad(16)] - - # Verify we have exactly one asset - assert.err=ERR_UNLIMITED_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS - # => [dest_ptr, pad(16)] - - mem_loadw_be - # => [ASSET, pad(16)] - - # Burn the asset from the transaction vault - # --------------------------------------------------------------------------------------------- - - # burn the asset (no supply tracking needed) - exec.faucet::burn dropw - # => [pad(16)] -end +pub use faucets::burn diff --git a/crates/miden-standards/asm/standards/supply/supply_limits.masm b/crates/miden-standards/asm/standards/supply/supply_limits.masm index 73bb669e36..3fbd363d51 100644 --- a/crates/miden-standards/asm/standards/supply/supply_limits.masm +++ b/crates/miden-standards/asm/standards/supply/supply_limits.masm @@ -1,188 +1,71 @@ # miden::standards::supply::supply_limits # -# Unified supply management for faucets. -# Enforces supply caps with optional time-based distribution windows -# and burn restrictions. -# -# Storage layout: [token_supply, max_supply, distribution_end_block, burn_only_flag] -# -# For fixed-supply faucets: set distribution_end_block=0 and burn_only_flag=0. -# For timed faucets: set distribution_end_block and burn_only_flag as needed. +# Reusable supply validation utilities for faucets. +# Pure check procedures — validate constraints but do not modify storage. -use miden::protocol::active_account -use miden::protocol::native_account use miden::protocol::tx -# CONSTANTS -# ================================================================================================ - -# The slot where the supply limits configuration is stored. -# Layout: [token_supply, max_supply, distribution_end_block, burn_only_flag] -const SUPPLY_LIMITS_SLOT = word("miden::standards::supply::supply_limits::config") - # ERRORS -# ================================================================================================ +# ================================================================================================= -const ERR_SUPPLY_EXCEEDED = "token supply exceeds max supply" -const ERR_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY = "token_supply plus the amount passed to distribute would exceed the maximum supply" -const ERR_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY = "asset amount to burn exceeds the existing token supply" const ERR_DISTRIBUTION_PERIOD_ENDED = "distribution period has ended" -const ERR_BURN_NOT_ALLOWED = "burn operations are not allowed during distribution period" - -# INTERNAL PROCEDURES -# ================================================================================================ - -#! Returns the current supply state from storage. -#! -#! Inputs: [] -#! Outputs: [token_supply, max_supply, distribution_end_block, burn_only_flag] -proc get_supply_state - push.SUPPLY_LIMITS_SLOT[0..2] exec.active_account::get_item - # => [burn_only_flag, distribution_end_block, max_supply, token_supply] - - # Rearrange to [token_supply, max_supply, distribution_end_block, burn_only_flag] - movdn.3 movdn.2 swap -end +const ERR_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY = "amount would exceed the maximum supply" +const ERR_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY = "burn amount exceeds the existing token supply" -#! Gets the current block height. -#! -#! Inputs: [] -#! Outputs: [block_height] -proc get_current_block_height - exec.tx::get_block_number -end - -# PUBLIC INTERFACE -# ================================================================================================ +# PROCEDURES +# ================================================================================================= -#! Checks supply constraints and updates the supply counter after minting. +#! Asserts the distribution period is still active. +#! If distribution_end_block is 0, the check is skipped (no time limit). #! -#! This merges the check and update into a single procedure to avoid -#! double storage reads. +#! Inputs: [distribution_end_block, ...] +#! Outputs: [...] #! -#! Inputs: [amount, pad(15)] -#! Outputs: [pad(16)] +#! Panics if: current_block >= distribution_end_block. #! -#! Panics if: -#! - the distribution period has ended (when distribution_end_block > 0). -#! - token_supply exceeds max_supply (invariant check). -#! - amount would exceed remaining mintable supply. -pub proc mint - exec.get_supply_state - # => [token, max, end, burn_only, amount, pad(15)] - - # Check if distribution period has ended (if end > 0) - dup.2 +#! Invocation: exec +pub proc check_distribution_period + dup if.true - exec.get_current_block_height - dup.3 # end block + exec.tx::get_block_number + # => [current_block, distribution_end_block, ...] + swap + # => [distribution_end_block, current_block, ...] lt assert.err=ERR_DISTRIBUTION_PERIOD_ENDED + # => [...] + else + drop end - # => [token, max, end, burn_only, amount, pad(15)] - - # Assert token <= max (invariant) - dup dup.2 lte assert.err=ERR_SUPPLY_EXCEEDED - # => [token, max, end, burn_only, amount, pad(15)] - - # Calculate max_mint = max - token - dup.1 dup.1 sub - # => [max_mint, token, max, end, burn_only, amount, pad(15)] - - # Assert amount <= max_mint - dup dup.6 swap lte assert.err=ERR_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY - # => [max_mint, token, max, end, burn_only, amount, pad(15)] - - # Drop max_mint, no longer needed - drop - # => [token, max, end, burn_only, amount, pad(15)] - - # Calculate new_token = token + amount - dup dup.5 add - # => [new_token, token, max, end, burn_only, amount, pad(15)] - - # Remove old token - swap drop - # => [new_token, max, end, burn_only, amount, pad(15)] - - # Reorder to [burn_only, end, max, new_token] for storage write - movdn.3 - # => [max, end, burn_only, new_token, amount, pad(15)] - movup.2 # burn_only - movup.2 # end - swap - # => [burn_only, end, max, new_token, amount, pad(15)] - - push.SUPPLY_LIMITS_SLOT[0..2] - movdn.5 movdn.5 - # => [burn_only, end, max, new_token, pre, suf, amount, pad(15)] - - exec.native_account::set_item - # => [OLD(4), amount, pad(15)] - - dropw drop repeat.15 drop end - padw padw padw padw end -#! Checks burn constraints and updates the supply counter after burning. +#! Asserts that minting `amount` would not exceed max_supply. #! -#! This merges the check and update into a single procedure to avoid -#! double storage reads. +#! Inputs: [amount, token_supply, max_supply, ...] +#! Outputs: [...] #! -#! Inputs: [amount, pad(15)] -#! Outputs: [pad(16)] +#! Panics if: token_supply + amount > max_supply. #! -#! Panics if: -#! - burn_only_flag is set and distribution period has not ended yet. -#! - amount exceeds the current token_supply. -pub proc burn - exec.get_supply_state - # => [token, max, end, burn_only, amount, pad(15)] - - # If burn_only_flag is 1, burns are only allowed AFTER the distribution period - dup.3 - if.true - dup.2 # end - dup push.0 neq assert.err=ERR_BURN_NOT_ALLOWED # end block must be set - exec.get_current_block_height - swap gte assert.err=ERR_BURN_NOT_ALLOWED - end - # => [token, max, end, burn_only, amount, pad(15)] - - # Assert amount <= token_supply - dup.4 dup.1 lte assert.err=ERR_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY - # => [token, max, end, burn_only, amount, pad(15)] - - # Calculate new_token = token - amount - dup dup.5 sub - # => [new_token, token, max, end, burn_only, amount, pad(15)] - - # Remove old token - swap drop - # => [new_token, max, end, burn_only, amount, pad(15)] - - # Reorder to [burn_only, end, max, new_token] for storage write - movdn.3 - # => [max, end, burn_only, new_token, amount, pad(15)] - movup.2 # burn_only - movup.2 # end - swap - # => [burn_only, end, max, new_token, amount, pad(15)] - - push.SUPPLY_LIMITS_SLOT[0..2] - movdn.5 movdn.5 - - exec.native_account::set_item - # => [OLD(4), amount, pad(15)] - - dropw drop repeat.15 drop end - padw padw padw padw +#! Invocation: exec +pub proc check_supply + # new_supply = token_supply + amount + swap add + # => [token_supply + amount, max_supply, ...] + + # assert new_supply <= max_supply + lte assert.err=ERR_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY + # => [...] end -#! Returns the current supply state. +#! Asserts that burning `amount` would not reduce supply below zero. +#! +#! Inputs: [amount, token_supply, ...] +#! Outputs: [...] +#! +#! Panics if: amount > token_supply. #! -#! Inputs: [pad(16)] -#! Outputs: [token_supply, max_supply, distribution_end_block, burn_only_flag, pad(12)] -pub proc get_supply - exec.get_supply_state - # => [token, max, end, burn_only, pad(12)] +#! Invocation: exec +pub proc check_burn + # assert amount <= token_supply + lte assert.err=ERR_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY + # => [...] end diff --git a/crates/miden-standards/src/account/faucets/timed_fungible.rs b/crates/miden-standards/src/account/faucets/timed_fungible.rs index 6970c1bc74..7e0b07e546 100644 --- a/crates/miden-standards/src/account/faucets/timed_fungible.rs +++ b/crates/miden-standards/src/account/faucets/timed_fungible.rs @@ -9,6 +9,7 @@ use miden_protocol::account::{ Account, AccountBuilder, AccountComponent, + AccountId, AccountStorage, AccountStorageMode, AccountType, @@ -21,13 +22,7 @@ use miden_protocol::{Felt, FieldElement, Word}; use super::token_metadata::TOKEN_SYMBOL_TYPE_ID; use super::{FungibleFaucetError, TokenMetadata}; -use crate::account::AuthScheme; -use crate::account::auth::{ - AuthEcdsaK256KeccakAcl, - AuthEcdsaK256KeccakAclConfig, - AuthFalcon512RpoAcl, - AuthFalcon512RpoAclConfig, -}; +use crate::account::auth::NoAuth; use crate::account::components::timed_fungible_faucet_library; use crate::account::interface::{AccountComponentInterface, AccountInterface, AccountInterfaceExt}; use crate::procedure_digest; @@ -35,8 +30,13 @@ use crate::procedure_digest; // SLOT NAMES // ================================================================================================ -static SUPPLY_CONFIG_SLOT: LazyLock = LazyLock::new(|| { - StorageSlotName::new("miden::standards::supply::supply_limits::config") +static TIMED_CONFIG_SLOT: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::faucets::timed_fungible::config") + .expect("storage slot name should be valid") +}); + +static OWNER_CONFIG_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::access::ownable::owner_config") .expect("storage slot name should be valid") }); @@ -63,21 +63,20 @@ procedure_digest!( /// case when using [`CodeBuilder`][builder]. The procedures of this component are: /// - `distribute`, which mints assets and creates a note for the provided recipient within the /// allowed time window. -/// - `burn`, which burns the provided asset (respects burn-only mode). +/// - `burn`, which burns the provided asset. Burns are always allowed. /// /// This component supports accounts of type [`AccountType::FungibleFaucet`]. /// /// ## Storage Layout /// /// - [`Self::metadata_slot`]: Stores [`TokenMetadata`]. -/// - [`Self::supply_config_slot`]: Stores supply config `[token_supply, max_supply, -/// distribution_end, burn_only]`. +/// - [`Self::timed_config_slot`]: Stores timed config `[0, 0, 0, distribution_end]`. /// /// [builder]: crate::code_builder::CodeBuilder pub struct TimedFungibleFaucet { metadata: TokenMetadata, distribution_end: u32, - burn_only: bool, + owner_account_id: AccountId, } impl TimedFungibleFaucet { @@ -110,15 +109,19 @@ impl TimedFungibleFaucet { decimals: u8, max_supply: Felt, distribution_end: u32, - burn_only: bool, + owner_account_id: AccountId, ) -> Result { let metadata = TokenMetadata::new(symbol, decimals, max_supply)?; - Ok(Self { metadata, distribution_end, burn_only }) + Ok(Self { metadata, distribution_end, owner_account_id }) } /// Creates a new [`TimedFungibleFaucet`] component from the given [`TokenMetadata`]. - pub fn from_metadata(metadata: TokenMetadata, distribution_end: u32, burn_only: bool) -> Self { - Self { metadata, distribution_end, burn_only } + pub fn from_metadata( + metadata: TokenMetadata, + distribution_end: u32, + owner_account_id: AccountId, + ) -> Self { + Self { metadata, distribution_end, owner_account_id } } /// Attempts to create a new [`TimedFungibleFaucet`] component from the associated account @@ -145,18 +148,29 @@ impl TimedFungibleFaucet { let metadata = TokenMetadata::try_from(storage)?; - // Read supply config: [token_supply, max_supply, distribution_end, burn_only] + // Read timed config: [0, 0, 0, distribution_end] let config_word: Word = storage - .get_item(TimedFungibleFaucet::supply_config_slot()) + .get_item(TimedFungibleFaucet::timed_config_slot()) .map_err(|err| FungibleFaucetError::StorageLookupFailed { - slot_name: TimedFungibleFaucet::supply_config_slot().clone(), + slot_name: TimedFungibleFaucet::timed_config_slot().clone(), source: err, })?; - let distribution_end = config_word[2].as_int() as u32; - let burn_only = config_word[3].as_int() != 0; + let distribution_end = config_word[3].as_int() as u32; - Ok(Self { metadata, distribution_end, burn_only }) + // Read owner account ID: [0, 0, suffix, prefix] + let owner_account_id_word: Word = storage + .get_item(TimedFungibleFaucet::owner_config_slot()) + .map_err(|err| FungibleFaucetError::StorageLookupFailed { + slot_name: TimedFungibleFaucet::owner_config_slot().clone(), + source: err, + })?; + + let prefix = owner_account_id_word[3]; + let suffix = owner_account_id_word[2]; + let owner_account_id = AccountId::new_unchecked([prefix, suffix]); + + Ok(Self { metadata, distribution_end, owner_account_id }) } // PUBLIC ACCESSORS @@ -167,9 +181,9 @@ impl TimedFungibleFaucet { TokenMetadata::metadata_slot() } - /// Returns the [`StorageSlotName`] where the supply configuration is stored. - pub fn supply_config_slot() -> &'static StorageSlotName { - &SUPPLY_CONFIG_SLOT + /// Returns the [`StorageSlotName`] where the timed configuration is stored. + pub fn timed_config_slot() -> &'static StorageSlotName { + &TIMED_CONFIG_SLOT } /// Returns the storage slot schema for the metadata slot. @@ -189,22 +203,48 @@ impl TimedFungibleFaucet { ) } - /// Returns the storage slot schema for the supply config slot. - pub fn supply_config_slot_schema() -> (StorageSlotName, StorageSlotSchema) { + /// Returns the storage slot schema for the timed config slot. + pub fn timed_config_slot_schema() -> (StorageSlotName, StorageSlotSchema) { ( - Self::supply_config_slot().clone(), + Self::timed_config_slot().clone(), StorageSlotSchema::value( - "Supply Config", + "Timed Config", [ - FeltSchema::felt("token_supply").with_default(Felt::new(0)), - FeltSchema::felt("max_supply"), + FeltSchema::new_void(), + FeltSchema::new_void(), + FeltSchema::new_void(), FeltSchema::u32("distribution_end"), - FeltSchema::felt("burn_only_flag").with_default(Felt::new(0)), ], ), ) } + /// Returns the [`StorageSlotName`] where the owner configuration is stored. + pub fn owner_config_slot() -> &'static StorageSlotName { + &OWNER_CONFIG_SLOT_NAME + } + + /// Returns the storage slot schema for the owner configuration slot. + pub fn owner_config_slot_schema() -> (StorageSlotName, StorageSlotSchema) { + ( + Self::owner_config_slot().clone(), + StorageSlotSchema::value( + "Owner account configuration", + [ + FeltSchema::new_void(), + FeltSchema::new_void(), + FeltSchema::felt("owner_suffix"), + FeltSchema::felt("owner_prefix"), + ], + ), + ) + } + + /// Returns the owner account ID of the faucet. + pub fn owner_account_id(&self) -> AccountId { + self.owner_account_id + } + /// Returns the token metadata. pub fn metadata(&self) -> &TokenMetadata { &self.metadata @@ -235,11 +275,6 @@ impl TimedFungibleFaucet { self.distribution_end } - /// Returns whether the faucet is in burn-only mode after the distribution period. - pub fn burn_only(&self) -> bool { - self.burn_only - } - /// Returns the digest of the `distribute` account procedure. pub fn distribute_digest() -> Word { *TIMED_FUNGIBLE_FAUCET_DISTRIBUTE @@ -270,20 +305,34 @@ impl From for AccountComponent { let metadata_slot: StorageSlot = faucet.metadata.into(); let config_val = [ - Felt::ZERO, // token_supply starts at zero - faucet.metadata.max_supply(), + Felt::ZERO, + Felt::ZERO, + Felt::ZERO, Felt::new(faucet.distribution_end as u64), - Felt::new(faucet.burn_only as u64), ]; let config_slot = StorageSlot::with_value( - TimedFungibleFaucet::supply_config_slot().clone(), + TimedFungibleFaucet::timed_config_slot().clone(), Word::new(config_val), ); + let owner_account_id_word: Word = [ + Felt::new(0), + Felt::new(0), + faucet.owner_account_id.suffix(), + faucet.owner_account_id.prefix().as_felt(), + ] + .into(); + + let owner_slot = StorageSlot::with_value( + TimedFungibleFaucet::owner_config_slot().clone(), + owner_account_id_word, + ); + let storage_schema = StorageSchema::new([ TimedFungibleFaucet::metadata_slot_schema(), - TimedFungibleFaucet::supply_config_slot_schema(), + TimedFungibleFaucet::timed_config_slot_schema(), + TimedFungibleFaucet::owner_config_slot_schema(), ]) .expect("storage schema should be valid"); @@ -296,7 +345,7 @@ impl From for AccountComponent { AccountComponent::new( timed_fungible_faucet_library(), - vec![metadata_slot, config_slot], + vec![metadata_slot, config_slot, owner_slot], metadata, ) .expect("timed fungible faucet component should satisfy the requirements of a valid account component") @@ -324,72 +373,36 @@ impl TryFrom<&Account> for TimedFungibleFaucet { } /// Creates a new faucet account with timed fungible faucet interface, -/// account storage type, specified authentication scheme, and provided metadata (token symbol, +/// account storage type, owner account, and provided metadata (token symbol, /// decimals, max supply, distribution end block, burn-only flag). /// -/// The timed faucet interface exposes two procedures: +/// The timed faucet interface exposes procedures: /// - `distribute`, which mints assets and creates a note for the provided recipient within the -/// distribution time window. -/// - `burn`, which burns the provided asset (respects burn-only mode). +/// distribution time window. Requires the caller to be the owner. +/// - `burn`, which burns the provided asset. Burns are always allowed. +/// - `transfer_ownership`, which transfers ownership to a new account. +/// - `renounce_ownership`, which renounces ownership. /// -/// The `distribute` procedure can be called from a transaction script and requires authentication -/// via the specified authentication scheme. The `burn` procedure can only be called from a note -/// script and requires the calling note to contain the asset to be burned. +/// The `distribute` procedure can only be called by the owner of the faucet. The `burn` procedure +/// can be called by anyone and requires the calling note to contain the asset to be burned. pub fn create_timed_fungible_faucet( init_seed: [u8; 32], symbol: TokenSymbol, decimals: u8, max_supply: Felt, distribution_end: u32, - burn_only: bool, storage_mode: AccountStorageMode, - auth_scheme: AuthScheme, + owner_account_id: AccountId, ) -> Result { - let distribute_proc_root = TimedFungibleFaucet::distribute_digest(); - - let auth_component: AccountComponent = match auth_scheme { - AuthScheme::Falcon512Rpo { pub_key } => AuthFalcon512RpoAcl::new( - pub_key, - AuthFalcon512RpoAclConfig::new() - .with_auth_trigger_procedures(vec![distribute_proc_root]) - .with_allow_unauthorized_input_notes(true), - ) - .map_err(FungibleFaucetError::AccountError)? - .into(), - AuthScheme::EcdsaK256Keccak { pub_key } => AuthEcdsaK256KeccakAcl::new( - pub_key, - AuthEcdsaK256KeccakAclConfig::new() - .with_auth_trigger_procedures(vec![distribute_proc_root]) - .with_allow_unauthorized_input_notes(true), - ) - .map_err(FungibleFaucetError::AccountError)? - .into(), - AuthScheme::NoAuth => { - return Err(FungibleFaucetError::UnsupportedAuthScheme( - "timed fungible faucets cannot be created with NoAuth authentication scheme".into(), - )); - }, - AuthScheme::Falcon512RpoMultisig { threshold: _, pub_keys: _ } => { - return Err(FungibleFaucetError::UnsupportedAuthScheme( - "timed fungible faucets do not support multisig authentication".into(), - )); - }, - AuthScheme::Unknown => { - return Err(FungibleFaucetError::UnsupportedAuthScheme( - "timed fungible faucets cannot be created with Unknown authentication scheme" - .into(), - )); - }, - AuthScheme::EcdsaK256KeccakMultisig { threshold: _, pub_keys: _ } => { - return Err(FungibleFaucetError::UnsupportedAuthScheme( - "timed fungible faucets do not support EcdsaK256KeccakMultisig authentication" - .into(), - )); - }, - }; + let auth_component: AccountComponent = NoAuth::new().into(); - let faucet_component = - TimedFungibleFaucet::new(symbol, decimals, max_supply, distribution_end, burn_only)?; + let faucet_component = TimedFungibleFaucet::new( + symbol, + decimals, + max_supply, + distribution_end, + owner_account_id, + )?; let account = AccountBuilder::new(init_seed) .account_type(AccountType::FungibleFaucet) @@ -409,13 +422,14 @@ pub fn create_timed_fungible_faucet( mod tests { use assert_matches::assert_matches; use miden_protocol::account::auth::PublicKeyCommitment; - use miden_protocol::{FieldElement, ONE, Word}; + use miden_protocol::testing::account_id::ACCOUNT_ID_SENDER; + use miden_protocol::{FieldElement, Word}; use super::{ AccountBuilder, + AccountId, AccountStorageMode, AccountType, - AuthScheme, Felt, FungibleFaucetError, TimedFungibleFaucet, @@ -425,10 +439,13 @@ mod tests { use crate::account::auth::AuthFalcon512Rpo; use crate::account::wallets::BasicWallet; + fn mock_owner_account_id() -> AccountId { + ACCOUNT_ID_SENDER.try_into().expect("valid account id") + } + #[test] fn timed_faucet_contract_creation() { - let pub_key_word = Word::new([ONE; 4]); - let auth_scheme: AuthScheme = AuthScheme::Falcon512Rpo { pub_key: pub_key_word.into() }; + let owner_account_id = mock_owner_account_id(); let init_seed: [u8; 32] = [ 90, 110, 209, 94, 84, 105, 250, 242, 223, 203, 216, 124, 22, 159, 14, 132, 215, 85, @@ -439,7 +456,6 @@ mod tests { let token_symbol = TokenSymbol::try_from("TMD").unwrap(); let decimals = 6u8; let distribution_end = 10_000u32; - let burn_only = true; let storage_mode = AccountStorageMode::Private; let faucet_account = create_timed_fungible_faucet( @@ -448,9 +464,8 @@ mod tests { decimals, max_supply, distribution_end, - burn_only, storage_mode, - auth_scheme, + owner_account_id, ) .unwrap(); @@ -463,17 +478,32 @@ mod tests { [Felt::ZERO, max_supply, Felt::new(6), token_symbol.into()].into() ); - // Check supply config slot + // Check timed config slot assert_eq!( faucet_account .storage() - .get_item(TimedFungibleFaucet::supply_config_slot()) + .get_item(TimedFungibleFaucet::timed_config_slot()) .unwrap(), [ Felt::ZERO, - max_supply, + Felt::ZERO, + Felt::ZERO, Felt::new(distribution_end as u64), - Felt::new(burn_only as u64) + ] + .into() + ); + + // Check owner config slot + assert_eq!( + faucet_account + .storage() + .get_item(TimedFungibleFaucet::owner_config_slot()) + .unwrap(), + [ + Felt::new(0), + Felt::new(0), + owner_account_id.suffix(), + owner_account_id.prefix().as_felt(), ] .into() ); @@ -485,7 +515,7 @@ mod tests { assert_eq!(faucet_component.max_supply(), max_supply); assert_eq!(faucet_component.token_supply(), Felt::ZERO); assert_eq!(faucet_component.distribution_end(), distribution_end); - assert_eq!(faucet_component.burn_only(), burn_only); + assert_eq!(faucet_component.owner_account_id(), owner_account_id); } #[test] @@ -493,13 +523,20 @@ mod tests { let mock_word = Word::from([0, 1, 2, 3u32]); let mock_public_key = PublicKeyCommitment::from(mock_word); let mock_seed = mock_word.as_bytes(); + let owner_account_id = mock_owner_account_id(); let token_symbol = TokenSymbol::new("TMD").expect("invalid token symbol"); let faucet_account = AccountBuilder::new(mock_seed) .account_type(AccountType::FungibleFaucet) .with_component( - TimedFungibleFaucet::new(token_symbol, 6, Felt::new(1_000_000), 10_000, true) - .expect("failed to create a timed fungible faucet component"), + TimedFungibleFaucet::new( + token_symbol, + 6, + Felt::new(1_000_000), + 10_000, + owner_account_id, + ) + .expect("failed to create a timed fungible faucet component"), ) .with_auth_component(AuthFalcon512Rpo::new(mock_public_key)) .build_existing() @@ -512,7 +549,7 @@ mod tests { assert_eq!(timed_ff.max_supply(), Felt::new(1_000_000)); assert_eq!(timed_ff.token_supply(), Felt::ZERO); assert_eq!(timed_ff.distribution_end(), 10_000); - assert!(timed_ff.burn_only()); + assert_eq!(timed_ff.owner_account_id(), owner_account_id); // invalid account: timed fungible faucet component is missing let invalid_faucet_account = AccountBuilder::new(mock_seed) diff --git a/crates/miden-standards/src/account/faucets/unlimited_fungible.rs b/crates/miden-standards/src/account/faucets/unlimited_fungible.rs index cb19f4ea62..13b2734cc5 100644 --- a/crates/miden-standards/src/account/faucets/unlimited_fungible.rs +++ b/crates/miden-standards/src/account/faucets/unlimited_fungible.rs @@ -9,6 +9,7 @@ use miden_protocol::account::{ Account, AccountBuilder, AccountComponent, + AccountId, AccountStorage, AccountStorageMode, AccountType, @@ -16,21 +17,24 @@ use miden_protocol::account::{ StorageSlotName, }; use miden_protocol::asset::TokenSymbol; +use miden_protocol::utils::sync::LazyLock; use miden_protocol::{Felt, Word}; use super::token_metadata::TOKEN_SYMBOL_TYPE_ID; use super::{FungibleFaucetError, TokenMetadata}; -use crate::account::AuthScheme; -use crate::account::auth::{ - AuthEcdsaK256KeccakAcl, - AuthEcdsaK256KeccakAclConfig, - AuthFalcon512RpoAcl, - AuthFalcon512RpoAclConfig, -}; +use crate::account::auth::NoAuth; use crate::account::components::unlimited_fungible_faucet_library; use crate::account::interface::{AccountComponentInterface, AccountInterface, AccountInterfaceExt}; use crate::procedure_digest; +// SLOT NAMES +// ================================================================================================ + +static OWNER_CONFIG_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::access::ownable::owner_config") + .expect("storage slot name should be valid") +}); + // UNLIMITED FUNGIBLE FAUCET ACCOUNT COMPONENT // ================================================================================================ @@ -67,6 +71,7 @@ procedure_digest!( /// [builder]: crate::code_builder::CodeBuilder pub struct UnlimitedFungibleFaucet { metadata: TokenMetadata, + owner_account_id: AccountId, } impl UnlimitedFungibleFaucet { @@ -95,15 +100,19 @@ impl UnlimitedFungibleFaucet { /// /// Returns an error if: /// - the decimals parameter exceeds maximum value of [`Self::MAX_DECIMALS`]. - pub fn new(symbol: TokenSymbol, decimals: u8) -> Result { + pub fn new( + symbol: TokenSymbol, + decimals: u8, + owner_account_id: AccountId, + ) -> Result { let max_supply = miden_protocol::asset::FungibleAsset::MAX_AMOUNT; let metadata = TokenMetadata::new(symbol, decimals, Felt::new(max_supply))?; - Ok(Self { metadata }) + Ok(Self { metadata, owner_account_id }) } /// Creates a new [`UnlimitedFungibleFaucet`] component from the given [`TokenMetadata`]. - pub fn from_metadata(metadata: TokenMetadata) -> Self { - Self { metadata } + pub fn from_metadata(metadata: TokenMetadata, owner_account_id: AccountId) -> Self { + Self { metadata, owner_account_id } } /// Attempts to create a new [`UnlimitedFungibleFaucet`] component from the associated account @@ -132,7 +141,20 @@ impl UnlimitedFungibleFaucet { } let metadata = TokenMetadata::try_from(storage)?; - Ok(Self { metadata }) + + // Read owner account ID: [0, 0, suffix, prefix] + let owner_account_id_word: Word = storage + .get_item(UnlimitedFungibleFaucet::owner_config_slot()) + .map_err(|err| FungibleFaucetError::StorageLookupFailed { + slot_name: UnlimitedFungibleFaucet::owner_config_slot().clone(), + source: err, + })?; + + let prefix = owner_account_id_word[3]; + let suffix = owner_account_id_word[2]; + let owner_account_id = AccountId::new_unchecked([prefix, suffix]); + + Ok(Self { metadata, owner_account_id }) } // PUBLIC ACCESSORS @@ -160,6 +182,32 @@ impl UnlimitedFungibleFaucet { ) } + /// Returns the [`StorageSlotName`] where the owner configuration is stored. + pub fn owner_config_slot() -> &'static StorageSlotName { + &OWNER_CONFIG_SLOT_NAME + } + + /// Returns the storage slot schema for the owner configuration slot. + pub fn owner_config_slot_schema() -> (StorageSlotName, StorageSlotSchema) { + ( + Self::owner_config_slot().clone(), + StorageSlotSchema::value( + "Owner account configuration", + [ + FeltSchema::new_void(), + FeltSchema::new_void(), + FeltSchema::felt("owner_suffix"), + FeltSchema::felt("owner_prefix"), + ], + ), + ) + } + + /// Returns the owner account ID of the faucet. + pub fn owner_account_id(&self) -> AccountId { + self.owner_account_id + } + /// Returns the token metadata. pub fn metadata(&self) -> &TokenMetadata { &self.metadata @@ -212,18 +260,38 @@ impl UnlimitedFungibleFaucet { impl From for AccountComponent { fn from(faucet: UnlimitedFungibleFaucet) -> Self { - let storage_slot: StorageSlot = faucet.metadata.into(); + let metadata_slot: StorageSlot = faucet.metadata.into(); + + let owner_account_id_word: Word = [ + Felt::new(0), + Felt::new(0), + faucet.owner_account_id.suffix(), + faucet.owner_account_id.prefix().as_felt(), + ] + .into(); + + let owner_slot = StorageSlot::with_value( + UnlimitedFungibleFaucet::owner_config_slot().clone(), + owner_account_id_word, + ); - let storage_schema = StorageSchema::new([UnlimitedFungibleFaucet::metadata_slot_schema()]) - .expect("storage schema should be valid"); + let storage_schema = StorageSchema::new([ + UnlimitedFungibleFaucet::metadata_slot_schema(), + UnlimitedFungibleFaucet::owner_config_slot_schema(), + ]) + .expect("storage schema should be valid"); let metadata = AccountComponentMetadata::new(UnlimitedFungibleFaucet::NAME) .with_description("Unlimited fungible faucet component for minting and burning tokens") .with_supported_type(AccountType::FungibleFaucet) .with_storage_schema(storage_schema); - AccountComponent::new(unlimited_fungible_faucet_library(), vec![storage_slot], metadata) - .expect("unlimited fungible faucet component should satisfy the requirements of a valid account component") + AccountComponent::new( + unlimited_fungible_faucet_library(), + vec![metadata_slot, owner_slot], + metadata, + ) + .expect("unlimited fungible faucet component should satisfy the requirements of a valid account component") } } @@ -248,12 +316,14 @@ impl TryFrom<&Account> for UnlimitedFungibleFaucet { } /// Creates a new faucet account with unlimited fungible faucet interface, -/// account storage type, specified authentication scheme, and provided metadata (token symbol, -/// decimals). +/// account storage type, owner account, and provided metadata (token symbol, decimals). /// -/// The unlimited faucet interface exposes two procedures: +/// The unlimited faucet interface exposes procedures: /// - `distribute`, which mints assets and creates a note for the provided recipient. -/// - `burn`, which burns the provided asset. +/// Requires the caller to be the owner. +/// - `burn`, which burns the provided asset. No ownership check. +/// - `transfer_ownership`, which transfers ownership to a new account. +/// - `renounce_ownership`, which renounces ownership. /// /// No supply checks are enforced at runtime. pub fn create_unlimited_fungible_faucet( @@ -261,53 +331,11 @@ pub fn create_unlimited_fungible_faucet( symbol: TokenSymbol, decimals: u8, storage_mode: AccountStorageMode, - auth_scheme: AuthScheme, + owner_account_id: AccountId, ) -> Result { - let distribute_proc_root = UnlimitedFungibleFaucet::distribute_digest(); - - let auth_component: AccountComponent = match auth_scheme { - AuthScheme::Falcon512Rpo { pub_key } => AuthFalcon512RpoAcl::new( - pub_key, - AuthFalcon512RpoAclConfig::new() - .with_auth_trigger_procedures(vec![distribute_proc_root]) - .with_allow_unauthorized_input_notes(true), - ) - .map_err(FungibleFaucetError::AccountError)? - .into(), - AuthScheme::EcdsaK256Keccak { pub_key } => AuthEcdsaK256KeccakAcl::new( - pub_key, - AuthEcdsaK256KeccakAclConfig::new() - .with_auth_trigger_procedures(vec![distribute_proc_root]) - .with_allow_unauthorized_input_notes(true), - ) - .map_err(FungibleFaucetError::AccountError)? - .into(), - AuthScheme::NoAuth => { - return Err(FungibleFaucetError::UnsupportedAuthScheme( - "unlimited fungible faucets cannot be created with NoAuth authentication scheme" - .into(), - )); - }, - AuthScheme::Falcon512RpoMultisig { threshold: _, pub_keys: _ } => { - return Err(FungibleFaucetError::UnsupportedAuthScheme( - "unlimited fungible faucets do not support multisig authentication".into(), - )); - }, - AuthScheme::Unknown => { - return Err(FungibleFaucetError::UnsupportedAuthScheme( - "unlimited fungible faucets cannot be created with Unknown authentication scheme" - .into(), - )); - }, - AuthScheme::EcdsaK256KeccakMultisig { threshold: _, pub_keys: _ } => { - return Err(FungibleFaucetError::UnsupportedAuthScheme( - "unlimited fungible faucets do not support EcdsaK256KeccakMultisig authentication" - .into(), - )); - }, - }; + let auth_component: AccountComponent = NoAuth::new().into(); - let faucet_component = UnlimitedFungibleFaucet::new(symbol, decimals)?; + let faucet_component = UnlimitedFungibleFaucet::new(symbol, decimals, owner_account_id)?; let account = AccountBuilder::new(init_seed) .account_type(AccountType::FungibleFaucet) @@ -328,13 +356,14 @@ mod tests { use assert_matches::assert_matches; use miden_protocol::account::auth::PublicKeyCommitment; use miden_protocol::asset::FungibleAsset; - use miden_protocol::{FieldElement, ONE, Word}; + use miden_protocol::testing::account_id::ACCOUNT_ID_SENDER; + use miden_protocol::{FieldElement, Word}; use super::{ AccountBuilder, + AccountId, AccountStorageMode, AccountType, - AuthScheme, Felt, FungibleFaucetError, TokenSymbol, @@ -344,10 +373,13 @@ mod tests { use crate::account::auth::AuthFalcon512Rpo; use crate::account::wallets::BasicWallet; + fn mock_owner_account_id() -> AccountId { + ACCOUNT_ID_SENDER.try_into().expect("valid account id") + } + #[test] fn unlimited_faucet_contract_creation() { - let pub_key_word = Word::new([ONE; 4]); - let auth_scheme: AuthScheme = AuthScheme::Falcon512Rpo { pub_key: pub_key_word.into() }; + let owner_account_id = mock_owner_account_id(); let init_seed: [u8; 32] = [ 90, 110, 209, 94, 84, 105, 250, 242, 223, 203, 216, 124, 22, 159, 14, 132, 215, 85, @@ -363,7 +395,7 @@ mod tests { token_symbol, decimals, storage_mode, - auth_scheme, + owner_account_id, ) .unwrap(); @@ -385,12 +417,28 @@ mod tests { .into() ); + // Check owner config slot + assert_eq!( + faucet_account + .storage() + .get_item(UnlimitedFungibleFaucet::owner_config_slot()) + .unwrap(), + [ + Felt::new(0), + Felt::new(0), + owner_account_id.suffix(), + owner_account_id.prefix().as_felt(), + ] + .into() + ); + // Verify the faucet can be extracted via TryFrom let faucet_component = UnlimitedFungibleFaucet::try_from(faucet_account.clone()).unwrap(); assert_eq!(faucet_component.symbol(), token_symbol); assert_eq!(faucet_component.decimals(), decimals); assert_eq!(faucet_component.max_supply(), Felt::new(FungibleAsset::MAX_AMOUNT)); assert_eq!(faucet_component.token_supply(), Felt::ZERO); + assert_eq!(faucet_component.owner_account_id(), owner_account_id); } #[test] @@ -398,12 +446,13 @@ mod tests { let mock_word = Word::from([0, 1, 2, 3u32]); let mock_public_key = PublicKeyCommitment::from(mock_word); let mock_seed = mock_word.as_bytes(); + let owner_account_id = mock_owner_account_id(); let token_symbol = TokenSymbol::new("UNL").expect("invalid token symbol"); let faucet_account = AccountBuilder::new(mock_seed) .account_type(AccountType::FungibleFaucet) .with_component( - UnlimitedFungibleFaucet::new(token_symbol, 8) + UnlimitedFungibleFaucet::new(token_symbol, 8, owner_account_id) .expect("failed to create an unlimited fungible faucet component"), ) .with_auth_component(AuthFalcon512Rpo::new(mock_public_key)) @@ -415,6 +464,7 @@ mod tests { assert_eq!(unlimited_ff.symbol(), token_symbol); assert_eq!(unlimited_ff.decimals(), 8); assert_eq!(unlimited_ff.token_supply(), Felt::ZERO); + assert_eq!(unlimited_ff.owner_account_id(), owner_account_id); // invalid account: unlimited fungible faucet component is missing let invalid_faucet_account = AccountBuilder::new(mock_seed) diff --git a/crates/miden-standards/src/errors/standards.rs b/crates/miden-standards/src/errors/standards.rs index bea02711a4..d7a859ce40 100644 --- a/crates/miden-standards/src/errors/standards.rs +++ b/crates/miden-standards/src/errors/standards.rs @@ -12,17 +12,6 @@ use miden_protocol::errors::MasmError; /// Error Message: "burn requires exactly 1 note asset" pub const ERR_BASIC_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS: MasmError = MasmError::from_static_str("burn requires exactly 1 note asset"); -/// Error Message: "asset amount to burn exceeds the existing token supply" -pub const ERR_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY: MasmError = MasmError::from_static_str("asset amount to burn exceeds the existing token supply"); -/// Error Message: "burn operations are not allowed during distribution period" -pub const ERR_BURN_NOT_ALLOWED: MasmError = MasmError::from_static_str("burn operations are not allowed during distribution period"); - -/// Error Message: "token_supply plus the amount passed to distribute would exceed the maximum supply" -pub const ERR_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY: MasmError = MasmError::from_static_str("token_supply plus the amount passed to distribute would exceed the maximum supply"); - -/// Error Message: "distribution period has ended" -pub const ERR_DISTRIBUTION_PERIOD_ENDED: MasmError = MasmError::from_static_str("distribution period has ended"); - /// Error Message: "asset amount to burn exceeds the existing token supply" pub const ERR_FAUCET_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY: MasmError = MasmError::from_static_str("asset amount to burn exceeds the existing token supply"); @@ -64,22 +53,18 @@ pub const ERR_P2ID_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS: MasmError = MasmError::fr /// Error Message: "note sender is not the owner" pub const ERR_SENDER_NOT_OWNER: MasmError = MasmError::from_static_str("note sender is not the owner"); -/// Error Message: "token supply exceeds max supply" -pub const ERR_SUPPLY_EXCEEDED: MasmError = MasmError::from_static_str("token supply exceeds max supply"); - /// Error Message: "SWAP script expects exactly 16 note storage items" pub const ERR_SWAP_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS: MasmError = MasmError::from_static_str("SWAP script expects exactly 16 note storage items"); /// Error Message: "SWAP script requires exactly 1 note asset" pub const ERR_SWAP_WRONG_NUMBER_OF_ASSETS: MasmError = MasmError::from_static_str("SWAP script requires exactly 1 note asset"); -/// Error Message: "burn requires exactly 1 note asset" -pub const ERR_TIMED_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS: MasmError = MasmError::from_static_str("burn requires exactly 1 note asset"); +/// Error Message: "burn operations are not allowed during distribution period" +pub const ERR_TIMED_FUNGIBLE_BURN_NOT_ALLOWED: MasmError = MasmError::from_static_str("burn operations are not allowed during distribution period"); +/// Error Message: "distribution period has ended" +pub const ERR_TIMED_FUNGIBLE_DISTRIBUTION_PERIOD_ENDED: MasmError = MasmError::from_static_str("distribution period has ended"); /// Error Message: "failed to approve multisig transaction as it was already executed" pub const ERR_TX_ALREADY_EXECUTED: MasmError = MasmError::from_static_str("failed to approve multisig transaction as it was already executed"); -/// Error Message: "burn requires exactly 1 note asset" -pub const ERR_UNLIMITED_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS: MasmError = MasmError::from_static_str("burn requires exactly 1 note asset"); - /// Error Message: "number of approvers or threshold must not be zero" pub const ERR_ZERO_IN_MULTISIG_CONFIG: MasmError = MasmError::from_static_str("number of approvers or threshold must not be zero"); From 4ad9388572326aea245480784a2ca3a0e9000473 Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Thu, 19 Feb 2026 10:41:02 -0300 Subject: [PATCH 06/10] feat: enhance error handling in fungible faucet components and improve code formatting --- .../src/account/faucets/timed_fungible.rs | 43 +++++++++---------- .../src/account/faucets/unlimited_fungible.rs | 4 +- .../miden-standards/src/errors/standards.rs | 11 ++++- 3 files changed, 31 insertions(+), 27 deletions(-) diff --git a/crates/miden-standards/src/account/faucets/timed_fungible.rs b/crates/miden-standards/src/account/faucets/timed_fungible.rs index 7e0b07e546..96da8585da 100644 --- a/crates/miden-standards/src/account/faucets/timed_fungible.rs +++ b/crates/miden-standards/src/account/faucets/timed_fungible.rs @@ -112,7 +112,11 @@ impl TimedFungibleFaucet { owner_account_id: AccountId, ) -> Result { let metadata = TokenMetadata::new(symbol, decimals, max_supply)?; - Ok(Self { metadata, distribution_end, owner_account_id }) + Ok(Self { + metadata, + distribution_end, + owner_account_id, + }) } /// Creates a new [`TimedFungibleFaucet`] component from the given [`TokenMetadata`]. @@ -121,7 +125,11 @@ impl TimedFungibleFaucet { distribution_end: u32, owner_account_id: AccountId, ) -> Self { - Self { metadata, distribution_end, owner_account_id } + Self { + metadata, + distribution_end, + owner_account_id, + } } /// Attempts to create a new [`TimedFungibleFaucet`] component from the associated account @@ -170,7 +178,11 @@ impl TimedFungibleFaucet { let suffix = owner_account_id_word[2]; let owner_account_id = AccountId::new_unchecked([prefix, suffix]); - Ok(Self { metadata, distribution_end, owner_account_id }) + Ok(Self { + metadata, + distribution_end, + owner_account_id, + }) } // PUBLIC ACCESSORS @@ -304,12 +316,8 @@ impl From for AccountComponent { fn from(faucet: TimedFungibleFaucet) -> Self { let metadata_slot: StorageSlot = faucet.metadata.into(); - let config_val = [ - Felt::ZERO, - Felt::ZERO, - Felt::ZERO, - Felt::new(faucet.distribution_end as u64), - ]; + let config_val = + [Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::new(faucet.distribution_end as u64)]; let config_slot = StorageSlot::with_value( TimedFungibleFaucet::timed_config_slot().clone(), @@ -396,13 +404,8 @@ pub fn create_timed_fungible_faucet( ) -> Result { let auth_component: AccountComponent = NoAuth::new().into(); - let faucet_component = TimedFungibleFaucet::new( - symbol, - decimals, - max_supply, - distribution_end, - owner_account_id, - )?; + let faucet_component = + TimedFungibleFaucet::new(symbol, decimals, max_supply, distribution_end, owner_account_id)?; let account = AccountBuilder::new(init_seed) .account_type(AccountType::FungibleFaucet) @@ -484,13 +487,7 @@ mod tests { .storage() .get_item(TimedFungibleFaucet::timed_config_slot()) .unwrap(), - [ - Felt::ZERO, - Felt::ZERO, - Felt::ZERO, - Felt::new(distribution_end as u64), - ] - .into() + [Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::new(distribution_end as u64),].into() ); // Check owner config slot diff --git a/crates/miden-standards/src/account/faucets/unlimited_fungible.rs b/crates/miden-standards/src/account/faucets/unlimited_fungible.rs index 13b2734cc5..5bb0ca39ff 100644 --- a/crates/miden-standards/src/account/faucets/unlimited_fungible.rs +++ b/crates/miden-standards/src/account/faucets/unlimited_fungible.rs @@ -319,8 +319,8 @@ impl TryFrom<&Account> for UnlimitedFungibleFaucet { /// account storage type, owner account, and provided metadata (token symbol, decimals). /// /// The unlimited faucet interface exposes procedures: -/// - `distribute`, which mints assets and creates a note for the provided recipient. -/// Requires the caller to be the owner. +/// - `distribute`, which mints assets and creates a note for the provided recipient. Requires the +/// caller to be the owner. /// - `burn`, which burns the provided asset. No ownership check. /// - `transfer_ownership`, which transfers ownership to a new account. /// - `renounce_ownership`, which renounces ownership. diff --git a/crates/miden-standards/src/errors/standards.rs b/crates/miden-standards/src/errors/standards.rs index d7a859ce40..5784102844 100644 --- a/crates/miden-standards/src/errors/standards.rs +++ b/crates/miden-standards/src/errors/standards.rs @@ -12,6 +12,15 @@ use miden_protocol::errors::MasmError; /// Error Message: "burn requires exactly 1 note asset" pub const ERR_BASIC_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS: MasmError = MasmError::from_static_str("burn requires exactly 1 note asset"); +/// Error Message: "burn amount exceeds the existing token supply" +pub const ERR_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY: MasmError = MasmError::from_static_str("burn amount exceeds the existing token supply"); + +/// Error Message: "amount would exceed the maximum supply" +pub const ERR_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY: MasmError = MasmError::from_static_str("amount would exceed the maximum supply"); + +/// Error Message: "distribution period has ended" +pub const ERR_DISTRIBUTION_PERIOD_ENDED: MasmError = MasmError::from_static_str("distribution period has ended"); + /// Error Message: "asset amount to burn exceeds the existing token supply" pub const ERR_FAUCET_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY: MasmError = MasmError::from_static_str("asset amount to burn exceeds the existing token supply"); @@ -58,8 +67,6 @@ pub const ERR_SWAP_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS: MasmError = MasmError::fr /// Error Message: "SWAP script requires exactly 1 note asset" pub const ERR_SWAP_WRONG_NUMBER_OF_ASSETS: MasmError = MasmError::from_static_str("SWAP script requires exactly 1 note asset"); -/// Error Message: "burn operations are not allowed during distribution period" -pub const ERR_TIMED_FUNGIBLE_BURN_NOT_ALLOWED: MasmError = MasmError::from_static_str("burn operations are not allowed during distribution period"); /// Error Message: "distribution period has ended" pub const ERR_TIMED_FUNGIBLE_DISTRIBUTION_PERIOD_ENDED: MasmError = MasmError::from_static_str("distribution period has ended"); From 4466e8241b0e4ceb4362844d90bfe307fcc2a460 Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Thu, 19 Feb 2026 11:32:22 -0300 Subject: [PATCH 07/10] feat: integrate supply limits checks in fungible faucet contracts and update error messages --- .../asm/standards/faucets/mod.masm | 14 ++++---------- .../asm/standards/faucets/timed_fungible.masm | 19 ++++--------------- .../standards/faucets/unlimited_fungible.masm | 3 ++- .../asm/standards/supply/supply_limits.masm | 11 ++++++++--- .../src/account/faucets/network_fungible.rs | 4 +--- .../src/account/faucets/timed_fungible.rs | 3 ++- .../src/account/faucets/unlimited_fungible.rs | 1 + .../miden-standards/src/errors/standards.rs | 10 ++-------- crates/miden-testing/tests/scripts/faucet.rs | 4 ++-- 9 files changed, 26 insertions(+), 43 deletions(-) diff --git a/crates/miden-standards/asm/standards/faucets/mod.masm b/crates/miden-standards/asm/standards/faucets/mod.masm index 5451bb9655..0dfaeb23f7 100644 --- a/crates/miden-standards/asm/standards/faucets/mod.masm +++ b/crates/miden-standards/asm/standards/faucets/mod.masm @@ -4,13 +4,9 @@ use miden::protocol::faucet use miden::protocol::native_account use miden::protocol::output_note use ::miden::protocol::asset::FUNGIBLE_ASSET_MAX_AMOUNT +use miden::standards::supply::supply_limits -# CONSTANTS -# ================================================================================================= - -const PRIVATE_NOTE=2 - -# ERRORS +# ERRORS # ================================================================================================= const ERR_FUNGIBLE_ASSET_TOKEN_SUPPLY_EXCEEDS_MAX_SUPPLY="token supply exceeds max supply" @@ -19,11 +15,9 @@ const ERR_FUNGIBLE_ASSET_MAX_SUPPLY_EXCEEDS_FUNGIBLE_ASSET_MAX_AMOUNT="max suppl const ERR_FUNGIBLE_ASSET_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY="token_supply plus the amount passed to distribute would exceed the maximum supply" -const ERR_FAUCET_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY="asset amount to burn exceeds the existing token supply" - const ERR_BASIC_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS="burn requires exactly 1 note asset" -# CONSTANTS +# STORAGE LAYOUT # ================================================================================================= # The local memory address at which the metadata slot content is stored. @@ -208,7 +202,7 @@ pub proc burn # => [token_supply, amount, token_symbol, decimals, max_supply, token_supply, amount, pad(16)] # assert that amount <= token_supply - lte assert.err=ERR_FAUCET_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY + exec.supply_limits::check_burn # => [token_symbol, decimals, max_supply, token_supply, amount, pad(16)] movup.3 movup.4 diff --git a/crates/miden-standards/asm/standards/faucets/timed_fungible.masm b/crates/miden-standards/asm/standards/faucets/timed_fungible.masm index 6ee279ed15..e992714a75 100644 --- a/crates/miden-standards/asm/standards/faucets/timed_fungible.masm +++ b/crates/miden-standards/asm/standards/faucets/timed_fungible.masm @@ -6,20 +6,15 @@ # ================================================================================================= use miden::protocol::active_account -use miden::protocol::tx use miden::standards::faucets use miden::standards::access::ownable +use miden::standards::supply::supply_limits # CONSTANTS # ================================================================================================= const TIMED_CONFIG_SLOT = word("miden::standards::faucets::timed_fungible::config") -# ERRORS -# ================================================================================================= - -const ERR_TIMED_FUNGIBLE_DISTRIBUTION_PERIOD_ENDED="distribution period has ended" - # OWNER MANAGEMENT # ================================================================================================= @@ -59,16 +54,10 @@ pub proc distribute push.TIMED_CONFIG_SLOT[0..2] exec.active_account::get_item # => [distribution_end, 0, 0, 0, amount, tag, note_type, RECIPIENT] - dup - if.true - exec.tx::get_block_number - dup.1 - # => [distribution_end, current_block, distribution_end, 0, 0, 0, ...] - lt assert.err=ERR_TIMED_FUNGIBLE_DISTRIBUTION_PERIOD_ENDED - # => [distribution_end, 0, 0, 0, ...] - end + exec.supply_limits::check_distribution_period + # => [0, 0, 0, amount, tag, note_type, RECIPIENT] - dropw + drop drop drop # => [amount, tag, note_type, RECIPIENT] exec.faucets::distribute diff --git a/crates/miden-standards/asm/standards/faucets/unlimited_fungible.masm b/crates/miden-standards/asm/standards/faucets/unlimited_fungible.masm index 1fe5215c75..33a41e4678 100644 --- a/crates/miden-standards/asm/standards/faucets/unlimited_fungible.masm +++ b/crates/miden-standards/asm/standards/faucets/unlimited_fungible.masm @@ -47,7 +47,8 @@ end #! Burns the fungible asset from the active note. #! -#! Burning the asset removes it from circulation. No supply tracking is performed. +#! Burning the asset removes it from circulation and updates the token supply. +#! Delegates to the shared burn procedure which handles supply tracking. #! #! Inputs: [pad(16)] #! Outputs: [pad(16)] diff --git a/crates/miden-standards/asm/standards/supply/supply_limits.masm b/crates/miden-standards/asm/standards/supply/supply_limits.masm index 3fbd363d51..1abb166928 100644 --- a/crates/miden-standards/asm/standards/supply/supply_limits.masm +++ b/crates/miden-standards/asm/standards/supply/supply_limits.masm @@ -10,7 +10,7 @@ use miden::protocol::tx const ERR_DISTRIBUTION_PERIOD_ENDED = "distribution period has ended" const ERR_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY = "amount would exceed the maximum supply" -const ERR_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY = "burn amount exceeds the existing token supply" +const ERR_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY = "asset amount to burn exceeds the existing token supply" # PROCEDURES # ================================================================================================= @@ -49,16 +49,20 @@ end pub proc check_supply # new_supply = token_supply + amount swap add - # => [token_supply + amount, max_supply, ...] + # => [new_supply, max_supply, ...] + + swap + # => [max_supply, new_supply, ...] # assert new_supply <= max_supply + # (b = max_supply, a = new_supply; lte checks a <= b) lte assert.err=ERR_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY # => [...] end #! Asserts that burning `amount` would not reduce supply below zero. #! -#! Inputs: [amount, token_supply, ...] +#! Inputs: [token_supply, amount, ...] #! Outputs: [...] #! #! Panics if: amount > token_supply. @@ -66,6 +70,7 @@ end #! Invocation: exec pub proc check_burn # assert amount <= token_supply + # (b = token_supply, a = amount; lte checks a <= b) lte assert.err=ERR_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY # => [...] end diff --git a/crates/miden-standards/src/account/faucets/network_fungible.rs b/crates/miden-standards/src/account/faucets/network_fungible.rs index db53a10dff..e753157af5 100644 --- a/crates/miden-standards/src/account/faucets/network_fungible.rs +++ b/crates/miden-standards/src/account/faucets/network_fungible.rs @@ -20,12 +20,10 @@ use miden_protocol::asset::TokenSymbol; use miden_protocol::utils::sync::LazyLock; use miden_protocol::{Felt, Word}; +use super::token_metadata::TOKEN_SYMBOL_TYPE_ID; use super::{FungibleFaucetError, TokenMetadata}; use crate::account::auth::NoAuth; use crate::account::components::network_fungible_faucet_library; - -/// The schema type ID for token symbols. -const TOKEN_SYMBOL_TYPE_ID: &str = "miden::standards::fungible_faucets::metadata::token_symbol"; use crate::account::interface::{AccountComponentInterface, AccountInterface, AccountInterfaceExt}; use crate::procedure_digest; diff --git a/crates/miden-standards/src/account/faucets/timed_fungible.rs b/crates/miden-standards/src/account/faucets/timed_fungible.rs index 96da8585da..66d9e58058 100644 --- a/crates/miden-standards/src/account/faucets/timed_fungible.rs +++ b/crates/miden-standards/src/account/faucets/timed_fungible.rs @@ -71,6 +71,7 @@ procedure_digest!( /// /// - [`Self::metadata_slot`]: Stores [`TokenMetadata`]. /// - [`Self::timed_config_slot`]: Stores timed config `[0, 0, 0, distribution_end]`. +/// - [`Self::owner_config_slot`]: Stores the owner account ID `[0, 0, suffix, prefix]`. /// /// [builder]: crate::code_builder::CodeBuilder pub struct TimedFungibleFaucet { @@ -382,7 +383,7 @@ impl TryFrom<&Account> for TimedFungibleFaucet { /// Creates a new faucet account with timed fungible faucet interface, /// account storage type, owner account, and provided metadata (token symbol, -/// decimals, max supply, distribution end block, burn-only flag). +/// decimals, max supply, distribution end block). /// /// The timed faucet interface exposes procedures: /// - `distribute`, which mints assets and creates a note for the provided recipient within the diff --git a/crates/miden-standards/src/account/faucets/unlimited_fungible.rs b/crates/miden-standards/src/account/faucets/unlimited_fungible.rs index 5bb0ca39ff..8fb6b8ed82 100644 --- a/crates/miden-standards/src/account/faucets/unlimited_fungible.rs +++ b/crates/miden-standards/src/account/faucets/unlimited_fungible.rs @@ -67,6 +67,7 @@ procedure_digest!( /// ## Storage Layout /// /// - [`Self::metadata_slot`]: Stores [`TokenMetadata`]. +/// - [`Self::owner_config_slot`]: Stores the owner account ID `[0, 0, suffix, prefix]`. /// /// [builder]: crate::code_builder::CodeBuilder pub struct UnlimitedFungibleFaucet { diff --git a/crates/miden-standards/src/errors/standards.rs b/crates/miden-standards/src/errors/standards.rs index 5784102844..93e685ca28 100644 --- a/crates/miden-standards/src/errors/standards.rs +++ b/crates/miden-standards/src/errors/standards.rs @@ -12,8 +12,8 @@ use miden_protocol::errors::MasmError; /// Error Message: "burn requires exactly 1 note asset" pub const ERR_BASIC_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS: MasmError = MasmError::from_static_str("burn requires exactly 1 note asset"); -/// Error Message: "burn amount exceeds the existing token supply" -pub const ERR_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY: MasmError = MasmError::from_static_str("burn amount exceeds the existing token supply"); +/// Error Message: "asset amount to burn exceeds the existing token supply" +pub const ERR_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY: MasmError = MasmError::from_static_str("asset amount to burn exceeds the existing token supply"); /// Error Message: "amount would exceed the maximum supply" pub const ERR_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY: MasmError = MasmError::from_static_str("amount would exceed the maximum supply"); @@ -21,9 +21,6 @@ pub const ERR_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY: MasmError = MasmError::from_ /// Error Message: "distribution period has ended" pub const ERR_DISTRIBUTION_PERIOD_ENDED: MasmError = MasmError::from_static_str("distribution period has ended"); -/// Error Message: "asset amount to burn exceeds the existing token supply" -pub const ERR_FAUCET_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY: MasmError = MasmError::from_static_str("asset amount to burn exceeds the existing token supply"); - /// Error Message: "token_supply plus the amount passed to distribute would exceed the maximum supply" pub const ERR_FUNGIBLE_ASSET_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY: MasmError = MasmError::from_static_str("token_supply plus the amount passed to distribute would exceed the maximum supply"); /// Error Message: "max supply exceeds maximum representable fungible asset amount" @@ -67,9 +64,6 @@ pub const ERR_SWAP_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS: MasmError = MasmError::fr /// Error Message: "SWAP script requires exactly 1 note asset" pub const ERR_SWAP_WRONG_NUMBER_OF_ASSETS: MasmError = MasmError::from_static_str("SWAP script requires exactly 1 note asset"); -/// Error Message: "distribution period has ended" -pub const ERR_TIMED_FUNGIBLE_DISTRIBUTION_PERIOD_ENDED: MasmError = MasmError::from_static_str("distribution period has ended"); - /// Error Message: "failed to approve multisig transaction as it was already executed" pub const ERR_TX_ALREADY_EXECUTED: MasmError = MasmError::from_static_str("failed to approve multisig transaction as it was already executed"); diff --git a/crates/miden-testing/tests/scripts/faucet.rs b/crates/miden-testing/tests/scripts/faucet.rs index 351648ea5e..1a89230732 100644 --- a/crates/miden-testing/tests/scripts/faucet.rs +++ b/crates/miden-testing/tests/scripts/faucet.rs @@ -34,7 +34,7 @@ use miden_standards::account::faucets::{ }; use miden_standards::code_builder::CodeBuilder; use miden_standards::errors::standards::{ - ERR_FAUCET_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY, + ERR_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY, ERR_FUNGIBLE_ASSET_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY, ERR_SENDER_NOT_OWNER, }; @@ -330,7 +330,7 @@ async fn faucet_burn_fungible_asset_fails_amount_exceeds_token_supply() -> anyho .execute() .await; - assert_transaction_executor_error!(tx, ERR_FAUCET_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY); + assert_transaction_executor_error!(tx, ERR_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY); Ok(()) } From e0b0afc17eed8366d84cb41a288716d1ec7ecf61 Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Thu, 19 Feb 2026 15:08:36 -0300 Subject: [PATCH 08/10] feat: add Timed Unlimited Fungible Faucet component with distribution and burn procedures --- .../timed_unlimited_fungible_faucet.masm | 4 + .../faucets/timed_unlimited_fungible.masm | 85 +++ .../src/account/components/mod.rs | 27 + .../src/account/faucets/mod.rs | 8 + .../faucets/timed_unlimited_fungible.rs | 592 ++++++++++++++++++ .../src/account/interface/component.rs | 32 + .../src/account/interface/extension.rs | 8 + 7 files changed, 756 insertions(+) create mode 100644 crates/miden-standards/asm/account_components/faucets/timed_unlimited_fungible_faucet.masm create mode 100644 crates/miden-standards/asm/standards/faucets/timed_unlimited_fungible.masm create mode 100644 crates/miden-standards/src/account/faucets/timed_unlimited_fungible.rs diff --git a/crates/miden-standards/asm/account_components/faucets/timed_unlimited_fungible_faucet.masm b/crates/miden-standards/asm/account_components/faucets/timed_unlimited_fungible_faucet.masm new file mode 100644 index 0000000000..827e10f75f --- /dev/null +++ b/crates/miden-standards/asm/account_components/faucets/timed_unlimited_fungible_faucet.masm @@ -0,0 +1,4 @@ +pub use ::miden::standards::faucets::timed_unlimited_fungible::distribute +pub use ::miden::standards::faucets::timed_unlimited_fungible::burn +pub use ::miden::standards::faucets::timed_unlimited_fungible::transfer_ownership +pub use ::miden::standards::faucets::timed_unlimited_fungible::renounce_ownership diff --git a/crates/miden-standards/asm/standards/faucets/timed_unlimited_fungible.masm b/crates/miden-standards/asm/standards/faucets/timed_unlimited_fungible.masm new file mode 100644 index 0000000000..be76144a6f --- /dev/null +++ b/crates/miden-standards/asm/standards/faucets/timed_unlimited_fungible.masm @@ -0,0 +1,85 @@ +# TIMED UNLIMITED SUPPLY FUNGIBLE FAUCET CONTRACT +# +# A fungible faucet with time-based distribution controls but no supply cap. +# This faucet enforces a distribution period for minting, with max_supply set +# to FUNGIBLE_ASSET_MAX_AMOUNT (effectively unlimited). +# Burns are always allowed. Supply tracking is handled by faucets::distribute/burn. +# ================================================================================================= + +use miden::protocol::active_account +use miden::standards::faucets +use miden::standards::access::ownable +use miden::standards::supply::supply_limits + +# CONSTANTS +# ================================================================================================= + +const TIMED_CONFIG_SLOT = word("miden::standards::faucets::timed_fungible::config") + +# OWNER MANAGEMENT +# ================================================================================================= + +pub use ownable::get_owner +pub use ownable::transfer_ownership +pub use ownable::renounce_ownership + +# PROCEDURES +# ================================================================================================= + +#! Distributes freshly minted fungible assets to the provided recipient by creating a note. +#! +#! This procedure checks the distribution period before delegating to the shared distribute +#! procedure which handles supply tracking in the metadata slot. +#! No supply cap is enforced — max_supply is set to FUNGIBLE_ASSET_MAX_AMOUNT at creation. +#! +#! Inputs: [amount, tag, note_type, RECIPIENT] +#! Outputs: [note_idx] +#! +#! Where: +#! - amount is the amount to be minted and sent. +#! - tag is the tag to be included in the note. +#! - note_type is the type of the note that holds the asset. +#! - RECIPIENT is the recipient of the asset, i.e., +#! hash(hash(hash(serial_num, [0; 4]), script_root), storage_commitment). +#! - note_idx is the index of the created note. +#! +#! Panics if: +#! - the transaction is being executed against an account that is not a fungible asset faucet. +#! - the note sender is not the owner of this faucet. +#! - the distribution period has ended. +#! +#! Invocation: exec +pub proc distribute + exec.ownable::verify_owner + + # Check distribution period hasn't ended + push.TIMED_CONFIG_SLOT[0..2] exec.active_account::get_item + # => [distribution_end, 0, 0, 0, amount, tag, note_type, RECIPIENT] + + exec.supply_limits::check_distribution_period + # => [0, 0, 0, amount, tag, note_type, RECIPIENT] + + drop drop drop + # => [amount, tag, note_type, RECIPIENT] + + exec.faucets::distribute + # => [note_idx] +end + +#! Burns the fungible asset from the active note. +#! +#! Burns are always allowed — no time or ownership restrictions. +#! Delegates to the shared burn procedure which handles supply tracking. +#! +#! Inputs: [pad(16)] +#! Outputs: [pad(16)] +#! +#! Panics if: +#! - the procedure is not called from a note context. +#! - the note does not contain exactly one asset. +#! - the transaction is executed against an account which is not a fungible asset faucet. +#! - the transaction is executed against a faucet which is not the origin of the specified asset. +#! - the amount to burn exceeds the current token supply. +#! +#! Invocation: call +pub use faucets::burn diff --git a/crates/miden-standards/src/account/components/mod.rs b/crates/miden-standards/src/account/components/mod.rs index 30b7467c91..3598b7c330 100644 --- a/crates/miden-standards/src/account/components/mod.rs +++ b/crates/miden-standards/src/account/components/mod.rs @@ -127,6 +127,16 @@ static TIMED_FUNGIBLE_FAUCET_LIBRARY: LazyLock = LazyLock::new(|| { Library::read_from_bytes(bytes).expect("Shipped Timed Fungible Faucet library is well-formed") }); +// Initialize the Timed Unlimited Fungible Faucet library only once. +static TIMED_UNLIMITED_FUNGIBLE_FAUCET_LIBRARY: LazyLock = LazyLock::new(|| { + let bytes = include_bytes!(concat!( + env!("OUT_DIR"), + "/assets/account_components/faucets/timed_unlimited_fungible_faucet.masl" + )); + Library::read_from_bytes(bytes) + .expect("Shipped Timed Unlimited Fungible Faucet library is well-formed") +}); + // METADATA LIBRARIES // ================================================================================================ @@ -159,6 +169,11 @@ pub fn timed_fungible_faucet_library() -> Library { TIMED_FUNGIBLE_FAUCET_LIBRARY.clone() } +/// Returns the Timed Unlimited Fungible Faucet Library. +pub fn timed_unlimited_fungible_faucet_library() -> Library { + TIMED_UNLIMITED_FUNGIBLE_FAUCET_LIBRARY.clone() +} + /// Returns the Network Fungible Faucet Library. pub fn network_fungible_faucet_library() -> Library { NETWORK_FUNGIBLE_FAUCET_LIBRARY.clone() @@ -214,6 +229,7 @@ pub enum StandardAccountComponent { BasicFungibleFaucet, UnlimitedFungibleFaucet, TimedFungibleFaucet, + TimedUnlimitedFungibleFaucet, NetworkFungibleFaucet, AuthEcdsaK256Keccak, AuthEcdsaK256KeccakAcl, @@ -232,6 +248,9 @@ impl StandardAccountComponent { Self::BasicFungibleFaucet => BASIC_FUNGIBLE_FAUCET_LIBRARY.as_ref(), Self::UnlimitedFungibleFaucet => UNLIMITED_FUNGIBLE_FAUCET_LIBRARY.as_ref(), Self::TimedFungibleFaucet => TIMED_FUNGIBLE_FAUCET_LIBRARY.as_ref(), + Self::TimedUnlimitedFungibleFaucet => { + TIMED_UNLIMITED_FUNGIBLE_FAUCET_LIBRARY.as_ref() + }, Self::NetworkFungibleFaucet => NETWORK_FUNGIBLE_FAUCET_LIBRARY.as_ref(), Self::AuthEcdsaK256Keccak => ECDSA_K256_KECCAK_LIBRARY.as_ref(), Self::AuthEcdsaK256KeccakAcl => ECDSA_K256_KECCAK_ACL_LIBRARY.as_ref(), @@ -285,6 +304,10 @@ impl StandardAccountComponent { Self::TimedFungibleFaucet => { component_interface_vec.push(AccountComponentInterface::TimedFungibleFaucet) }, + Self::TimedUnlimitedFungibleFaucet => { + component_interface_vec + .push(AccountComponentInterface::TimedUnlimitedFungibleFaucet) + }, Self::NetworkFungibleFaucet => { component_interface_vec.push(AccountComponentInterface::NetworkFungibleFaucet) }, @@ -321,6 +344,10 @@ impl StandardAccountComponent { Self::BasicFungibleFaucet.extract_component(procedures_set, component_interface_vec); Self::UnlimitedFungibleFaucet.extract_component(procedures_set, component_interface_vec); Self::TimedFungibleFaucet.extract_component(procedures_set, component_interface_vec); + // Note: TimedUnlimitedFungibleFaucet is intentionally not extracted here because its + // compiled MASM procedures are identical to TimedFungibleFaucet (the only difference + // is the Rust-level max_supply initialization). Auto-detection would always match + // TimedFungibleFaucet first due to extraction order. Self::NetworkFungibleFaucet.extract_component(procedures_set, component_interface_vec); Self::AuthEcdsaK256Keccak.extract_component(procedures_set, component_interface_vec); Self::AuthEcdsaK256KeccakAcl.extract_component(procedures_set, component_interface_vec); diff --git a/crates/miden-standards/src/account/faucets/mod.rs b/crates/miden-standards/src/account/faucets/mod.rs index 2b00d8ec5c..5c3308f130 100644 --- a/crates/miden-standards/src/account/faucets/mod.rs +++ b/crates/miden-standards/src/account/faucets/mod.rs @@ -7,12 +7,16 @@ use thiserror::Error; mod basic_fungible; mod network_fungible; mod timed_fungible; +mod timed_unlimited_fungible; mod token_metadata; mod unlimited_fungible; pub use basic_fungible::{BasicFungibleFaucet, create_basic_fungible_faucet}; pub use network_fungible::{NetworkFungibleFaucet, create_network_fungible_faucet}; pub use timed_fungible::{TimedFungibleFaucet, create_timed_fungible_faucet}; +pub use timed_unlimited_fungible::{ + TimedUnlimitedFungibleFaucet, create_timed_unlimited_fungible_faucet, +}; pub use token_metadata::TokenMetadata; pub use unlimited_fungible::{UnlimitedFungibleFaucet, create_unlimited_fungible_faucet}; @@ -44,6 +48,10 @@ pub enum FungibleFaucetError { "account interface does not have the procedures of the unlimited fungible faucet component" )] MissingUnlimitedFungibleFaucetInterface, + #[error( + "account interface does not have the procedures of the timed unlimited fungible faucet component" + )] + MissingTimedUnlimitedFungibleFaucetInterface, #[error("failed to retrieve storage slot with name {slot_name}")] StorageLookupFailed { slot_name: StorageSlotName, diff --git a/crates/miden-standards/src/account/faucets/timed_unlimited_fungible.rs b/crates/miden-standards/src/account/faucets/timed_unlimited_fungible.rs new file mode 100644 index 0000000000..5cff21658e --- /dev/null +++ b/crates/miden-standards/src/account/faucets/timed_unlimited_fungible.rs @@ -0,0 +1,592 @@ +use miden_protocol::account::component::{ + AccountComponentMetadata, + FeltSchema, + SchemaTypeId, + StorageSchema, + StorageSlotSchema, +}; +use miden_protocol::account::{ + Account, + AccountBuilder, + AccountComponent, + AccountId, + AccountStorage, + AccountStorageMode, + AccountType, + StorageSlot, + StorageSlotName, +}; +use miden_protocol::asset::TokenSymbol; +use miden_protocol::utils::sync::LazyLock; +use miden_protocol::{Felt, FieldElement, Word}; + +use super::token_metadata::TOKEN_SYMBOL_TYPE_ID; +use super::{FungibleFaucetError, TokenMetadata}; +use crate::account::auth::NoAuth; +use crate::account::components::timed_unlimited_fungible_faucet_library; +use crate::account::interface::{AccountComponentInterface, AccountInterface, AccountInterfaceExt}; +use crate::procedure_digest; + +// SLOT NAMES +// ================================================================================================ + +static TIMED_CONFIG_SLOT: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::faucets::timed_fungible::config") + .expect("storage slot name should be valid") +}); + +static OWNER_CONFIG_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::access::ownable::owner_config") + .expect("storage slot name should be valid") +}); + +// TIMED UNLIMITED FUNGIBLE FAUCET ACCOUNT COMPONENT +// ================================================================================================ + +procedure_digest!( + TIMED_UNLIMITED_FUNGIBLE_FAUCET_DISTRIBUTE, + TimedUnlimitedFungibleFaucet::DISTRIBUTE_PROC_NAME, + timed_unlimited_fungible_faucet_library +); + +procedure_digest!( + TIMED_UNLIMITED_FUNGIBLE_FAUCET_BURN, + TimedUnlimitedFungibleFaucet::BURN_PROC_NAME, + timed_unlimited_fungible_faucet_library +); + +/// An [`AccountComponent`] implementing a timed unlimited fungible faucet. +/// +/// It reexports the procedures from `miden::standards::faucets::timed_unlimited_fungible`. When +/// linking against this component, the `miden` library (i.e. +/// [`ProtocolLib`](miden_protocol::ProtocolLib)) must be available to the assembler which is the +/// case when using [`CodeBuilder`][builder]. The procedures of this component are: +/// - `distribute`, which mints assets and creates a note for the provided recipient within the +/// allowed time window. +/// - `burn`, which burns the provided asset. Burns are always allowed. +/// +/// No supply cap is enforced — minting is unrestricted up to protocol limits +/// ([`FungibleAsset::MAX_AMOUNT`](miden_protocol::asset::FungibleAsset::MAX_AMOUNT)), +/// but distribution is time-bounded by `distribution_end_block`. +/// +/// This component supports accounts of type [`AccountType::FungibleFaucet`]. +/// +/// ## Storage Layout +/// +/// - [`Self::metadata_slot`]: Stores [`TokenMetadata`]. +/// - [`Self::timed_config_slot`]: Stores timed config `[0, 0, 0, distribution_end]`. +/// - [`Self::owner_config_slot`]: Stores the owner account ID `[0, 0, suffix, prefix]`. +/// +/// [builder]: crate::code_builder::CodeBuilder +pub struct TimedUnlimitedFungibleFaucet { + metadata: TokenMetadata, + distribution_end: u32, + owner_account_id: AccountId, +} + +impl TimedUnlimitedFungibleFaucet { + // CONSTANTS + // -------------------------------------------------------------------------------------------- + + /// The name of the component. + pub const NAME: &'static str = "miden::timed_unlimited_fungible_faucet"; + + /// The maximum number of decimals supported by the component. + pub const MAX_DECIMALS: u8 = TokenMetadata::MAX_DECIMALS; + + const DISTRIBUTE_PROC_NAME: &str = "timed_unlimited_fungible_faucet::distribute"; + const BURN_PROC_NAME: &str = "timed_unlimited_fungible_faucet::burn"; + + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Creates a new [`TimedUnlimitedFungibleFaucet`] component. + /// + /// The max supply is set to + /// [`FungibleAsset::MAX_AMOUNT`](miden_protocol::asset::FungibleAsset::MAX_AMOUNT) + /// since this faucet does not enforce supply limits at runtime, only time limits. + /// + /// # Errors + /// + /// Returns an error if: + /// - the decimals parameter exceeds maximum value of [`Self::MAX_DECIMALS`]. + pub fn new( + symbol: TokenSymbol, + decimals: u8, + distribution_end: u32, + owner_account_id: AccountId, + ) -> Result { + let max_supply = miden_protocol::asset::FungibleAsset::MAX_AMOUNT; + let metadata = TokenMetadata::new(symbol, decimals, Felt::new(max_supply))?; + Ok(Self { + metadata, + distribution_end, + owner_account_id, + }) + } + + /// Creates a new [`TimedUnlimitedFungibleFaucet`] component from the given [`TokenMetadata`]. + pub fn from_metadata( + metadata: TokenMetadata, + distribution_end: u32, + owner_account_id: AccountId, + ) -> Self { + Self { + metadata, + distribution_end, + owner_account_id, + } + } + + /// Attempts to create a new [`TimedUnlimitedFungibleFaucet`] component from the associated + /// account interface and storage. + /// + /// # Errors + /// + /// Returns an error if: + /// - the provided [`AccountInterface`] does not contain a + /// [`AccountComponentInterface::TimedUnlimitedFungibleFaucet`] component. + /// - the decimals parameter exceeds maximum value of [`Self::MAX_DECIMALS`]. + /// - the max supply value exceeds maximum possible amount for a fungible asset of + /// [`miden_protocol::asset::FungibleAsset::MAX_AMOUNT`]. + /// - the token supply exceeds the max supply. + /// - the token symbol encoded value exceeds the maximum value of + /// [`TokenSymbol::MAX_ENCODED_VALUE`]. + fn try_from_interface( + interface: AccountInterface, + storage: &AccountStorage, + ) -> Result { + // Accept either TimedUnlimitedFungibleFaucet or TimedFungibleFaucet because the + // compiled MASM procedures are identical (same distribute/burn logic). The auto-detection + // in extract_standard_components will match TimedFungibleFaucet first, so we accept both. + if !interface + .components() + .contains(&AccountComponentInterface::TimedUnlimitedFungibleFaucet) + && !interface + .components() + .contains(&AccountComponentInterface::TimedFungibleFaucet) + { + return Err(FungibleFaucetError::MissingTimedUnlimitedFungibleFaucetInterface); + } + + let metadata = TokenMetadata::try_from(storage)?; + + // Read timed config: [0, 0, 0, distribution_end] + let config_word: Word = storage + .get_item(TimedUnlimitedFungibleFaucet::timed_config_slot()) + .map_err(|err| FungibleFaucetError::StorageLookupFailed { + slot_name: TimedUnlimitedFungibleFaucet::timed_config_slot().clone(), + source: err, + })?; + + let distribution_end = config_word[3].as_int() as u32; + + // Read owner account ID: [0, 0, suffix, prefix] + let owner_account_id_word: Word = storage + .get_item(TimedUnlimitedFungibleFaucet::owner_config_slot()) + .map_err(|err| FungibleFaucetError::StorageLookupFailed { + slot_name: TimedUnlimitedFungibleFaucet::owner_config_slot().clone(), + source: err, + })?; + + let prefix = owner_account_id_word[3]; + let suffix = owner_account_id_word[2]; + let owner_account_id = AccountId::new_unchecked([prefix, suffix]); + + Ok(Self { + metadata, + distribution_end, + owner_account_id, + }) + } + + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the [`StorageSlotName`] where the [`TimedUnlimitedFungibleFaucet`]'s metadata is + /// stored. + pub fn metadata_slot() -> &'static StorageSlotName { + TokenMetadata::metadata_slot() + } + + /// Returns the [`StorageSlotName`] where the timed configuration is stored. + pub fn timed_config_slot() -> &'static StorageSlotName { + &TIMED_CONFIG_SLOT + } + + /// Returns the storage slot schema for the metadata slot. + pub fn metadata_slot_schema() -> (StorageSlotName, StorageSlotSchema) { + let token_symbol_type = SchemaTypeId::new(TOKEN_SYMBOL_TYPE_ID).expect("valid type id"); + ( + Self::metadata_slot().clone(), + StorageSlotSchema::value( + "Token metadata", + [ + FeltSchema::felt("token_supply").with_default(Felt::new(0)), + FeltSchema::felt("max_supply"), + FeltSchema::u8("decimals"), + FeltSchema::new_typed(token_symbol_type, "symbol"), + ], + ), + ) + } + + /// Returns the storage slot schema for the timed config slot. + pub fn timed_config_slot_schema() -> (StorageSlotName, StorageSlotSchema) { + ( + Self::timed_config_slot().clone(), + StorageSlotSchema::value( + "Timed Config", + [ + FeltSchema::new_void(), + FeltSchema::new_void(), + FeltSchema::new_void(), + FeltSchema::u32("distribution_end"), + ], + ), + ) + } + + /// Returns the [`StorageSlotName`] where the owner configuration is stored. + pub fn owner_config_slot() -> &'static StorageSlotName { + &OWNER_CONFIG_SLOT_NAME + } + + /// Returns the storage slot schema for the owner configuration slot. + pub fn owner_config_slot_schema() -> (StorageSlotName, StorageSlotSchema) { + ( + Self::owner_config_slot().clone(), + StorageSlotSchema::value( + "Owner account configuration", + [ + FeltSchema::new_void(), + FeltSchema::new_void(), + FeltSchema::felt("owner_suffix"), + FeltSchema::felt("owner_prefix"), + ], + ), + ) + } + + /// Returns the owner account ID of the faucet. + pub fn owner_account_id(&self) -> AccountId { + self.owner_account_id + } + + /// Returns the token metadata. + pub fn metadata(&self) -> &TokenMetadata { + &self.metadata + } + + /// Returns the symbol of the faucet. + pub fn symbol(&self) -> TokenSymbol { + self.metadata.symbol() + } + + /// Returns the decimals of the faucet. + pub fn decimals(&self) -> u8 { + self.metadata.decimals() + } + + /// Returns the max supply (in base units) of the faucet. + pub fn max_supply(&self) -> Felt { + self.metadata.max_supply() + } + + /// Returns the token supply (in base units) of the faucet. + pub fn token_supply(&self) -> Felt { + self.metadata.token_supply() + } + + /// Returns the block number at which distribution ends. + pub fn distribution_end(&self) -> u32 { + self.distribution_end + } + + /// Returns the digest of the `distribute` account procedure. + pub fn distribute_digest() -> Word { + *TIMED_UNLIMITED_FUNGIBLE_FAUCET_DISTRIBUTE + } + + /// Returns the digest of the `burn` account procedure. + pub fn burn_digest() -> Word { + *TIMED_UNLIMITED_FUNGIBLE_FAUCET_BURN + } + + // MUTATORS + // -------------------------------------------------------------------------------------------- + + /// Sets the token_supply (in base units) of the timed unlimited fungible faucet. + /// + /// # Errors + /// + /// Returns an error if: + /// - the token supply exceeds the max supply. + pub fn with_token_supply(mut self, token_supply: Felt) -> Result { + self.metadata = self.metadata.with_token_supply(token_supply)?; + Ok(self) + } +} + +impl From for AccountComponent { + fn from(faucet: TimedUnlimitedFungibleFaucet) -> Self { + let metadata_slot: StorageSlot = faucet.metadata.into(); + + let config_val = + [Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::new(faucet.distribution_end as u64)]; + + let config_slot = StorageSlot::with_value( + TimedUnlimitedFungibleFaucet::timed_config_slot().clone(), + Word::new(config_val), + ); + + let owner_account_id_word: Word = [ + Felt::new(0), + Felt::new(0), + faucet.owner_account_id.suffix(), + faucet.owner_account_id.prefix().as_felt(), + ] + .into(); + + let owner_slot = StorageSlot::with_value( + TimedUnlimitedFungibleFaucet::owner_config_slot().clone(), + owner_account_id_word, + ); + + let storage_schema = StorageSchema::new([ + TimedUnlimitedFungibleFaucet::metadata_slot_schema(), + TimedUnlimitedFungibleFaucet::timed_config_slot_schema(), + TimedUnlimitedFungibleFaucet::owner_config_slot_schema(), + ]) + .expect("storage schema should be valid"); + + let metadata = AccountComponentMetadata::new(TimedUnlimitedFungibleFaucet::NAME) + .with_description( + "Timed unlimited fungible faucet component for time-bounded minting and burning tokens without supply cap", + ) + .with_supported_type(AccountType::FungibleFaucet) + .with_storage_schema(storage_schema); + + AccountComponent::new( + timed_unlimited_fungible_faucet_library(), + vec![metadata_slot, config_slot, owner_slot], + metadata, + ) + .expect("timed unlimited fungible faucet component should satisfy the requirements of a valid account component") + } +} + +impl TryFrom for TimedUnlimitedFungibleFaucet { + type Error = FungibleFaucetError; + + fn try_from(account: Account) -> Result { + let account_interface = AccountInterface::from_account(&account); + + TimedUnlimitedFungibleFaucet::try_from_interface(account_interface, account.storage()) + } +} + +impl TryFrom<&Account> for TimedUnlimitedFungibleFaucet { + type Error = FungibleFaucetError; + + fn try_from(account: &Account) -> Result { + let account_interface = AccountInterface::from_account(account); + + TimedUnlimitedFungibleFaucet::try_from_interface(account_interface, account.storage()) + } +} + +/// Creates a new faucet account with timed unlimited fungible faucet interface, +/// account storage type, owner account, and provided metadata (token symbol, +/// decimals, distribution end block). +/// +/// The timed unlimited faucet interface exposes procedures: +/// - `distribute`, which mints assets and creates a note for the provided recipient within the +/// distribution time window. Requires the caller to be the owner. +/// - `burn`, which burns the provided asset. Burns are always allowed. +/// - `transfer_ownership`, which transfers ownership to a new account. +/// - `renounce_ownership`, which renounces ownership. +/// +/// No supply cap is enforced at runtime. The `distribute` procedure can only be called +/// by the owner of the faucet within the distribution period. The `burn` procedure can +/// be called by anyone and requires the calling note to contain the asset to be burned. +pub fn create_timed_unlimited_fungible_faucet( + init_seed: [u8; 32], + symbol: TokenSymbol, + decimals: u8, + distribution_end: u32, + storage_mode: AccountStorageMode, + owner_account_id: AccountId, +) -> Result { + let auth_component: AccountComponent = NoAuth::new().into(); + + let faucet_component = + TimedUnlimitedFungibleFaucet::new(symbol, decimals, distribution_end, owner_account_id)?; + + let account = AccountBuilder::new(init_seed) + .account_type(AccountType::FungibleFaucet) + .storage_mode(storage_mode) + .with_auth_component(auth_component) + .with_component(faucet_component) + .build() + .map_err(FungibleFaucetError::AccountError)?; + + Ok(account) +} + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + use assert_matches::assert_matches; + use miden_protocol::account::auth::PublicKeyCommitment; + use miden_protocol::asset::FungibleAsset; + use miden_protocol::testing::account_id::ACCOUNT_ID_SENDER; + use miden_protocol::{FieldElement, Word}; + + use super::{ + AccountBuilder, + AccountId, + AccountStorageMode, + AccountType, + Felt, + FungibleFaucetError, + TimedUnlimitedFungibleFaucet, + TokenSymbol, + create_timed_unlimited_fungible_faucet, + }; + use crate::account::auth::AuthFalcon512Rpo; + use crate::account::wallets::BasicWallet; + + fn mock_owner_account_id() -> AccountId { + ACCOUNT_ID_SENDER.try_into().expect("valid account id") + } + + #[test] + fn timed_unlimited_faucet_contract_creation() { + let owner_account_id = mock_owner_account_id(); + + let init_seed: [u8; 32] = [ + 90, 110, 209, 94, 84, 105, 250, 242, 223, 203, 216, 124, 22, 159, 14, 132, 215, 85, + 183, 204, 149, 90, 166, 68, 100, 73, 106, 168, 125, 237, 138, 16, + ]; + + let token_symbol = TokenSymbol::try_from("TUF").unwrap(); + let decimals = 6u8; + let distribution_end = 10_000u32; + let storage_mode = AccountStorageMode::Private; + + let faucet_account = create_timed_unlimited_fungible_faucet( + init_seed, + token_symbol, + decimals, + distribution_end, + storage_mode, + owner_account_id, + ) + .unwrap(); + + assert!(faucet_account.is_faucet()); + assert_eq!(faucet_account.account_type(), AccountType::FungibleFaucet); + + // Check metadata slot: max_supply should be FungibleAsset::MAX_AMOUNT + assert_eq!( + faucet_account + .storage() + .get_item(TimedUnlimitedFungibleFaucet::metadata_slot()) + .unwrap(), + [ + Felt::ZERO, + Felt::new(FungibleAsset::MAX_AMOUNT), + Felt::new(6), + token_symbol.into() + ] + .into() + ); + + // Check timed config slot + assert_eq!( + faucet_account + .storage() + .get_item(TimedUnlimitedFungibleFaucet::timed_config_slot()) + .unwrap(), + [Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::new(distribution_end as u64),].into() + ); + + // Check owner config slot + assert_eq!( + faucet_account + .storage() + .get_item(TimedUnlimitedFungibleFaucet::owner_config_slot()) + .unwrap(), + [ + Felt::new(0), + Felt::new(0), + owner_account_id.suffix(), + owner_account_id.prefix().as_felt(), + ] + .into() + ); + + // Verify the faucet can be extracted via TryFrom + let faucet_component = + TimedUnlimitedFungibleFaucet::try_from(faucet_account.clone()).unwrap(); + assert_eq!(faucet_component.symbol(), token_symbol); + assert_eq!(faucet_component.decimals(), decimals); + assert_eq!(faucet_component.max_supply(), Felt::new(FungibleAsset::MAX_AMOUNT)); + assert_eq!(faucet_component.token_supply(), Felt::ZERO); + assert_eq!(faucet_component.distribution_end(), distribution_end); + assert_eq!(faucet_component.owner_account_id(), owner_account_id); + } + + #[test] + fn timed_unlimited_faucet_create_from_account() { + let mock_word = Word::from([0, 1, 2, 3u32]); + let mock_public_key = PublicKeyCommitment::from(mock_word); + let mock_seed = mock_word.as_bytes(); + let owner_account_id = mock_owner_account_id(); + + let token_symbol = TokenSymbol::new("TUF").expect("invalid token symbol"); + let faucet_account = AccountBuilder::new(mock_seed) + .account_type(AccountType::FungibleFaucet) + .with_component( + TimedUnlimitedFungibleFaucet::new(token_symbol, 6, 10_000, owner_account_id) + .expect("failed to create a timed unlimited fungible faucet component"), + ) + .with_auth_component(AuthFalcon512Rpo::new(mock_public_key)) + .build_existing() + .expect("failed to create faucet account"); + + let timed_unlimited_ff = TimedUnlimitedFungibleFaucet::try_from(faucet_account) + .expect("timed unlimited fungible faucet creation failed"); + assert_eq!(timed_unlimited_ff.symbol(), token_symbol); + assert_eq!(timed_unlimited_ff.decimals(), 6); + assert_eq!(timed_unlimited_ff.max_supply(), Felt::new(FungibleAsset::MAX_AMOUNT)); + assert_eq!(timed_unlimited_ff.token_supply(), Felt::ZERO); + assert_eq!(timed_unlimited_ff.distribution_end(), 10_000); + assert_eq!(timed_unlimited_ff.owner_account_id(), owner_account_id); + + // invalid account: timed unlimited fungible faucet component is missing + let invalid_faucet_account = AccountBuilder::new(mock_seed) + .account_type(AccountType::FungibleFaucet) + .with_auth_component(AuthFalcon512Rpo::new(mock_public_key)) + .with_component(BasicWallet) + .build_existing() + .expect("failed to create account"); + + let err = TimedUnlimitedFungibleFaucet::try_from(invalid_faucet_account) + .err() + .expect("timed unlimited fungible faucet creation should fail"); + assert_matches!( + err, + FungibleFaucetError::MissingTimedUnlimitedFungibleFaucetInterface + ); + } + + #[test] + fn get_timed_unlimited_faucet_procedures() { + let _distribute_digest = TimedUnlimitedFungibleFaucet::distribute_digest(); + let _burn_digest = TimedUnlimitedFungibleFaucet::burn_digest(); + } +} diff --git a/crates/miden-standards/src/account/interface/component.rs b/crates/miden-standards/src/account/interface/component.rs index dd3ee544d3..7931188444 100644 --- a/crates/miden-standards/src/account/interface/component.rs +++ b/crates/miden-standards/src/account/interface/component.rs @@ -35,6 +35,10 @@ pub enum AccountComponentInterface { /// [`TimedFungibleFaucet`][crate::account::faucets::TimedFungibleFaucet] module. TimedFungibleFaucet, /// Exposes procedures from the + /// [`TimedUnlimitedFungibleFaucet`][crate::account::faucets::TimedUnlimitedFungibleFaucet] + /// module. + TimedUnlimitedFungibleFaucet, + /// Exposes procedures from the /// [`NetworkFungibleFaucet`][crate::account::faucets::NetworkFungibleFaucet] module. NetworkFungibleFaucet, /// Exposes procedures from the @@ -81,6 +85,9 @@ impl AccountComponentInterface { "Unlimited Fungible Faucet".to_string() }, AccountComponentInterface::TimedFungibleFaucet => "Timed Fungible Faucet".to_string(), + AccountComponentInterface::TimedUnlimitedFungibleFaucet => { + "Timed Unlimited Fungible Faucet".to_string() + }, AccountComponentInterface::NetworkFungibleFaucet => { "Network Fungible Faucet".to_string() }, @@ -324,6 +331,31 @@ impl AccountComponentInterface { amount = asset.unwrap_fungible().amount() )); }, + AccountComponentInterface::TimedUnlimitedFungibleFaucet => { + if partial_note.assets().num_assets() != 1 { + return Err(AccountInterfaceError::FaucetNoteWithoutAsset); + } + + let asset = + partial_note.assets().iter().next().expect("note should contain an asset"); + + if asset.faucet_id_prefix() != sender_account_id.prefix() { + return Err(AccountInterfaceError::IssuanceFaucetMismatch( + asset.faucet_id_prefix(), + )); + } + + body.push_str(&format!( + " + push.{amount} + call.::miden::standards::faucets::timed_unlimited_fungible::distribute + # => [note_idx, pad(25)] + swapdw dropw dropw swap drop + # => [note_idx, pad(16)]\n + ", + amount = asset.unwrap_fungible().amount() + )); + }, AccountComponentInterface::BasicWallet => { body.push_str( " diff --git a/crates/miden-standards/src/account/interface/extension.rs b/crates/miden-standards/src/account/interface/extension.rs index 6126186f6d..32dc60c8e3 100644 --- a/crates/miden-standards/src/account/interface/extension.rs +++ b/crates/miden-standards/src/account/interface/extension.rs @@ -22,6 +22,7 @@ use crate::account::components::{ network_fungible_faucet_library, no_auth_library, timed_fungible_faucet_library, + timed_unlimited_fungible_faucet_library, unlimited_fungible_faucet_library, }; use crate::account::interface::{ @@ -109,6 +110,13 @@ impl AccountInterfaceExt for AccountInterface { component_proc_digests .extend(timed_fungible_faucet_library().mast_forest().procedure_digests()); }, + AccountComponentInterface::TimedUnlimitedFungibleFaucet => { + component_proc_digests.extend( + timed_unlimited_fungible_faucet_library() + .mast_forest() + .procedure_digests(), + ); + }, AccountComponentInterface::NetworkFungibleFaucet => { component_proc_digests.extend( network_fungible_faucet_library().mast_forest().procedure_digests(), From 1270c236d28c6a0a720094bed5cdbc7b51ec82de Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Thu, 19 Feb 2026 15:37:45 -0300 Subject: [PATCH 09/10] refactor: simplify supply checks in distribute procedure and remove redundant error constant --- .../asm/standards/faucets/mod.masm | 30 +++++++------------ 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/crates/miden-standards/asm/standards/faucets/mod.masm b/crates/miden-standards/asm/standards/faucets/mod.masm index 0dfaeb23f7..878806b90a 100644 --- a/crates/miden-standards/asm/standards/faucets/mod.masm +++ b/crates/miden-standards/asm/standards/faucets/mod.masm @@ -13,8 +13,6 @@ const ERR_FUNGIBLE_ASSET_TOKEN_SUPPLY_EXCEEDS_MAX_SUPPLY="token supply exceeds m const ERR_FUNGIBLE_ASSET_MAX_SUPPLY_EXCEEDS_FUNGIBLE_ASSET_MAX_AMOUNT="max supply exceeds maximum representable fungible asset amount" -const ERR_FUNGIBLE_ASSET_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY="token_supply plus the amount passed to distribute would exceed the maximum supply" - const ERR_BASIC_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS="burn requires exactly 1 note asset" # STORAGE LAYOUT @@ -63,17 +61,12 @@ pub proc distribute # Assert that minting does not violate any supply constraints. # # To make sure we cannot mint more than intended, we need to check: - # 1) (max_supply - token_supply) <= max_supply, i.e. the subtraction does not wrap around - # 2) amount + token_supply does not exceed max_supply - # 3) amount + token_supply is less than FUNGIBLE_ASSET_MAX_AMOUNT + # 1) token_supply <= max_supply, i.e. the subtraction would not wrap around + # 2) max_supply <= FUNGIBLE_ASSET_MAX_AMOUNT (protocol limit) + # 3) token_supply + amount <= max_supply (delegated to supply_limits::check_supply) # - # This is done with the following concrete assertions: - # - assert token_supply <= max_supply which ensures 1) - # - assert max_supply <= FUNGIBLE_ASSET_MAX_AMOUNT to help ensure 3) - # - assert amount <= max_mint_amount to ensure 2) as well as 3) - # - this ensures 3) because token_supply + max_mint_amount at most ends up being equal to - # max_supply and we already asserted that max_supply does not exceed - # FUNGIBLE_ASSET_MAX_AMOUNT + # Check 3) also transitively ensures token_supply + amount <= FUNGIBLE_ASSET_MAX_AMOUNT + # because we already asserted max_supply <= FUNGIBLE_ASSET_MAX_AMOUNT in check 2). # --------------------------------------------------------------------------------------------- dup.1 dup.1 @@ -88,15 +81,12 @@ pub proc distribute assert.err=ERR_FUNGIBLE_ASSET_MAX_SUPPLY_EXCEEDS_FUNGIBLE_ASSET_MAX_AMOUNT # => [max_supply, token_supply, amount, tag, note_type, RECIPIENT] - dup.2 swap dup.2 - # => [token_supply, max_supply, amount, token_supply, amount, tag, note_type, RECIPIENT] - - # compute maximum amount that can be minted, max_mint_amount = max_supply - token_supply - sub - # => [max_mint_amount, amount, token_supply, amount, tag, note_type, RECIPIENT] + # assert token_supply + amount <= max_supply via supply_limits::check_supply + # Prepare args: [amount, token_supply, max_supply, ...] while keeping originals below + dup.2 dup.2 swap + # => [amount, token_supply, max_supply, token_supply, amount, tag, note_type, RECIPIENT] - # assert amount <= max_mint_amount - lte assert.err=ERR_FUNGIBLE_ASSET_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY + exec.supply_limits::check_supply # => [token_supply, amount, tag, note_type, RECIPIENT] # Compute the new token_supply and update in storage. From 7e1d333ec1549e4e88cb488f9611c0e9e4569782 Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Thu, 19 Feb 2026 16:04:51 -0300 Subject: [PATCH 10/10] refactor: streamline supply checks and error handling in fungible faucet components making the code easier to review and more correct. --- .../asm/standards/faucets/mod.masm | 80 ++++------------ .../asm/standards/faucets/timed_fungible.masm | 13 --- .../faucets/timed_unlimited_fungible.masm | 13 --- .../asm/standards/supply/supply_limits.masm | 95 +++++++++++++++---- .../src/account/components/mod.rs | 10 +- .../src/account/faucets/mod.rs | 3 +- .../faucets/timed_unlimited_fungible.rs | 9 +- .../src/account/interface/extension.rs | 4 +- .../miden-standards/src/errors/standards.rs | 13 ++- crates/miden-testing/tests/scripts/faucet.rs | 4 +- 10 files changed, 112 insertions(+), 132 deletions(-) diff --git a/crates/miden-standards/asm/standards/faucets/mod.masm b/crates/miden-standards/asm/standards/faucets/mod.masm index 878806b90a..76bcd51a9d 100644 --- a/crates/miden-standards/asm/standards/faucets/mod.masm +++ b/crates/miden-standards/asm/standards/faucets/mod.masm @@ -3,16 +3,11 @@ use miden::protocol::active_note use miden::protocol::faucet use miden::protocol::native_account use miden::protocol::output_note -use ::miden::protocol::asset::FUNGIBLE_ASSET_MAX_AMOUNT use miden::standards::supply::supply_limits # ERRORS # ================================================================================================= -const ERR_FUNGIBLE_ASSET_TOKEN_SUPPLY_EXCEEDS_MAX_SUPPLY="token supply exceeds max supply" - -const ERR_FUNGIBLE_ASSET_MAX_SUPPLY_EXCEEDS_FUNGIBLE_ASSET_MAX_AMOUNT="max supply exceeds maximum representable fungible asset amount" - const ERR_BASIC_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS="burn requires exactly 1 note asset" # STORAGE LAYOUT @@ -47,74 +42,44 @@ const METADATA_SLOT=word("miden::standards::fungible_faucets::metadata") #! Invocation: exec @locals(4) pub proc distribute - # Get the configured max supply and the token supply (= current supply). - # --------------------------------------------------------------------------------------------- - - push.METADATA_SLOT[0..2] exec.active_account::get_item - # => [token_symbol, decimals, max_supply, token_supply, amount, tag, note_type, RECIPIENT] - - # store a copy of the current slot content for the token_supply update later - loc_storew_be.METADATA_SLOT_LOCAL - drop drop - # => [max_supply, token_supply, amount, tag, note_type, RECIPIENT] - # Assert that minting does not violate any supply constraints. - # - # To make sure we cannot mint more than intended, we need to check: - # 1) token_supply <= max_supply, i.e. the subtraction would not wrap around - # 2) max_supply <= FUNGIBLE_ASSET_MAX_AMOUNT (protocol limit) - # 3) token_supply + amount <= max_supply (delegated to supply_limits::check_supply) - # - # Check 3) also transitively ensures token_supply + amount <= FUNGIBLE_ASSET_MAX_AMOUNT - # because we already asserted max_supply <= FUNGIBLE_ASSET_MAX_AMOUNT in check 2). + # check_supply reads the metadata slot internally and validates: + # 1) token_supply <= max_supply + # 2) max_supply <= FUNGIBLE_ASSET_MAX_AMOUNT + # 3) token_supply + amount <= max_supply # --------------------------------------------------------------------------------------------- - dup.1 dup.1 - # => [max_supply, token_supply, max_supply, token_supply, amount, tag, note_type, RECIPIENT] - - # assert that token_supply <= max_supply - lte assert.err=ERR_FUNGIBLE_ASSET_TOKEN_SUPPLY_EXCEEDS_MAX_SUPPLY - # => [max_supply, token_supply, amount, tag, note_type, RECIPIENT] + exec.supply_limits::check_supply + # => [amount, tag, note_type, RECIPIENT] - # assert max_supply <= FUNGIBLE_ASSET_MAX_AMOUNT - dup lte.FUNGIBLE_ASSET_MAX_AMOUNT - assert.err=ERR_FUNGIBLE_ASSET_MAX_SUPPLY_EXCEEDS_FUNGIBLE_ASSET_MAX_AMOUNT - # => [max_supply, token_supply, amount, tag, note_type, RECIPIENT] + # Read metadata to compute and store the updated token_supply. + # --------------------------------------------------------------------------------------------- - # assert token_supply + amount <= max_supply via supply_limits::check_supply - # Prepare args: [amount, token_supply, max_supply, ...] while keeping originals below - dup.2 dup.2 swap - # => [amount, token_supply, max_supply, token_supply, amount, tag, note_type, RECIPIENT] + push.METADATA_SLOT[0..2] exec.active_account::get_item + # => [token_symbol, decimals, max_supply, token_supply, amount, tag, note_type, RECIPIENT] - exec.supply_limits::check_supply + loc_storew_be.METADATA_SLOT_LOCAL + drop drop drop # => [token_supply, amount, tag, note_type, RECIPIENT] - # Compute the new token_supply and update in storage. - # --------------------------------------------------------------------------------------------- - dup.1 add # => [new_token_supply, amount, tag, note_type, RECIPIENT] padw loc_loadw_be.METADATA_SLOT_LOCAL - # => [[token_symbol, decimals, max_supply, token_supply], new_token_supply, amount, tag, note_type, RECIPIENT] + # => [token_symbol, decimals, max_supply, old_token_supply, new_token_supply, amount, tag, note_type, RECIPIENT] movup.3 drop - # => [[token_symbol, decimals, max_supply, new_token_supply], amount, tag, note_type, RECIPIENT] + # => [token_symbol, decimals, max_supply, new_token_supply, amount, tag, note_type, RECIPIENT] - # update the metadata slot with the new supply push.METADATA_SLOT[0..2] exec.native_account::set_item dropw # => [amount, tag, note_type, RECIPIENT] # Mint the asset. # --------------------------------------------------------------------------------------------- - # creating the asset exec.faucet::create_fungible_asset # => [ASSET, tag, note_type, RECIPIENT] - # mint the asset; this is needed to satisfy asset preservation logic. - # this ensures that the asset's faucet ID matches the native account's ID. - # this is ensured because create_fungible_asset creates the asset with the native account's ID exec.faucet::mint # => [ASSET, tag, note_type, RECIPIENT] @@ -124,11 +89,9 @@ pub proc distribute # Create a new note with the asset. # --------------------------------------------------------------------------------------------- - # create a note exec.output_note::create # => [note_idx, ASSET] - # load the ASSET and add it to the note dup movdn.5 movdn.5 # => [ASSET, note_idx, note_idx] @@ -182,17 +145,17 @@ pub proc burn exec.faucet::burn dropw # => [amount, pad(16)] - # Subtract burnt amount from current token_supply in storage. + # Assert that the burn does not exceed the current token supply. + # check_burn reads the metadata slot internally. # --------------------------------------------------------------------------------------------- - push.METADATA_SLOT[0..2] exec.active_account::get_item - # => [token_symbol, decimals, max_supply, token_supply, amount, pad(16)] + exec.supply_limits::check_burn + # => [amount, pad(16)] - dup.4 dup.4 - # => [token_supply, amount, token_symbol, decimals, max_supply, token_supply, amount, pad(16)] + # Subtract burnt amount from current token_supply in storage. + # --------------------------------------------------------------------------------------------- - # assert that amount <= token_supply - exec.supply_limits::check_burn + push.METADATA_SLOT[0..2] exec.active_account::get_item # => [token_symbol, decimals, max_supply, token_supply, amount, pad(16)] movup.3 movup.4 @@ -205,7 +168,6 @@ pub proc burn movdn.3 # => [token_symbol, decimals, max_supply, new_token_supply, pad(16)] - # update the metadata slot with the new supply push.METADATA_SLOT[0..2] exec.native_account::set_item dropw # => [pad(16)] end diff --git a/crates/miden-standards/asm/standards/faucets/timed_fungible.masm b/crates/miden-standards/asm/standards/faucets/timed_fungible.masm index e992714a75..9d5ff6cfdf 100644 --- a/crates/miden-standards/asm/standards/faucets/timed_fungible.masm +++ b/crates/miden-standards/asm/standards/faucets/timed_fungible.masm @@ -5,16 +5,10 @@ # Burns are always allowed. Supply tracking is handled by faucets::distribute/burn. # ================================================================================================= -use miden::protocol::active_account use miden::standards::faucets use miden::standards::access::ownable use miden::standards::supply::supply_limits -# CONSTANTS -# ================================================================================================= - -const TIMED_CONFIG_SLOT = word("miden::standards::faucets::timed_fungible::config") - # OWNER MANAGEMENT # ================================================================================================= @@ -50,14 +44,7 @@ pub use ownable::renounce_ownership pub proc distribute exec.ownable::verify_owner - # Check distribution period hasn't ended - push.TIMED_CONFIG_SLOT[0..2] exec.active_account::get_item - # => [distribution_end, 0, 0, 0, amount, tag, note_type, RECIPIENT] - exec.supply_limits::check_distribution_period - # => [0, 0, 0, amount, tag, note_type, RECIPIENT] - - drop drop drop # => [amount, tag, note_type, RECIPIENT] exec.faucets::distribute diff --git a/crates/miden-standards/asm/standards/faucets/timed_unlimited_fungible.masm b/crates/miden-standards/asm/standards/faucets/timed_unlimited_fungible.masm index be76144a6f..fe600c7458 100644 --- a/crates/miden-standards/asm/standards/faucets/timed_unlimited_fungible.masm +++ b/crates/miden-standards/asm/standards/faucets/timed_unlimited_fungible.masm @@ -6,16 +6,10 @@ # Burns are always allowed. Supply tracking is handled by faucets::distribute/burn. # ================================================================================================= -use miden::protocol::active_account use miden::standards::faucets use miden::standards::access::ownable use miden::standards::supply::supply_limits -# CONSTANTS -# ================================================================================================= - -const TIMED_CONFIG_SLOT = word("miden::standards::faucets::timed_fungible::config") - # OWNER MANAGEMENT # ================================================================================================= @@ -52,14 +46,7 @@ pub use ownable::renounce_ownership pub proc distribute exec.ownable::verify_owner - # Check distribution period hasn't ended - push.TIMED_CONFIG_SLOT[0..2] exec.active_account::get_item - # => [distribution_end, 0, 0, 0, amount, tag, note_type, RECIPIENT] - exec.supply_limits::check_distribution_period - # => [0, 0, 0, amount, tag, note_type, RECIPIENT] - - drop drop drop # => [amount, tag, note_type, RECIPIENT] exec.faucets::distribute diff --git a/crates/miden-standards/asm/standards/supply/supply_limits.masm b/crates/miden-standards/asm/standards/supply/supply_limits.masm index 1abb166928..b60f097ce2 100644 --- a/crates/miden-standards/asm/standards/supply/supply_limits.masm +++ b/crates/miden-standards/asm/standards/supply/supply_limits.masm @@ -3,34 +3,58 @@ # Reusable supply validation utilities for faucets. # Pure check procedures — validate constraints but do not modify storage. +use miden::protocol::active_account use miden::protocol::tx +use ::miden::protocol::asset::FUNGIBLE_ASSET_MAX_AMOUNT # ERRORS # ================================================================================================= const ERR_DISTRIBUTION_PERIOD_ENDED = "distribution period has ended" +const ERR_TOKEN_SUPPLY_EXCEEDS_MAX_SUPPLY = "token supply exceeds max supply" +const ERR_MAX_SUPPLY_EXCEEDS_MAX_AMOUNT = "max supply exceeds maximum representable fungible asset amount" const ERR_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY = "amount would exceed the maximum supply" const ERR_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY = "asset amount to burn exceeds the existing token supply" +# CONSTANTS +# ================================================================================================= + +# The standard slot where fungible faucet metadata is stored. +# Word layout: [token_supply, max_supply, decimals, token_symbol] +const METADATA_SLOT = word("miden::standards::fungible_faucets::metadata") + +# The storage slot where timed faucet configuration is stored. +# Word layout: [0, 0, 0, distribution_end] +const TIMED_CONFIG_SLOT = word("miden::standards::faucets::timed_fungible::config") + # PROCEDURES # ================================================================================================= #! Asserts the distribution period is still active. +#! +#! Reads the timed config slot from the active account to get distribution_end_block. #! If distribution_end_block is 0, the check is skipped (no time limit). #! -#! Inputs: [distribution_end_block, ...] +#! Inputs: [...] #! Outputs: [...] #! #! Panics if: current_block >= distribution_end_block. #! #! Invocation: exec pub proc check_distribution_period + push.TIMED_CONFIG_SLOT[0..2] exec.active_account::get_item + # => [distribution_end, 0, 0, 0, ...] + + # Move distribution_end out and drop the 3 padding zeros + movdn.3 drop drop drop + # => [distribution_end, ...] + dup if.true exec.tx::get_block_number - # => [current_block, distribution_end_block, ...] + # => [current_block, distribution_end, ...] swap - # => [distribution_end_block, current_block, ...] + # => [distribution_end, current_block, ...] lt assert.err=ERR_DISTRIBUTION_PERIOD_ENDED # => [...] else @@ -38,39 +62,70 @@ pub proc check_distribution_period end end -#! Asserts that minting `amount` would not exceed max_supply. +#! Validates all supply constraints for minting. #! -#! Inputs: [amount, token_supply, max_supply, ...] -#! Outputs: [...] +#! Reads the fungible faucet metadata slot to get token_supply and max_supply, +#! then checks: +#! 1. token_supply <= max_supply (state consistency invariant) +#! 2. max_supply <= FUNGIBLE_ASSET_MAX_AMOUNT (protocol bound) +#! 3. token_supply + amount <= max_supply (mint does not exceed cap) #! -#! Panics if: token_supply + amount > max_supply. +#! Check 3 transitively ensures token_supply + amount <= FUNGIBLE_ASSET_MAX_AMOUNT +#! because max_supply is bounded by check 2. +#! +#! Inputs: [amount, ...] +#! Outputs: [amount, ...] +#! +#! Panics if any of the above checks fail. #! #! Invocation: exec pub proc check_supply - # new_supply = token_supply + amount - swap add - # => [new_supply, max_supply, ...] + push.METADATA_SLOT[0..2] exec.active_account::get_item + # => [token_symbol, decimals, max_supply, token_supply, amount, ...] + + drop drop + # => [max_supply, token_supply, amount, ...] + + # Check 1: assert token_supply <= max_supply + # (lte pops [b, a, ...] and checks a <= b) + dup.1 dup.1 + # => [max_supply, token_supply, max_supply, token_supply, amount, ...] + lte assert.err=ERR_TOKEN_SUPPLY_EXCEEDS_MAX_SUPPLY + # => [max_supply, token_supply, amount, ...] - swap - # => [max_supply, new_supply, ...] + # Check 2: assert max_supply <= FUNGIBLE_ASSET_MAX_AMOUNT + dup lte.FUNGIBLE_ASSET_MAX_AMOUNT + assert.err=ERR_MAX_SUPPLY_EXCEEDS_MAX_AMOUNT + # => [max_supply, token_supply, amount, ...] - # assert new_supply <= max_supply - # (b = max_supply, a = new_supply; lte checks a <= b) + # Check 3: assert token_supply + amount <= max_supply + swap dup.2 add swap + # => [max_supply, new_supply, amount, ...] lte assert.err=ERR_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY - # => [...] + # => [amount, ...] end #! Asserts that burning `amount` would not reduce supply below zero. #! -#! Inputs: [token_supply, amount, ...] -#! Outputs: [...] +#! Reads the fungible faucet metadata slot to get token_supply, +#! then asserts amount <= token_supply. +#! +#! Inputs: [amount, ...] +#! Outputs: [amount, ...] #! #! Panics if: amount > token_supply. #! #! Invocation: exec pub proc check_burn - # assert amount <= token_supply - # (b = token_supply, a = amount; lte checks a <= b) + push.METADATA_SLOT[0..2] exec.active_account::get_item + # => [token_symbol, decimals, max_supply, token_supply, amount, ...] + + drop drop drop + # => [token_supply, amount, ...] + + # assert amount <= token_supply, preserving amount below + dup.1 swap + # => [token_supply, amount, amount, ...] lte assert.err=ERR_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY - # => [...] + # => [amount, ...] end diff --git a/crates/miden-standards/src/account/components/mod.rs b/crates/miden-standards/src/account/components/mod.rs index 3598b7c330..cb88c207f8 100644 --- a/crates/miden-standards/src/account/components/mod.rs +++ b/crates/miden-standards/src/account/components/mod.rs @@ -248,9 +248,7 @@ impl StandardAccountComponent { Self::BasicFungibleFaucet => BASIC_FUNGIBLE_FAUCET_LIBRARY.as_ref(), Self::UnlimitedFungibleFaucet => UNLIMITED_FUNGIBLE_FAUCET_LIBRARY.as_ref(), Self::TimedFungibleFaucet => TIMED_FUNGIBLE_FAUCET_LIBRARY.as_ref(), - Self::TimedUnlimitedFungibleFaucet => { - TIMED_UNLIMITED_FUNGIBLE_FAUCET_LIBRARY.as_ref() - }, + Self::TimedUnlimitedFungibleFaucet => TIMED_UNLIMITED_FUNGIBLE_FAUCET_LIBRARY.as_ref(), Self::NetworkFungibleFaucet => NETWORK_FUNGIBLE_FAUCET_LIBRARY.as_ref(), Self::AuthEcdsaK256Keccak => ECDSA_K256_KECCAK_LIBRARY.as_ref(), Self::AuthEcdsaK256KeccakAcl => ECDSA_K256_KECCAK_ACL_LIBRARY.as_ref(), @@ -304,10 +302,8 @@ impl StandardAccountComponent { Self::TimedFungibleFaucet => { component_interface_vec.push(AccountComponentInterface::TimedFungibleFaucet) }, - Self::TimedUnlimitedFungibleFaucet => { - component_interface_vec - .push(AccountComponentInterface::TimedUnlimitedFungibleFaucet) - }, + Self::TimedUnlimitedFungibleFaucet => component_interface_vec + .push(AccountComponentInterface::TimedUnlimitedFungibleFaucet), Self::NetworkFungibleFaucet => { component_interface_vec.push(AccountComponentInterface::NetworkFungibleFaucet) }, diff --git a/crates/miden-standards/src/account/faucets/mod.rs b/crates/miden-standards/src/account/faucets/mod.rs index 5c3308f130..5f1cc7bfa5 100644 --- a/crates/miden-standards/src/account/faucets/mod.rs +++ b/crates/miden-standards/src/account/faucets/mod.rs @@ -15,7 +15,8 @@ pub use basic_fungible::{BasicFungibleFaucet, create_basic_fungible_faucet}; pub use network_fungible::{NetworkFungibleFaucet, create_network_fungible_faucet}; pub use timed_fungible::{TimedFungibleFaucet, create_timed_fungible_faucet}; pub use timed_unlimited_fungible::{ - TimedUnlimitedFungibleFaucet, create_timed_unlimited_fungible_faucet, + TimedUnlimitedFungibleFaucet, + create_timed_unlimited_fungible_faucet, }; pub use token_metadata::TokenMetadata; pub use unlimited_fungible::{UnlimitedFungibleFaucet, create_unlimited_fungible_faucet}; diff --git a/crates/miden-standards/src/account/faucets/timed_unlimited_fungible.rs b/crates/miden-standards/src/account/faucets/timed_unlimited_fungible.rs index 5cff21658e..f4c6e7dd9c 100644 --- a/crates/miden-standards/src/account/faucets/timed_unlimited_fungible.rs +++ b/crates/miden-standards/src/account/faucets/timed_unlimited_fungible.rs @@ -162,9 +162,7 @@ impl TimedUnlimitedFungibleFaucet { if !interface .components() .contains(&AccountComponentInterface::TimedUnlimitedFungibleFaucet) - && !interface - .components() - .contains(&AccountComponentInterface::TimedFungibleFaucet) + && !interface.components().contains(&AccountComponentInterface::TimedFungibleFaucet) { return Err(FungibleFaucetError::MissingTimedUnlimitedFungibleFaucetInterface); } @@ -578,10 +576,7 @@ mod tests { let err = TimedUnlimitedFungibleFaucet::try_from(invalid_faucet_account) .err() .expect("timed unlimited fungible faucet creation should fail"); - assert_matches!( - err, - FungibleFaucetError::MissingTimedUnlimitedFungibleFaucetInterface - ); + assert_matches!(err, FungibleFaucetError::MissingTimedUnlimitedFungibleFaucetInterface); } #[test] diff --git a/crates/miden-standards/src/account/interface/extension.rs b/crates/miden-standards/src/account/interface/extension.rs index 32dc60c8e3..565f141942 100644 --- a/crates/miden-standards/src/account/interface/extension.rs +++ b/crates/miden-standards/src/account/interface/extension.rs @@ -112,9 +112,7 @@ impl AccountInterfaceExt for AccountInterface { }, AccountComponentInterface::TimedUnlimitedFungibleFaucet => { component_proc_digests.extend( - timed_unlimited_fungible_faucet_library() - .mast_forest() - .procedure_digests(), + timed_unlimited_fungible_faucet_library().mast_forest().procedure_digests(), ); }, AccountComponentInterface::NetworkFungibleFaucet => { diff --git a/crates/miden-standards/src/errors/standards.rs b/crates/miden-standards/src/errors/standards.rs index 93e685ca28..116a61f091 100644 --- a/crates/miden-standards/src/errors/standards.rs +++ b/crates/miden-standards/src/errors/standards.rs @@ -21,16 +21,12 @@ pub const ERR_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY: MasmError = MasmError::from_ /// Error Message: "distribution period has ended" pub const ERR_DISTRIBUTION_PERIOD_ENDED: MasmError = MasmError::from_static_str("distribution period has ended"); -/// Error Message: "token_supply plus the amount passed to distribute would exceed the maximum supply" -pub const ERR_FUNGIBLE_ASSET_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY: MasmError = MasmError::from_static_str("token_supply plus the amount passed to distribute would exceed the maximum supply"); -/// Error Message: "max supply exceeds maximum representable fungible asset amount" -pub const ERR_FUNGIBLE_ASSET_MAX_SUPPLY_EXCEEDS_FUNGIBLE_ASSET_MAX_AMOUNT: MasmError = MasmError::from_static_str("max supply exceeds maximum representable fungible asset amount"); -/// Error Message: "token supply exceeds max supply" -pub const ERR_FUNGIBLE_ASSET_TOKEN_SUPPLY_EXCEEDS_MAX_SUPPLY: MasmError = MasmError::from_static_str("token supply exceeds max supply"); - /// Error Message: "number of approvers must be equal to or greater than threshold" pub const ERR_MALFORMED_MULTISIG_CONFIG: MasmError = MasmError::from_static_str("number of approvers must be equal to or greater than threshold"); +/// Error Message: "max supply exceeds maximum representable fungible asset amount" +pub const ERR_MAX_SUPPLY_EXCEEDS_MAX_AMOUNT: MasmError = MasmError::from_static_str("max supply exceeds maximum representable fungible asset amount"); + /// Error Message: "MINT script expects exactly 12 storage items for private or 16+ storage items for public output notes" pub const ERR_MINT_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS: MasmError = MasmError::from_static_str("MINT script expects exactly 12 storage items for private or 16+ storage items for public output notes"); @@ -64,6 +60,9 @@ pub const ERR_SWAP_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS: MasmError = MasmError::fr /// Error Message: "SWAP script requires exactly 1 note asset" pub const ERR_SWAP_WRONG_NUMBER_OF_ASSETS: MasmError = MasmError::from_static_str("SWAP script requires exactly 1 note asset"); +/// Error Message: "token supply exceeds max supply" +pub const ERR_TOKEN_SUPPLY_EXCEEDS_MAX_SUPPLY: MasmError = MasmError::from_static_str("token supply exceeds max supply"); + /// Error Message: "failed to approve multisig transaction as it was already executed" pub const ERR_TX_ALREADY_EXECUTED: MasmError = MasmError::from_static_str("failed to approve multisig transaction as it was already executed"); diff --git a/crates/miden-testing/tests/scripts/faucet.rs b/crates/miden-testing/tests/scripts/faucet.rs index 1a89230732..af15cd03a7 100644 --- a/crates/miden-testing/tests/scripts/faucet.rs +++ b/crates/miden-testing/tests/scripts/faucet.rs @@ -35,7 +35,7 @@ use miden_standards::account::faucets::{ use miden_standards::code_builder::CodeBuilder; use miden_standards::errors::standards::{ ERR_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY, - ERR_FUNGIBLE_ASSET_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY, + ERR_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY, ERR_SENDER_NOT_OWNER, }; use miden_standards::note::{BurnNote, MintNote, MintNoteStorage, StandardNote}; @@ -193,7 +193,7 @@ async fn faucet_contract_mint_fungible_asset_fails_exceeds_max_supply() -> anyho .execute() .await; - assert_transaction_executor_error!(tx, ERR_FUNGIBLE_ASSET_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY); + assert_transaction_executor_error!(tx, ERR_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY); Ok(()) }