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..f7b0c63437 --- /dev/null +++ b/crates/miden-standards/asm/account_components/faucets/timed_fungible_faucet.masm @@ -0,0 +1,8 @@ +# 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 +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/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/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..a700390731 --- /dev/null +++ b/crates/miden-standards/asm/account_components/faucets/unlimited_fungible_faucet.masm @@ -0,0 +1,8 @@ +# 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 +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/mod.masm b/crates/miden-standards/asm/standards/faucets/mod.masm index 5451bb9655..76bcd51a9d 100644 --- a/crates/miden-standards/asm/standards/faucets/mod.masm +++ b/crates/miden-standards/asm/standards/faucets/mod.masm @@ -3,27 +3,14 @@ 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 -# CONSTANTS +# ERRORS # ================================================================================================= -const PRIVATE_NOTE=2 - -# 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_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. @@ -55,82 +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) (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 + # 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] - - # 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] + exec.supply_limits::check_supply + # => [amount, tag, note_type, RECIPIENT] - dup.2 swap dup.2 - # => [token_supply, max_supply, amount, token_supply, amount, tag, note_type, RECIPIENT] + # Read metadata to compute and store the updated token_supply. + # --------------------------------------------------------------------------------------------- - # 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] + push.METADATA_SLOT[0..2] exec.active_account::get_item + # => [token_symbol, decimals, 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 + 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] @@ -140,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] @@ -198,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 - lte assert.err=ERR_FAUCET_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY + push.METADATA_SLOT[0..2] exec.active_account::get_item # => [token_symbol, decimals, max_supply, token_supply, amount, pad(16)] movup.3 movup.4 @@ -221,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 new file mode 100644 index 0000000000..9d5ff6cfdf --- /dev/null +++ b/crates/miden-standards/asm/standards/faucets/timed_fungible.masm @@ -0,0 +1,70 @@ +# TIMED SUPPLY FUNGIBLE FAUCET CONTRACT +# +# 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::standards::faucets +use miden::standards::access::ownable +use miden::standards::supply::supply_limits + +# 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. +#! +#! 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 + + exec.supply_limits::check_distribution_period + # => [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/asm/standards/faucets/timed_unlimited_fungible.masm b/crates/miden-standards/asm/standards/faucets/timed_unlimited_fungible.masm new file mode 100644 index 0000000000..fe600c7458 --- /dev/null +++ b/crates/miden-standards/asm/standards/faucets/timed_unlimited_fungible.masm @@ -0,0 +1,72 @@ +# 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::standards::faucets +use miden::standards::access::ownable +use miden::standards::supply::supply_limits + +# 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 + + exec.supply_limits::check_distribution_period + # => [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/asm/standards/faucets/unlimited_fungible.masm b/crates/miden-standards/asm/standards/faucets/unlimited_fungible.masm new file mode 100644 index 0000000000..33a41e4678 --- /dev/null +++ b/crates/miden-standards/asm/standards/faucets/unlimited_fungible.masm @@ -0,0 +1,63 @@ +# UNLIMITED SUPPLY FUNGIBLE FAUCET CONTRACT +# +# A fungible faucet with no supply restrictions. +# This faucet allows unlimited minting and burning of tokens. +# ================================================================================================= + +use miden::standards::faucets +use miden::standards::access::ownable + +# 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 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. +#! - the note sender is not the owner of this faucet. +#! +#! Invocation: exec +pub proc distribute + exec.ownable::verify_owner + # => [amount, tag, note_type, RECIPIENT] + + exec.faucets::distribute + # => [note_idx] +end + +#! Burns the fungible asset from the active note. +#! +#! 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)] +#! +#! 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 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 new file mode 100644 index 0000000000..b60f097ce2 --- /dev/null +++ b/crates/miden-standards/asm/standards/supply/supply_limits.masm @@ -0,0 +1,131 @@ +# miden::standards::supply::supply_limits +# +# 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: [...] +#! 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, ...] + swap + # => [distribution_end, current_block, ...] + lt assert.err=ERR_DISTRIBUTION_PERIOD_ENDED + # => [...] + else + drop + end +end + +#! Validates all supply constraints for minting. +#! +#! 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) +#! +#! 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 + 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, ...] + + # 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, ...] + + # 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. +#! +#! 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 + 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 7a7df0a0d3..cb88c207f8 100644 --- a/crates/miden-standards/src/account/components/mod.rs +++ b/crates/miden-standards/src/account/components/mod.rs @@ -108,6 +108,35 @@ 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") +}); + +// 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 // ================================================================================================ @@ -130,6 +159,21 @@ 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 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() @@ -183,6 +227,9 @@ pub fn falcon_512_rpo_multisig_library() -> Library { pub enum StandardAccountComponent { BasicWallet, BasicFungibleFaucet, + UnlimitedFungibleFaucet, + TimedFungibleFaucet, + TimedUnlimitedFungibleFaucet, NetworkFungibleFaucet, AuthEcdsaK256Keccak, AuthEcdsaK256KeccakAcl, @@ -199,6 +246,9 @@ 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::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(), @@ -246,6 +296,14 @@ 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::TimedUnlimitedFungibleFaucet => component_interface_vec + .push(AccountComponentInterface::TimedUnlimitedFungibleFaucet), Self::NetworkFungibleFaucet => { component_interface_vec.push(AccountComponentInterface::NetworkFungibleFaucet) }, @@ -280,6 +338,12 @@ 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); + // 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 2b7eab4fef..5f1cc7bfa5 100644 --- a/crates/miden-standards/src/account/faucets/mod.rs +++ b/crates/miden-standards/src/account/faucets/mod.rs @@ -6,11 +6,20 @@ 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}; // FUNGIBLE FAUCET ERROR // ================================================================================================ @@ -32,6 +41,18 @@ 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( + "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/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 new file mode 100644 index 0000000000..66d9e58058 --- /dev/null +++ b/crates/miden-standards/src/account/faucets/timed_fungible.rs @@ -0,0 +1,571 @@ +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_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 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 +); + +/// 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. Burns are always allowed. +/// +/// 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 TimedFungibleFaucet { + metadata: TokenMetadata, + distribution_end: u32, + owner_account_id: AccountId, +} + +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, + max_supply: Felt, + distribution_end: u32, + owner_account_id: AccountId, + ) -> Result { + let metadata = TokenMetadata::new(symbol, decimals, max_supply)?; + 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, + owner_account_id: AccountId, + ) -> Self { + Self { + metadata, + distribution_end, + owner_account_id, + } + } + + /// 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 timed config: [0, 0, 0, distribution_end] + let config_word: Word = storage + .get_item(TimedFungibleFaucet::timed_config_slot()) + .map_err(|err| FungibleFaucetError::StorageLookupFailed { + slot_name: TimedFungibleFaucet::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(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 + // -------------------------------------------------------------------------------------------- + + /// Returns the [`StorageSlotName`] where the [`TimedFungibleFaucet`]'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_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, Felt::ZERO, Felt::ZERO, Felt::new(faucet.distribution_end as u64)]; + + let config_slot = StorageSlot::with_value( + 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::timed_config_slot_schema(), + TimedFungibleFaucet::owner_config_slot_schema(), + ]) + .expect("storage schema should be valid"); + + let metadata = AccountComponentMetadata::new(TimedFungibleFaucet::NAME) + .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, owner_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, owner account, and provided metadata (token symbol, +/// 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 +/// 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 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, + storage_mode: AccountStorageMode, + owner_account_id: AccountId, +) -> Result { + let auth_component: AccountComponent = NoAuth::new().into(); + + let faucet_component = + TimedFungibleFaucet::new(symbol, decimals, max_supply, 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::testing::account_id::ACCOUNT_ID_SENDER; + use miden_protocol::{FieldElement, Word}; + + use super::{ + AccountBuilder, + AccountId, + AccountStorageMode, + AccountType, + Felt, + FungibleFaucetError, + TimedFungibleFaucet, + TokenSymbol, + create_timed_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_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 max_supply = Felt::new(1_000_000); + let token_symbol = TokenSymbol::try_from("TMD").unwrap(); + let decimals = 6u8; + let distribution_end = 10_000u32; + let storage_mode = AccountStorageMode::Private; + + let faucet_account = create_timed_fungible_faucet( + init_seed, + token_symbol, + decimals, + max_supply, + distribution_end, + storage_mode, + owner_account_id, + ) + .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 timed config slot + assert_eq!( + faucet_account + .storage() + .get_item(TimedFungibleFaucet::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(TimedFungibleFaucet::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 = 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.owner_account_id(), owner_account_id); + } + + #[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 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, + owner_account_id, + ) + .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_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) + .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/timed_unlimited_fungible.rs b/crates/miden-standards/src/account/faucets/timed_unlimited_fungible.rs new file mode 100644 index 0000000000..f4c6e7dd9c --- /dev/null +++ b/crates/miden-standards/src/account/faucets/timed_unlimited_fungible.rs @@ -0,0 +1,587 @@ +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/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 new file mode 100644 index 0000000000..8fb6b8ed82 --- /dev/null +++ b/crates/miden-standards/src/account/faucets/unlimited_fungible.rs @@ -0,0 +1,489 @@ +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, Word}; + +use super::token_metadata::TOKEN_SYMBOL_TYPE_ID; +use super::{FungibleFaucetError, TokenMetadata}; +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 +// ================================================================================================ + +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 +); + +/// 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`]. +/// - [`Self::owner_config_slot`]: Stores the owner account ID `[0, 0, suffix, prefix]`. +/// +/// [builder]: crate::code_builder::CodeBuilder +pub struct UnlimitedFungibleFaucet { + metadata: TokenMetadata, + owner_account_id: AccountId, +} + +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, + 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, owner_account_id }) + } + + /// Creates a new [`UnlimitedFungibleFaucet`] component from the given [`TokenMetadata`]. + 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 + /// 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)?; + + // 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 + // -------------------------------------------------------------------------------------------- + + /// 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 [`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 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 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(), + 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![metadata_slot, owner_slot], + metadata, + ) + .expect("unlimited fungible faucet component should satisfy the requirements of a valid account component") + } +} + +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, 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. +/// - `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( + init_seed: [u8; 32], + symbol: TokenSymbol, + decimals: u8, + storage_mode: AccountStorageMode, + owner_account_id: AccountId, +) -> Result { + let auth_component: AccountComponent = NoAuth::new().into(); + + let faucet_component = UnlimitedFungibleFaucet::new(symbol, decimals, 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, + TokenSymbol, + UnlimitedFungibleFaucet, + create_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 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("UNL").unwrap(); + let decimals = 8u8; + let storage_mode = AccountStorageMode::Private; + + let faucet_account = create_unlimited_fungible_faucet( + init_seed, + token_symbol, + decimals, + 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(UnlimitedFungibleFaucet::metadata_slot()) + .unwrap(), + [ + Felt::ZERO, + Felt::new(FungibleAsset::MAX_AMOUNT), + Felt::new(8), + token_symbol.into() + ] + .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] + 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 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, owner_account_id) + .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); + 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) + .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(); + } +} diff --git a/crates/miden-standards/src/account/interface/component.rs b/crates/miden-standards/src/account/interface/component.rs index 110e23d1b3..7931188444 100644 --- a/crates/miden-standards/src/account/interface/component.rs +++ b/crates/miden-standards/src/account/interface/component.rs @@ -29,6 +29,16 @@ 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 + /// [`TimedUnlimitedFungibleFaucet`][crate::account::faucets::TimedUnlimitedFungibleFaucet] + /// module. + TimedUnlimitedFungibleFaucet, + /// Exposes procedures from the /// [`NetworkFungibleFaucet`][crate::account::faucets::NetworkFungibleFaucet] module. NetworkFungibleFaucet, /// Exposes procedures from the @@ -71,6 +81,13 @@ 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::TimedUnlimitedFungibleFaucet => { + "Timed Unlimited Fungible Faucet".to_string() + }, AccountComponentInterface::NetworkFungibleFaucet => { "Network Fungible Faucet".to_string() }, @@ -264,6 +281,81 @@ 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::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 9954d32632..565f141942 100644 --- a/crates/miden-standards/src/account/interface/extension.rs +++ b/crates/miden-standards/src/account/interface/extension.rs @@ -21,6 +21,9 @@ use crate::account::components::{ falcon_512_rpo_multisig_library, network_fungible_faucet_library, no_auth_library, + timed_fungible_faucet_library, + timed_unlimited_fungible_faucet_library, + unlimited_fungible_faucet_library, }; use crate::account::interface::{ AccountComponentInterface, @@ -98,6 +101,20 @@ 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::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(), diff --git a/crates/miden-standards/src/errors/standards.rs b/crates/miden-standards/src/errors/standards.rs index f16cc23ed4..116a61f091 100644 --- a/crates/miden-standards/src/errors/standards.rs +++ b/crates/miden-standards/src/errors/standards.rs @@ -13,18 +13,20 @@ use miden_protocol::errors::MasmError; 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_FAUCET_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY: MasmError = MasmError::from_static_str("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: "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: "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: "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"); @@ -58,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 351648ea5e..af15cd03a7 100644 --- a/crates/miden-testing/tests/scripts/faucet.rs +++ b/crates/miden-testing/tests/scripts/faucet.rs @@ -34,8 +34,8 @@ use miden_standards::account::faucets::{ }; use miden_standards::code_builder::CodeBuilder; use miden_standards::errors::standards::{ - ERR_FAUCET_BURN_AMOUNT_EXCEEDS_TOKEN_SUPPLY, - ERR_FUNGIBLE_ASSET_DISTRIBUTE_AMOUNT_EXCEEDS_MAX_SUPPLY, + ERR_BURN_AMOUNT_EXCEEDS_TOKEN_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(()) } @@ -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(()) } 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 +)?; +```