diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cd583af25..0815228d92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ - [BREAKING] Narrowed the block header version field from 32 to 8 bits and set it to the only supported version instead of taking it as a `BlockHeader::new` parameter ([#3696](https://github.com/0xMiden/protocol/pull/3696)). - [BREAKING] Serialize the version in `Account`, `AccountHeader`, `PartialNoteMetadata` and `AssetId` ([#3697](https://github.com/0xMiden/protocol/pull/3697)). - [BREAKING] Moved the account delta and patch domain separators into the hasher capacity word. Added a version to their commitments ([#3698](https://github.com/0xMiden/protocol/pull/3698)). +- [BREAKING] Replaced the dedicated AggLayer faucet account component with the standard `FungibleFaucet`. `AggLayerFaucet` is now a stateless namespace, `AgglayerFaucetError` and the `miden-agglayer-faucet` MASM package were removed, `AggLayerFaucet::account_builder` and `create_existing_agglayer_faucet` take a token name, and `account_builder` now takes `TokenName` / `TokenSymbol` / `AssetAmount` rather than `&str` / `Felt` ([#3525](https://github.com/0xMiden/protocol/pull/3525)). - [BREAKING] Introduced `ProtocolConfig` that commits to all kernel's procedures, the fee asset ID and the security policy for recursive verification ([#3725](https://github.com/0xMiden/protocol/pull/3725)). - [BREAKING] Renamed `ValidatorKeys` to `ValidatorConfig` and added a quorum to it ([#3725](https://github.com/0xMiden/protocol/pull/3725)). - [BREAKING] Replaced `tx::get_fee_faucet_id` with `tx::get_fee_asset_id` ([#3741](https://github.com/0xMiden/protocol/pull/3741)). diff --git a/bin/bench-transaction/src/context_setups/mod.rs b/bin/bench-transaction/src/context_setups/mod.rs index c3153831a0..ae2f26695c 100644 --- a/bin/bench-transaction/src/context_setups/mod.rs +++ b/bin/bench-transaction/src/context_setups/mod.rs @@ -498,6 +498,7 @@ pub async fn tx_consume_claim_note( let (proof_data, leaf_data, ger, _cgi_chain_hash) = data_source.get_data(); // CREATE AGGLAYER FAUCET ACCOUNT + let token_name = "AggLayer Token"; let token_symbol = "AGG"; let decimals = 8u8; let max_supply: Felt = FungibleAsset::MAX_AMOUNT.into(); @@ -509,6 +510,7 @@ pub async fn tx_consume_claim_note( let agglayer_faucet = create_existing_agglayer_faucet( agglayer_faucet_seed, + token_name, token_symbol, decimals, max_supply, @@ -706,6 +708,7 @@ pub async fn tx_consume_b2agg_note( let faucet = create_existing_agglayer_faucet( builder.rng_mut().draw_word(), + "AggLayer Token", "AGG", 8, FungibleAsset::MAX_AMOUNT.into(), @@ -716,7 +719,11 @@ pub async fn tx_consume_b2agg_note( builder.add_account(faucet.clone())?; // CREATE CONFIG_AGG_BRIDGE NOTE (registers faucet + token address in bridge) - let metadata_hash = MetadataHash::from_token_info("AGG", "AGG", 8); + let metadata_hash = MetadataHash::from_token_info( + &vectors.token_name, + &vectors.token_symbol, + vectors.token_decimals, + ); let config_note = ConfigAggBridgeNote::create( ConversionMetadata { faucet_account_id: faucet.id(), @@ -791,6 +798,7 @@ fn setup_faucet_registration( let agglayer_faucet = create_existing_agglayer_faucet( builder.rng_mut().draw_word(), + "AggLayer Token", "AGG", 8, FungibleAsset::MAX_AMOUNT.into(), diff --git a/crates/miden-agglayer/SPEC.md b/crates/miden-agglayer/SPEC.md index 76da54c200..c1a37e73b0 100644 --- a/crates/miden-agglayer/SPEC.md +++ b/crates/miden-agglayer/SPEC.md @@ -21,7 +21,7 @@ implementation are called out inline with `TODO (Future)` markers. |--------|-------------|--------------| | **User** | End-user Miden account that holds assets and initiates bridge-out deposits, or receives assets from a bridge-in claim. | Any account with `basic_wallet` component | | **AggLayer Bridge** | Onchain bridge account that manages the Local Exit Tree (LET), faucet registry, and GER state. Consumes B2AGG, CONFIG, and UPDATE_GER notes. | Network-mode account with a single `bridge` component | -| **AggLayer Faucet** | Fungible faucet that represents a single bridged token. Mints on bridge-in claims, burns on bridge-out. Each foreign token has its own faucet instance. | `FungibleFaucet`, network-mode, with `agglayer_faucet` component | +| **AggLayer Faucet** | Fungible faucet that represents a single bridged token. Mints on bridge-in claims, burns on bridge-out. Each foreign token has its own faucet instance. | `FungibleFaucet`, network-mode, owned by the bridge | | **Integration Service** (offchain) | Observes L1 events (deposits, GER updates) and creates UPDATE_GER and CLAIM notes on Miden. Trusted to provide correct proofs and data. | Creates notes targeting bridge/faucet | | **Bridge Operator** (offchain) | Deploys bridge and faucet accounts. Creates CONFIG_AGG_BRIDGE notes to register faucets. Must hold the `FAUCET_MNGR` role. | Creates config notes | | **Bridge Admin (`BRIDGE_ADMIN`)** (offchain) | Holds the bridge account's built-in `ADMIN` role. Manages bridge roles, restores a paused bridge, and controls allowlists and fee-policy selection. | Creates RBAC_CONFIG, PAUSE_CONFIG, and NETWORK_ACCOUNT_CONFIG notes | @@ -478,21 +478,25 @@ Initial state: all map slots empty, all value slots `[0, 0, 0, 0]`. The initial member and the initial `FAUCET_MNGR` / `GER_INJECTOR` / `GER_REMOVER` / `FEE_MNGR` / `PAUSER` role holders are seeded into the access-control components at account creation time. -### 3.2 Faucet Account Component +### 3.2 Faucet Account -The faucet account has the `agglayer_faucet` component (`components/faucet.masm`), -which is a thin wrapper, on top of `Ownable2Step` + the RBAC access-control stack -(`RoleBasedAccessControl` + `Authority::RbacControlled`), that re-exports the -standard Miden fungible-faucet procedures: +The faucet account is the standard `FungibleFaucet` installed on top of `Ownable2Step` and the +RBAC access-control stack (`RoleBasedAccessControl` + `Authority::RbacControlled`). Bridge-in and +bridge-out run through the standard `mint_and_send` and `receive_and_burn`, and the unified +MINT/BURN note scripts use `has_procedure` to detect the faucet kind reflectively. -- `mint_and_send` (from `miden::standards::faucets::fungible::mint_and_send`) -- `receive_and_burn` (from `miden::standards::faucets::fungible::receive_and_burn`) -- `has_procedure` (from the `CodeInspection` component, so the unified MINT/BURN note scripts - can reflectively detect the faucet kind) +What makes it an AggLayer faucet is its deployment configuration: -The underlying library code lives in `asm/agglayer/faucet/mod.masm`. +- The bridge is its `Ownable2Step` owner, and its active mint and burn policies are `owner_only`, + so only the bridge can mint on a bridge-in claim or burn on a bridge-out. +- Its note allowlist is limited to MINT, BURN, RBAC_CONFIG and CONSTANT_FEE_POLICY_CONFIG plus the + network-account defaults, which keeps the standard `set_*` metadata setters unreachable until + `FAUCET_ADMIN` explicitly allowlists a note that calls them. +- Its token name holds the foreign token's real name rather than a copy of its symbol, so the + faucet carries the metadata hash preimage `abi.encode(name, symbol, decimals)` (see + [Section 7.1](#71-registering-faucets-on-miden)). -#### `agglayer_faucet::mint_and_send` +#### `fungible::mint_and_send` | | | |-|-| @@ -502,7 +506,7 @@ The underlying library code lives in `asm/agglayer/faucet/mod.masm`. | **Context** | Consuming a `MINT` note on the faucet account | | **Panics** | Faucet owner verification fails; minting exceeds supply; the asset stored in the MINT note does not belong to the consuming faucet | -Re-export of `miden::standards::faucets::fungible::mint_and_send`. Mints the asset +Mints the asset identified by `ASSET_ID` / `ASSET_VALUE` and creates an output note with the given recipient. Requires the faucet's owner (the bridge account) to be the creator of this note (the bridge is stored in `Ownable2Step` storage slot as the owner; the faucet's @@ -511,9 +515,9 @@ recipient. Requires the faucet's owner (the bridge account) to be the creator of for the active faucet and panics if the stored `ASSET_ID` does not belong to that faucet, which binds the MINT note to its resolved faucet (see §4.10). -#### `agglayer_faucet::receive_and_burn` +#### `fungible::receive_and_burn` -This is a re-export of `miden::standards::faucets::fungible::receive_and_burn`. It burns the fungible asset from the active note, decreasing the faucet's token supply. +Burns the fungible asset from the active note, decreasing the faucet's token supply. | | | |-|-| @@ -525,9 +529,20 @@ This is a re-export of `miden::standards::faucets::fungible::receive_and_burn`. #### Faucet Account Storage +The faucet contributes the full standard `FungibleFaucet` slot set (25 value slots): + | Slot name | Slot type | Value encoding | Purpose | |-----------|-----------|----------------|---------| -| Faucet metadata (standard) | Value | `[token_supply, max_supply, decimals, token_symbol]` | Standard `NetworkFungibleFaucet` metadata | +| `token_config` | Value | `[token_supply, max_supply, decimals, token_symbol]` | Standard fungible faucet token configuration | +| `token_name_0` .. `token_name_1` | Value | Fixed-width string chunks | Token name, up to 32 UTF-8 bytes. Part of the AggLayer metadata hash preimage | +| `mutability_config` | Value | Boolean flags | Whether description / logo URI / external link / max supply may be updated. All `false` for AggLayer faucets | +| `token_description_0` .. `_6` | Value | Fixed-width string chunks | Optional description; empty for AggLayer faucets | +| `logo_uri_0` .. `_6` | Value | Fixed-width string chunks | Optional logo URI; empty for AggLayer faucets | +| `external_link_0` .. `_6` | Value | Fixed-width string chunks | Optional external link; empty for AggLayer faucets | + +Conversion metadata (origin token address, origin network, scale, metadata hash) is **not** stored +here. It lives on the bridge in `faucet_metadata_map`, written at registration time (see +[Section 7](#7-faucet-registry)). **Companion component storage slots:** @@ -1420,9 +1435,20 @@ be referenced by a `B2AGG` (bridge-out) or `CLAIM` (bridge-in) note. Registratio same flow for both kinds; the `is_native` flag in the `CONFIG_AGG_BRIDGE` note storage tells the bridge which dispatch path to take for each future bridge operation against that faucet. -The `AggLayerFaucet` Rust struct (`src/faucet.rs`) holds only -token metadata — symbol, decimals, max supply, and token supply -(TODO Missing token name ([#2585](https://github.com/0xMiden/protocol/issues/2585))). +The faucet holds only token metadata — name, symbol, decimals, max supply, and token supply. +Because the name is stored alongside the symbol and decimals, the full metadata hash preimage +`abi.encode(name, symbol, decimals)` is recoverable from faucet storage, which is what makes +registration-time verification possible. + +The preimage is the *origin* token's `(name, symbol, decimals)`, and Miden's types cannot always +hold those values: `FungibleFaucet::MAX_DECIMALS` is 12, so an 18-decimal ERC-20 cannot be +mirrored; `TokenName` caps names at 32 bytes; and `TokenSymbol` admits only 1-12 uppercase ASCII +characters, which excludes symbols such as `USDC.e`, `wstETH` or `1INCH`. A wrapped faucet +therefore may not be able to reproduce its registered hash at all, in which case that hash is the +one carried in the origin chain's leaf +(TODO Support metadata hash verification for wrapped faucets +([#2586](https://github.com/0xMiden/protocol/issues/2586))). + Conversion metadata (origin address, origin network, scale, and metadata hash) is *not* stored on the faucet; it is carried by the `CONFIG_AGG_BRIDGE` note at registration time and written directly into the bridge's `faucet_metadata_map`. The metadata hash is diff --git a/crates/miden-agglayer/asm/agglayer/faucet/mod.masm b/crates/miden-agglayer/asm/agglayer/faucet/mod.masm deleted file mode 100644 index 1c35106856..0000000000 --- a/crates/miden-agglayer/asm/agglayer/faucet/mod.masm +++ /dev/null @@ -1,6 +0,0 @@ -# PUBLIC INTERFACE -# ================================================================================================= - -pub use {receive_and_burn} from miden::standards::faucets::fungible -pub use {mint_and_send} from miden::standards::faucets::fungible -pub use {has_procedure} from miden::standards::inspection::code_inspection diff --git a/crates/miden-agglayer/asm/agglayer/mod.masm b/crates/miden-agglayer/asm/agglayer/mod.masm index f0bafa9be4..837495b869 100644 --- a/crates/miden-agglayer/asm/agglayer/mod.masm +++ b/crates/miden-agglayer/asm/agglayer/mod.masm @@ -1,3 +1,2 @@ pub mod bridge -pub mod faucet pub mod notes diff --git a/crates/miden-agglayer/asm/components/faucet/faucet.masm b/crates/miden-agglayer/asm/components/faucet/faucet.masm deleted file mode 100644 index aa53edd522..0000000000 --- a/crates/miden-agglayer/asm/components/faucet/faucet.masm +++ /dev/null @@ -1,14 +0,0 @@ -# The MASM code of the AggLayer Faucet Account Component. -# -# This is a thin wrapper that re-exports faucet-related procedures from the agglayer library. -# -# The faucet exposes: -# - `mint_and_send` from the fungible faucet (for MINT note consumption, with owner verification -# gated by the active mint policy) -# - `receive_and_burn` for bridge-out -# - `has_procedure` from the CodeInspection component, so the unified MINT/BURN note scripts can -# reflectively detect the faucet kind. - -pub use {mint_and_send} from agglayer::faucet -pub use {receive_and_burn} from agglayer::faucet -pub use {has_procedure} from agglayer::faucet diff --git a/crates/miden-agglayer/asm/components/faucet/miden-project.toml b/crates/miden-agglayer/asm/components/faucet/miden-project.toml deleted file mode 100644 index c4c2bec004..0000000000 --- a/crates/miden-agglayer/asm/components/faucet/miden-project.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "miden-agglayer-faucet" -version.workspace = true - -[lib] -kind = "account-component" -namespace = "agglayer::components::faucet" -path = "faucet.masm" - -[dependencies] -miden-agglayer.workspace = true -miden-core.workspace = true -miden-protocol.workspace = true -miden-standards.workspace = true diff --git a/crates/miden-agglayer/asm/components/miden-project.toml b/crates/miden-agglayer/asm/components/miden-project.toml index 393e2f8a34..68226a3ab4 100644 --- a/crates/miden-agglayer/asm/components/miden-project.toml +++ b/crates/miden-agglayer/asm/components/miden-project.toml @@ -1,5 +1,5 @@ [workspace] -members = ["bridge", "faucet"] +members = ["bridge"] [workspace.package] version = "0.16.0" diff --git a/crates/miden-agglayer/build.rs b/crates/miden-agglayer/build.rs index 95913e00bb..ea7f040253 100644 --- a/crates/miden-agglayer/build.rs +++ b/crates/miden-agglayer/build.rs @@ -32,12 +32,6 @@ use miden_standards::account::fees::{ ConstantFeeManager, FeePolicyManager, }; -use miden_standards::account::policies::{ - BurnPolicy, - MintPolicy, - TokenPolicyManager, - TransferPolicy, -}; // CONSTANTS // ================================================================================================ @@ -152,7 +146,6 @@ fn compile_agglayer_package( /// /// This file contains: /// - AggLayer Bridge code commitment. -/// - AggLayer Faucet code commitment. fn generate_agglayer_constants( target_file: impl AsRef, component_packages: Vec>, @@ -177,9 +170,9 @@ fn generate_agglayer_constants( // code commitment, so it doesn't matter what does this metadata holds. let dummy_metadata = AccountComponentMetadata::new("dummy"); - // iterate over the AggLayer Bridge and AggLayer Faucet packages + // iterate over the AggLayer component packages for package in component_packages { - // Derive the short component name (e.g. "bridge" / "faucet") from the package name + // Derive the short component name (e.g. "bridge") from the package name // (e.g. "miden-agglayer-bridge"). let component_name = package .name @@ -217,24 +210,6 @@ fn generate_agglayer_constants( components.push(AccountComponent::from(PausableManager)); components .push(AccountComponent::from(ConstantFeeManager::for_basic_constant_fee_policy())); - } else if component_name == "faucet" { - components.push(AccountComponent::from( - miden_standards::account::access::Ownable2Step::new(dummy_account_id), - )); - components.extend(AccessControl::Rbac { - admin: dummy_account_id, - procedure_roles: std::collections::BTreeMap::new(), - }); - let token_policy_manager = TokenPolicyManager::builder() - .active_mint_policy(MintPolicy::owner_only()) - .active_burn_policy(BurnPolicy::owner_only()) - .active_send_policy(TransferPolicy::allow_all()) - .active_receive_policy(TransferPolicy::allow_all()) - .build(); - - components.extend(token_policy_manager); - components - .push(AccountComponent::from(ConstantFeeManager::for_basic_constant_fee_policy())); } // use `AccountCode` to merge codes of agglayer and authentication components diff --git a/crates/miden-agglayer/src/faucet.rs b/crates/miden-agglayer/src/faucet.rs index 9bc6ac3cd1..f8127d54b9 100644 --- a/crates/miden-agglayer/src/faucet.rs +++ b/crates/miden-agglayer/src/faucet.rs @@ -1,59 +1,13 @@ extern crate alloc; use alloc::collections::{BTreeMap, BTreeSet}; -use alloc::string::ToString; -use alloc::vec; -use alloc::vec::Vec; -use miden_core::{Felt, Word}; -use miden_protocol::account::component::AccountComponentMetadata; -use miden_protocol::account::{ - Account, - AccountComponent, - AccountId, - AccountProcedureRoot, - RoleSymbol, - StorageSlot, - StorageSlotName, -}; -use miden_protocol::asset::{AssetAmount, AssetCallbacks, TokenSymbol}; -use miden_protocol::errors::AccountIdError; +use miden_protocol::account::{AccountProcedureRoot, RoleSymbol}; use miden_protocol::note::NoteScriptRoot; -use miden_standards::account::access::{Authority, Ownable2Step}; use miden_standards::account::auth::AuthNetworkAccount; -use miden_standards::account::faucets::{FungibleFaucet, FungibleFaucetError, TokenName}; use miden_standards::account::fees::ConstantFeeManager; -use miden_standards::account::policies::TokenPolicyManager; -pub use miden_standards::interop::eth::{ - EthAddress, - EthAmount, - EthAmountError, - EthEmbeddedAccountId, -}; use miden_standards::note::{BurnNote, ConstantFeePolicyConfigNote, MintNote, RbacConfigNote}; use miden_utils_sync::LazyLock; -use thiserror::Error; - -use super::agglayer_faucet_component_package; -pub use crate::{ - AggLayerBridge, - B2AggNote, - ClaimNoteStorage, - ConfigAggBridgeNote, - ExitRoot, - GlobalIndex, - GlobalIndexError, - LeafData, - MetadataHash, - ProofData, - SmtNode, - UpdateGerNote, -}; - -// CONSTANTS -// ================================================================================================ -// Include the generated agglayer constants -include!(concat!(env!("OUT_DIR"), "/agglayer_constants.rs")); // FAUCET RBAC ROLES // ================================================================================================ @@ -61,95 +15,19 @@ include!(concat!(env!("OUT_DIR"), "/agglayer_constants.rs")); static FEE_MANAGER_ROLE: LazyLock = LazyLock::new(|| RoleSymbol::new("FEE_MNGR").expect("FEE_MNGR role symbol should be valid")); -// AGGLAYER FAUCET STRUCT +// AGGLAYER FAUCET // ================================================================================================ -/// An [`AccountComponent`] implementing the AggLayer Faucet. -/// -/// It re-exports `mint_and_send` and `receive_and_burn` from the agglayer faucet package. -/// Conversion metadata (origin address, origin network, scale, metadata hash) is held by the -/// bridge, not the faucet — see -/// [`AggLayerBridge`] and the `faucet_metadata_map` populated on registration. -/// -/// ## Storage Layout +/// The deployment configuration of an AggLayer faucet. /// -/// - All [`FungibleFaucet`] storage slots (token config + name + mutability + description + logo -/// URI + external link). Conversion metadata is no longer stored on the faucet; the bridge holds -/// it in `faucet_metadata_map`. -/// -/// ## Required Companion Components -/// -/// This component re-exports `fungible::mint_and_send`, which requires: -/// - [`Ownable2Step`]: Provides ownership data (bridge account ID as owner). -/// - [`miden_standards::account::policies::TokenPolicyManager`]: Provides mint and burn policy -/// management. -/// -/// These must be added as separate components when building the faucet account. -#[derive(Debug, Clone)] -pub struct AggLayerFaucet { - faucet: FungibleFaucet, -} +/// An AggLayer faucet is a [`FungibleFaucet`](miden_standards::account::faucets::FungibleFaucet) +/// owned by the bridge. This type is a stateless namespace for the settings the bridge and the +/// faucet operator agree on when one is deployed: the note allowlist, the RBAC roles, and the +/// account builder. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AggLayerFaucet; impl AggLayerFaucet { - // CONSTRUCTORS - // -------------------------------------------------------------------------------------------- - - /// Creates a new AggLayer faucet component from the given configuration. - /// - /// The faucet's display name is derived from the symbol (an AggLayer faucet is identified by - /// its symbol; the human-readable name is not used in the bridge protocol). - /// - /// # Errors - /// Returns an error if: - /// - The decimals parameter exceeds maximum value of [`FungibleFaucet::MAX_DECIMALS`]. - /// - The max supply exceeds maximum possible amount for a fungible asset. - /// - The token supply exceeds the max supply. - pub fn new( - symbol: TokenSymbol, - decimals: u8, - max_supply: Felt, - token_supply: Felt, - ) -> Result { - // Use the symbol as the display name; AggLayer faucets do not use a separate token name. - let name = TokenName::new(symbol.to_string().as_str()) - .expect("symbol fits within token name capacity"); - let max_supply_amount = AssetAmount::try_from(max_supply).map_err(|_| { - FungibleFaucetError::MaxSupplyTooLarge { - actual: max_supply.as_canonical_u64(), - max: AssetAmount::MAX.as_u64(), - } - })?; - let token_supply_amount = AssetAmount::try_from(token_supply).map_err(|_| { - FungibleFaucetError::MaxSupplyTooLarge { - actual: token_supply.as_canonical_u64(), - max: AssetAmount::MAX.as_u64(), - } - })?; - let faucet = FungibleFaucet::builder() - .name(name) - .symbol(symbol) - .decimals(decimals) - .max_supply(max_supply_amount) - .token_supply(token_supply_amount) - .build()?; - Ok(Self { faucet }) - } - - /// Sets the token supply for an existing faucet (e.g. for testing scenarios). - /// - /// # Errors - /// Returns an error if the token supply exceeds the max supply. - pub fn with_token_supply(mut self, token_supply: Felt) -> Result { - let token_supply_amount = AssetAmount::try_from(token_supply).map_err(|_| { - FungibleFaucetError::MaxSupplyTooLarge { - actual: token_supply.as_canonical_u64(), - max: AssetAmount::MAX.as_u64(), - } - })?; - self.faucet = self.faucet.with_token_supply(token_supply_amount)?; - Ok(self) - } - // RBAC ROLES // -------------------------------------------------------------------------------------------- @@ -164,21 +42,6 @@ impl AggLayerFaucet { BTreeMap::from([(ConstantFeeManager::set_note_fee_root(), Self::fee_manager_role())]) } - // PUBLIC ACCESSORS - // -------------------------------------------------------------------------------------------- - - /// Storage slot name for the token config word - /// `[token_supply, max_supply, decimals, token_symbol]`. - pub fn token_config_slot() -> &'static StorageSlotName { - FungibleFaucet::token_config_slot() - } - - /// Storage slot name for the owner account ID (bridge), provided by the - /// [`Ownable2Step`] companion component. - pub fn owner_config_slot() -> &'static StorageSlotName { - Ownable2Step::slot_name() - } - // ALLOWED NOTES // -------------------------------------------------------------------------------------------- @@ -196,166 +59,4 @@ impl AggLayerFaucet { notes.extend(AuthNetworkAccount::default_allowed_note_scripts()); notes } - - /// Extracts the underlying [`FungibleFaucet`] component (which holds the token metadata) - /// from the storage slots of the provided account. - /// - /// # Errors - /// - /// Returns an error if: - /// - the provided account is not an [`AggLayerFaucet`] account. - pub fn try_faucet_from_account( - faucet_account: &Account, - ) -> Result { - // check that the provided account is a faucet account - Self::assert_faucet_account(faucet_account)?; - - FungibleFaucet::try_from(faucet_account.storage()) - .map_err(AgglayerFaucetError::FungibleFaucetError) - } - - /// Extracts the bridge account ID from the [`Ownable2Step`] owner config storage slot - /// of the provided account. - /// - /// # Errors - /// - /// Returns an error if: - /// - the provided account is not an [`AggLayerFaucet`] account. - pub fn owner_account_id(faucet_account: &Account) -> Result { - // check that the provided account is a faucet account - Self::assert_faucet_account(faucet_account)?; - - let ownership = Ownable2Step::try_from_storage(faucet_account.storage()) - .map_err(AgglayerFaucetError::Ownable2StepError)?; - ownership.owner().ok_or(AgglayerFaucetError::OwnershipRenounced) - } - - // HELPER FUNCTIONS - // -------------------------------------------------------------------------------------------- - - /// Checks that the provided account is an [`AggLayerFaucet`] account. - /// - /// # Errors - /// - /// Returns an error if: - /// - the provided account does not have all AggLayer Faucet specific storage slots. - /// - the provided account does not have all AggLayer Faucet specific procedures. - fn assert_faucet_account(account: &Account) -> Result<(), AgglayerFaucetError> { - // check that the storage slots are as expected - Self::assert_storage_slots(account)?; - - // check that the procedure roots are as expected - Self::assert_code_commitment(account)?; - - Ok(()) - } - - /// Checks that the provided account has all storage slots required for the [`AggLayerFaucet`]. - /// - /// # Errors - /// - /// Returns an error if: - /// - provided account does not have all AggLayer Faucet specific storage slots). - fn assert_storage_slots(account: &Account) -> Result<(), AgglayerFaucetError> { - // get the storage slot names of the provided account - let account_storage_slot_names: Vec<&StorageSlotName> = account - .storage() - .slots() - .iter() - .map(|storage_slot| storage_slot.name()) - .collect::>(); - - // check that all bridge specific storage slots are presented in the provided account - let are_slots_present = Self::slot_names() - .iter() - .all(|slot_name| account_storage_slot_names.contains(slot_name)); - if !are_slots_present { - return Err(AgglayerFaucetError::StorageSlotsMismatch); - } - - Ok(()) - } - - /// Checks that the code commitment of the provided account matches the code commitment of the - /// [`AggLayerFaucet`]. - /// - /// # Errors - /// - /// Returns an error if: - /// - the code commitment of the provided account does not match the code commitment of the - /// [`AggLayerFaucet`]. - fn assert_code_commitment(account: &Account) -> Result<(), AgglayerFaucetError> { - if FAUCET_CODE_COMMITMENT != account.code().commitment() { - return Err(AgglayerFaucetError::CodeCommitmentMismatch); - } - - Ok(()) - } - - /// Returns a vector of all [`AggLayerFaucet`] storage slot names. - fn slot_names() -> Vec<&'static StorageSlotName> { - let mut slot_names = vec![ - FungibleFaucet::token_config_slot(), - Ownable2Step::slot_name(), - Authority::authority_slot(), - TokenPolicyManager::active_mint_policy_slot(), - TokenPolicyManager::active_burn_policy_slot(), - TokenPolicyManager::allowed_mint_policies_slot(), - TokenPolicyManager::allowed_burn_policies_slot(), - TokenPolicyManager::allowed_send_policies_slot(), - TokenPolicyManager::allowed_receive_policies_slot(), - ]; - - // The faucet registers send and receive transfer policies, so its policy manager installs - // the protocol-reserved asset callback slots. Their presence is what makes the account ID - // carry an enabled asset callback flag, so requiring them certifies that the faucet's - // transfer policies can be invoked at all. - slot_names.extend(AssetCallbacks::slot_names()); - - slot_names - } -} - -impl From for AccountComponent { - fn from(agglayer_faucet: AggLayerFaucet) -> Self { - // Bring in all of the FungibleFaucet's storage slots (token config + name + - // mutability + description + logo URI + external link). - agglayer_faucet_component(agglayer_faucet.faucet.into_storage_slots()) - } -} - -// AGGLAYER FAUCET ERROR -// ================================================================================================ - -/// AggLayer Faucet related errors. -#[derive(Debug, Error)] -pub enum AgglayerFaucetError { - #[error( - "provided account does not have storage slots required for the AggLayer Faucet account" - )] - StorageSlotsMismatch, - #[error("provided account does not have procedures required for the AggLayer Faucet account")] - CodeCommitmentMismatch, - #[error("fungible faucet error")] - FungibleFaucetError(#[source] FungibleFaucetError), - #[error("account ID error")] - AccountIdError(#[source] AccountIdError), - #[error("ownable2step error")] - Ownable2StepError(#[source] miden_standards::account::access::Ownable2StepError), - #[error("faucet ownership has been renounced")] - OwnershipRenounced, -} - -// HELPER FUNCTIONS -// ================================================================================================ - -/// Creates an Agglayer Faucet component with the specified storage slots. -fn agglayer_faucet_component(storage_slots: Vec) -> AccountComponent { - let package = agglayer_faucet_component_package(); - let metadata = AccountComponentMetadata::new("agglayer::faucet") - .with_description("AggLayer faucet component"); - - AccountComponent::new(package, storage_slots, metadata).expect( - "agglayer_faucet component should satisfy the requirements of a valid account component", - ) } diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index 44306716c4..033e31d9ee 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -2,10 +2,10 @@ extern crate alloc; -use miden_core::{Felt, Word}; -use miden_protocol::account::{AccountBuilder, AccountComponent, AccountId}; +use miden_core::Word; +use miden_protocol::account::{AccountBuilder, AccountId}; use miden_protocol::assembly::Path; -use miden_protocol::asset::TokenSymbol; +use miden_protocol::asset::{AssetAmount, TokenSymbol}; use miden_protocol::note::NoteScript; use miden_protocol::vm::Package; use miden_standards::account::access::{ @@ -17,6 +17,7 @@ use miden_standards::account::access::{ RoleConfig, }; use miden_standards::account::auth::NetworkAccount; +use miden_standards::account::faucets::{FungibleFaucet, TokenName}; use miden_standards::account::fees::{ BasicConstantFeePolicy, ConstantFeeManager, @@ -65,7 +66,7 @@ pub use deregister_note::DeregisterAggFaucetNote; #[cfg(any(test, feature = "testing"))] pub use eth_types::GlobalIndexExt; pub use eth_types::{GlobalIndex, GlobalIndexError, MetadataHash}; -pub use faucet::{AggLayerFaucet, AgglayerFaucetError}; +pub use faucet::AggLayerFaucet; pub use remove_ger_note::RemoveGerNote; pub use update_ger_note::UpdateGerNote; pub use utils::Keccak256Output; @@ -85,13 +86,6 @@ static BRIDGE_COMPONENT_PACKAGE: LazyLock = LazyLock::new(|| { .expect("shipped bridge component package is well-formed") }); -static FAUCET_COMPONENT_PACKAGE: LazyLock = LazyLock::new(|| { - let bytes = - include_bytes!(concat!(env!("OUT_DIR"), "/assets/components/miden-agglayer-faucet.masp")); - Package::read_from_bytes_trusted(bytes) - .expect("shipped faucet component package is well-formed") -}); - /// Returns the AggLayer package containing all agglayer modules, including the note scripts. /// /// The note scripts this crate builds are external references into this package rather than @@ -120,42 +114,9 @@ fn agglayer_bridge_component_package() -> Package { BRIDGE_COMPONENT_PACKAGE.clone() } -/// Returns the Faucet component package. -fn agglayer_faucet_component_package() -> Package { - FAUCET_COMPONENT_PACKAGE.clone() -} - // AGGLAYER ACCOUNT CREATION HELPERS // ================================================================================================ -/// Creates an agglayer faucet account component with the specified configuration. -/// -/// The faucet holds only token metadata; conversion metadata (origin address, origin network, -/// scale, metadata hash) lives on the bridge and is populated at registration time. -/// -/// # Parameters -/// - `token_symbol`: The symbol for the fungible token (e.g., "AGG") -/// - `decimals`: Number of decimal places for the token -/// - `max_supply`: Maximum supply of the token -/// - `initial_supply`: Initial outstanding token supply (0 for new faucets) -/// -/// # Returns -/// Returns an [`AccountComponent`] configured for agglayer faucet operations. -/// -/// # Panics -/// Panics if the token symbol is invalid or metadata validation fails. -fn create_agglayer_faucet_component( - token_symbol: &str, - decimals: u8, - max_supply: Felt, - initial_supply: Felt, -) -> AccountComponent { - let symbol = TokenSymbol::new(token_symbol).expect("token symbol should be valid"); - AggLayerFaucet::new(symbol, decimals, max_supply, initial_supply) - .expect("agglayer faucet metadata should be valid") - .into() -} - impl AggLayerBridge { /// Returns an [`AccountBuilder`] for a bridge account with the standard configuration. /// @@ -200,21 +161,30 @@ impl AggLayerFaucet { /// Returns an [`AccountBuilder`] for a faucet account with the specified deployment /// configuration. /// - /// `faucet_admin` is the initial member of the faucet's built-in `ADMIN` role; - /// `fee_manager` is the initial member of its `FEE_MNGR` role; `bridge_account_id` is its - /// [`Ownable2Step`] owner. `fee_policy` must contain entries for - /// [`AggLayerFaucet::allowed_notes`], denominated in the asset issued by `fee_faucet_id`. + /// The account is a standard [`FungibleFaucet`]: `mint_and_send` and `receive_and_burn` drive + /// bridge-in and bridge-out, and the standard metadata getters expose the token name, symbol + /// and decimals. Conversion metadata (origin address, origin network, scale, metadata hash) + /// lives on the bridge and is written there at registration time. + /// + /// `faucet_admin` is the initial member of the faucet's built-in `ADMIN` role; `fee_manager` + /// is the initial member of its `FEE_MNGR` role; `bridge_account_id` is its [`Ownable2Step`] + /// owner, which is what the `owner_only` mint and burn policies gate on. `fee_policy` must + /// contain entries for [`AggLayerFaucet::allowed_notes`], denominated in the asset issued by + /// `fee_faucet_id`. /// /// # Panics /// - /// Panics if the token metadata is invalid. + /// Panics if: + /// - `decimals` exceeds [`FungibleFaucet::MAX_DECIMALS`]; + /// - `initial_supply` exceeds `max_supply`. #[allow(clippy::too_many_arguments)] pub fn account_builder( seed: Word, - token_symbol: &str, + token_name: TokenName, + token_symbol: TokenSymbol, decimals: u8, - max_supply: Felt, - initial_supply: Felt, + max_supply: AssetAmount, + initial_supply: AssetAmount, faucet_admin: AccountId, fee_manager: AccountId, bridge_account_id: AccountId, @@ -225,8 +195,14 @@ impl AggLayerFaucet { .fee_faucet_id(fee_faucet_id) .active_fee_policy(fee_policy.into()) .build(); - let agglayer_component = - create_agglayer_faucet_component(token_symbol, decimals, max_supply, initial_supply); + let faucet = FungibleFaucet::builder() + .name(token_name) + .symbol(token_symbol) + .decimals(decimals) + .max_supply(max_supply) + .token_supply(initial_supply) + .build() + .expect("agglayer faucet decimals and supplies should be within their valid ranges"); let token_policy_manager = TokenPolicyManager::builder() .active_mint_policy(MintPolicy::owner_only()) @@ -243,7 +219,7 @@ impl AggLayerFaucet { NetworkAccount::builder(seed.into(), AggLayerFaucet::allowed_notes(), fee_policy_manager) .expect("faucet note allowlist is non-empty") - .with_component(agglayer_component) + .with_component(faucet) .with_component(Ownable2Step::new(bridge_account_id)) .with_component(rbac) .with_component(Authority::RbacControlled { @@ -259,6 +235,7 @@ impl AggLayerFaucet { #[cfg(test)] mod tests { + use miden_core::Felt; use miden_protocol::account::AssetCallbackFlag; use miden_protocol::asset::AssetCallbacks; use miden_protocol::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE; @@ -279,6 +256,7 @@ mod tests { let faucet = create_existing_agglayer_faucet( Word::default(), + "AggLayer Token", "AGG", 6, Felt::from(1000u32), @@ -306,6 +284,7 @@ mod tests { create_existing_bridge_account_with_roles(Word::default(), id, id, id, id, id, id, 77); let faucet = create_existing_agglayer_faucet( Word::default(), + "AggLayer Token", "AGG", 6, Felt::from(1000u32), diff --git a/crates/miden-agglayer/src/testing/mod.rs b/crates/miden-agglayer/src/testing/mod.rs index dbd38d7c1a..46a7cb46e1 100644 --- a/crates/miden-agglayer/src/testing/mod.rs +++ b/crates/miden-agglayer/src/testing/mod.rs @@ -7,7 +7,7 @@ use alloc::string::{String, ToString}; use alloc::vec::Vec; use miden_protocol::account::{Account, AccountId}; -use miden_protocol::asset::AssetAmount; +use miden_protocol::asset::{AssetAmount, TokenSymbol}; use miden_protocol::note::NoteScriptRoot; use miden_protocol::testing::account_id::{ ACCOUNT_ID_FEE_FAUCET, @@ -16,6 +16,7 @@ use miden_protocol::testing::account_id::{ use miden_protocol::utils::hex_to_bytes; use miden_protocol::utils::sync::LazyLock; use miden_protocol::{Felt, Word}; +use miden_standards::account::faucets::TokenName; use miden_standards::account::fees::{BasicConstantFeePolicy, FeePolicyManager}; use miden_standards::interop::eth::{EthAddress, EthAmount}; use serde::Deserialize; @@ -97,6 +98,7 @@ pub fn create_existing_bridge_account_with_roles( /// Creates an existing AggLayer faucet account with a zero-fee policy. pub fn create_existing_agglayer_faucet( seed: Word, + token_name: &str, token_symbol: &str, decimals: u8, max_supply: Felt, @@ -107,10 +109,11 @@ pub fn create_existing_agglayer_faucet( let faucet_admin = bridge_admin_account_id(); AggLayerFaucet::account_builder( seed, - token_symbol, + TokenName::new(token_name).expect("token name should be valid"), + TokenSymbol::new(token_symbol).expect("token symbol should be valid"), decimals, - max_supply, - initial_supply, + AssetAmount::try_from(max_supply).expect("max supply should be valid"), + AssetAmount::try_from(initial_supply).expect("initial supply should be valid"), faucet_admin, fee_manager, bridge_account_id, diff --git a/crates/miden-testing/tests/agglayer/bridge_in.rs b/crates/miden-testing/tests/agglayer/bridge_in.rs index 064085c7bd..42de2329b1 100644 --- a/crates/miden-testing/tests/agglayer/bridge_in.rs +++ b/crates/miden-testing/tests/agglayer/bridge_in.rs @@ -180,9 +180,10 @@ async fn test_bridge_in_claim_to_p2id( // -------------------------------------------------------------------------------------------- let (proof_data, leaf_data, ger, cgi_chain_hash) = data_source.get_data(); - // CREATE AGGLAYER FAUCET ACCOUNT (with agglayer_faucet component) + // CREATE AGGLAYER FAUCET ACCOUNT // Use the origin token address and network from the claim data. // -------------------------------------------------------------------------------------------- + let token_name = "AggLayer Token"; let token_symbol = "AGG"; let decimals = 8u8; let max_supply: Felt = FungibleAsset::MAX_AMOUNT.into(); @@ -194,6 +195,7 @@ async fn test_bridge_in_claim_to_p2id( let agglayer_faucet = priced_faucet_builder( agglayer_faucet_seed, + token_name, token_symbol, decimals, max_supply, @@ -517,7 +519,9 @@ async fn test_mint_cannot_be_consumed_by_unrelated_faucet() -> anyhow::Result<() let (proof_data, leaf_data, ger, _cgi_chain_hash) = data_source.get_data(); + let token_name_a = "AggLayer Token A"; let token_symbol_a = "AGGA"; + let token_name_b = "AggLayer Token B"; let token_symbol_b = "AGGB"; let decimals = 8u8; let max_supply: Felt = FungibleAsset::MAX_AMOUNT.into(); @@ -527,6 +531,7 @@ async fn test_mint_cannot_be_consumed_by_unrelated_faucet() -> anyhow::Result<() let faucet_a_seed = builder.rng_mut().draw_word(); let faucet_a = create_existing_agglayer_faucet( faucet_a_seed, + token_name_a, token_symbol_a, decimals, max_supply, @@ -545,6 +550,7 @@ async fn test_mint_cannot_be_consumed_by_unrelated_faucet() -> anyhow::Result<() let other_token_address = EthAddress::new(other_bytes); let faucet_b = create_existing_agglayer_faucet( faucet_b_seed, + token_name_b, token_symbol_b, decimals, max_supply, @@ -732,9 +738,10 @@ async fn test_claim_rejects_wrong_destination_network() -> anyhow::Result<()> { // -------------------------------------------------------------------------------------------- leaf_data.destination_network = MIDEN_NETWORK_ID.saturating_add(1); - // CREATE AGGLAYER FAUCET ACCOUNT (with agglayer_faucet component) + // CREATE AGGLAYER FAUCET ACCOUNT // Use the origin token address and network from the claim data. // -------------------------------------------------------------------------------------------- + let token_name = "AggLayer Token"; let token_symbol = "AGG"; let decimals = 8u8; let max_supply: Felt = FungibleAsset::MAX_AMOUNT.into(); @@ -746,6 +753,7 @@ async fn test_claim_rejects_wrong_destination_network() -> anyhow::Result<()> { let metadata_hash = leaf_data.metadata_hash; let agglayer_faucet = create_existing_agglayer_faucet( agglayer_faucet_seed, + token_name, token_symbol, decimals, max_supply, @@ -881,6 +889,7 @@ async fn test_duplicate_claim_note_rejected() -> anyhow::Result<()> { let (proof_data, leaf_data, ger, _cgi_chain_hash) = data_source.get_data(); // CREATE AGGLAYER FAUCET ACCOUNT + let token_name = "AggLayer Token"; let token_symbol = "AGG"; let decimals = 8u8; let max_supply: Felt = FungibleAsset::MAX_AMOUNT.into(); @@ -892,6 +901,7 @@ async fn test_duplicate_claim_note_rejected() -> anyhow::Result<()> { let agglayer_faucet = create_existing_agglayer_faucet( agglayer_faucet_seed, + token_name, token_symbol, decimals, max_supply, @@ -1051,6 +1061,7 @@ async fn test_claim_rejects_removed_ger() -> anyhow::Result<()> { let (proof_data, leaf_data, ger, _cgi_chain_hash) = data_source.get_data(); // CREATE AGGLAYER FAUCET ACCOUNT + let token_name = "AggLayer Token"; let token_symbol = "AGG"; let decimals = 8u8; let max_supply: Felt = FungibleAsset::MAX_AMOUNT.into(); @@ -1062,6 +1073,7 @@ async fn test_claim_rejects_removed_ger() -> anyhow::Result<()> { let agglayer_faucet = create_existing_agglayer_faucet( agglayer_faucet_seed, + token_name, token_symbol, decimals, max_supply, @@ -1754,6 +1766,7 @@ async fn test_claim_fails_when_origin_network_unregistered() -> anyhow::Result<( let (proof_data, leaf_data, ger, _cgi_chain_hash) = data_source.get_data(); + let token_name = "AggLayer Token"; let token_symbol = "AGG"; let decimals = 8u8; let max_supply: Felt = FungibleAsset::MAX_AMOUNT.into(); @@ -1773,6 +1786,7 @@ async fn test_claim_fails_when_origin_network_unregistered() -> anyhow::Result<( let agglayer_faucet = create_existing_agglayer_faucet( agglayer_faucet_seed, + token_name, token_symbol, decimals, max_supply, @@ -1902,6 +1916,7 @@ async fn test_reregister_clears_prior_token_key() -> anyhow::Result<()> { let (proof_data, leaf_data, ger, _cgi_chain_hash) = data_source.get_data(); + let token_name = "AggLayer Token"; let token_symbol = "AGG"; let decimals = 8u8; let max_supply: Felt = FungibleAsset::MAX_AMOUNT.into(); @@ -1917,6 +1932,7 @@ async fn test_reregister_clears_prior_token_key() -> anyhow::Result<()> { let agglayer_faucet = create_existing_agglayer_faucet( agglayer_faucet_seed, + token_name, token_symbol, decimals, max_supply, diff --git a/crates/miden-testing/tests/agglayer/bridge_out.rs b/crates/miden-testing/tests/agglayer/bridge_out.rs index 442274c1d9..f1cc1ea4a9 100644 --- a/crates/miden-testing/tests/agglayer/bridge_out.rs +++ b/crates/miden-testing/tests/agglayer/bridge_out.rs @@ -141,14 +141,10 @@ async fn bridge_out_consecutive( .expect("valid shared origin token address"); let origin_network = 64u32; let scale = 0u8; - let metadata_hash = MetadataHash::from_token_info( - &vectors.token_name, - &vectors.token_symbol, - vectors.token_decimals, - ); let faucet_seed = builder.rng_mut().draw_word(); let faucet = priced_faucet_builder( faucet_seed, + &vectors.token_name, &vectors.token_symbol, vectors.token_decimals, FungibleAsset::MAX_AMOUNT.into(), @@ -157,6 +153,11 @@ async fn bridge_out_consecutive( verification_base_fee, )? .build_existing()?; + let metadata_hash = MetadataHash::from_token_info( + &vectors.token_name, + &vectors.token_symbol, + vectors.token_decimals, + ); builder.add_account(faucet.clone())?; // CONFIG_AGG_BRIDGE note to register the faucet in the bridge (sent by faucet manager) @@ -465,13 +466,9 @@ async fn bridge_out_at_high_num_leaves(#[case] initial_num_leaves: u32) -> anyho .expect("valid shared origin token address"); let origin_network = 64u32; let scale = 0u8; - let metadata_hash = MetadataHash::from_token_info( - &vectors.token_name, - &vectors.token_symbol, - vectors.token_decimals, - ); let faucet = create_existing_agglayer_faucet( builder.rng_mut().draw_word(), + &vectors.token_name, &vectors.token_symbol, vectors.token_decimals, Felt::from(FungibleAsset::MAX_AMOUNT), @@ -479,6 +476,11 @@ async fn bridge_out_at_high_num_leaves(#[case] initial_num_leaves: u32) -> anyho bridge_admin_account_id(), bridge_account.id(), ); + let metadata_hash = MetadataHash::from_token_info( + &vectors.token_name, + &vectors.token_symbol, + vectors.token_decimals, + ); builder.add_account(faucet.clone())?; let config_note = ConfigAggBridgeNote::create( @@ -602,6 +604,7 @@ async fn test_bridge_out_fails_with_unregistered_faucet() -> anyhow::Result<()> let vectors = &*SOLIDITY_MTF_VECTORS; let faucet = create_existing_agglayer_faucet( builder.rng_mut().draw_word(), + &vectors.token_name, &vectors.token_symbol, vectors.token_decimals, FungibleAsset::MAX_AMOUNT.into(), @@ -708,13 +711,9 @@ async fn test_bridge_out_rejects_invalid_b2agg_note( let origin_token_address = EthAddress::from_hex(&vectors.origin_token_address).expect("valid origin token address"); let origin_network = 64u32; - let metadata_hash = MetadataHash::from_token_info( - &vectors.token_name, - &vectors.token_symbol, - vectors.token_decimals, - ); let faucet = create_existing_agglayer_faucet( builder.rng_mut().draw_word(), + &vectors.token_name, &vectors.token_symbol, vectors.token_decimals, FungibleAsset::MAX_AMOUNT.into(), @@ -722,6 +721,11 @@ async fn test_bridge_out_rejects_invalid_b2agg_note( bridge_admin_account_id(), bridge_account.id(), ); + let metadata_hash = MetadataHash::from_token_info( + &vectors.token_name, + &vectors.token_symbol, + vectors.token_decimals, + ); builder.add_account(faucet.clone())?; // CREATE CONFIG_AGG_BRIDGE NOTE (registers faucet + token address in bridge) @@ -1121,7 +1125,12 @@ async fn bridge_out_lock_native_token() -> anyhow::Result<()> { .expect("valid eth address"); let origin_network = 7u32; // any stable u32 — Miden's test network id let scale = 0u8; - let metadata_hash = MetadataHash::from_token_info("Native Token", "NATIVE", 8); + let native_metadata = FungibleFaucet::try_from(&native_faucet)?; + let metadata_hash = MetadataHash::from_token_info( + native_metadata.token_name().as_str(), + &native_metadata.symbol().to_string(), + native_metadata.decimals(), + ); let config_note = ConfigAggBridgeNote::create( ConversionMetadata { diff --git a/crates/miden-testing/tests/agglayer/faucet_helpers.rs b/crates/miden-testing/tests/agglayer/faucet_helpers.rs index 0951c1ba1b..a3b01c35e1 100644 --- a/crates/miden-testing/tests/agglayer/faucet_helpers.rs +++ b/crates/miden-testing/tests/agglayer/faucet_helpers.rs @@ -1,11 +1,12 @@ extern crate alloc; -use miden_agglayer::AggLayerFaucet; use miden_agglayer::testing::create_existing_agglayer_faucet; use miden_protocol::Felt; use miden_protocol::account::auth::AuthScheme; -use miden_protocol::asset::FungibleAsset; +use miden_protocol::asset::{AssetAmount, FungibleAsset}; use miden_protocol::crypto::rand::FeltRng; +use miden_standards::account::access::Ownable2Step; +use miden_standards::account::faucets::FungibleFaucet; use miden_testing::{Auth, MockChain}; use super::test_utils::{ @@ -14,8 +15,9 @@ use super::test_utils::{ create_existing_bridge_account_with_roles, }; +/// An agglayer faucet is a standard [`FungibleFaucet`] owned by the bridge. #[test] -fn test_faucet_helper_methods() -> anyhow::Result<()> { +fn agglayer_faucet_is_a_bridge_owned_fungible_faucet() -> anyhow::Result<()> { let mut builder = MockChain::builder(); let faucet_manager = builder.add_existing_wallet(Auth::BasicAuth { @@ -40,6 +42,7 @@ fn test_faucet_helper_methods() -> anyhow::Result<()> { ); builder.add_account(bridge_account.clone())?; + let token_name = "AggLayer Token"; let token_symbol = "AGG"; let decimals = 8u8; let max_supply: Felt = FungibleAsset::MAX_AMOUNT.into(); @@ -47,6 +50,7 @@ fn test_faucet_helper_methods() -> anyhow::Result<()> { let faucet = create_existing_agglayer_faucet( builder.rng_mut().draw_word(), + token_name, token_symbol, decimals, max_supply, @@ -55,7 +59,22 @@ fn test_faucet_helper_methods() -> anyhow::Result<()> { bridge_account.id(), ); - assert_eq!(AggLayerFaucet::owner_account_id(&faucet)?, bridge_account.id()); + // The account carries the standard fungible faucet interface, so `try_from` (which checks the + // procedure roots before decoding storage) succeeds. + let metadata = FungibleFaucet::try_from(&faucet)?; + + // Every field round-trips, most importantly the token name: it makes the metadata hash + // preimage `abi.encode(name, symbol, decimals)` recoverable from faucet storage, which is what + // will let the bridge verify the registered hash on-chain (issue #2586). + assert_eq!(metadata.token_name().as_str(), token_name); + assert_eq!(metadata.symbol().to_string(), token_symbol); + assert_eq!(metadata.decimals(), decimals); + assert_eq!(metadata.max_supply(), AssetAmount::try_from(max_supply)?); + assert_eq!(metadata.token_supply(), AssetAmount::try_from(token_supply)?); + + // Mint and burn authorization is bound to the bridge through `Ownable2Step`. + let ownership = Ownable2Step::try_from_storage(faucet.storage())?; + assert_eq!(ownership.owner(), Some(bridge_account.id())); Ok(()) } diff --git a/crates/miden-testing/tests/agglayer/fee_policy.rs b/crates/miden-testing/tests/agglayer/fee_policy.rs index 061b72ada3..1034763fb6 100644 --- a/crates/miden-testing/tests/agglayer/fee_policy.rs +++ b/crates/miden-testing/tests/agglayer/fee_policy.rs @@ -3,11 +3,12 @@ use alloc::collections::BTreeSet; use miden_agglayer::testing::bridge_admin_account_id; use miden_agglayer::{AggLayerBridge, AggLayerFaucet, BridgeRoles}; use miden_protocol::account::{Account, AccountBuilder, AccountId, AccountType, StorageMapKey}; -use miden_protocol::asset::{AssetId, FungibleAsset}; +use miden_protocol::asset::{AssetAmount, AssetId, FungibleAsset, TokenSymbol}; use miden_protocol::note::{Note, NoteScriptRoot}; use miden_protocol::transaction::RawOutputNote; use miden_protocol::{Felt, Word}; use miden_standards::account::auth::{AuthNetworkAccount, NetworkAccount}; +use miden_standards::account::faucets::TokenName; use miden_standards::account::fees::{ BasicConstantFeePolicy, ConstantFeeManager, @@ -158,10 +159,11 @@ fn build_managed_account(managed: ManagedAccount) -> anyhow::Result { ManagedAccount::Bridge => bridge, ManagedAccount::Faucet => AggLayerFaucet::account_builder( Word::from([1u32, 0, 0, 0]), - "AGG", + TokenName::new("AggLayer Token")?, + TokenSymbol::new("AGG")?, 6, - 1_000u32.into(), - Felt::ZERO, + AssetAmount::new(1_000)?, + AssetAmount::ZERO, account_admin, fee_manager_id(), bridge.id(), diff --git a/crates/miden-testing/tests/agglayer/network_account_regression.rs b/crates/miden-testing/tests/agglayer/network_account_regression.rs index c230f617e6..347b88ce0f 100644 --- a/crates/miden-testing/tests/agglayer/network_account_regression.rs +++ b/crates/miden-testing/tests/agglayer/network_account_regression.rs @@ -165,6 +165,7 @@ async fn faucet_rejects_tx_script() -> anyhow::Result<()> { let faucet = create_existing_agglayer_faucet( builder.rng_mut().draw_word(), + "Test Token", "TEST", 8, Felt::new(1_000_000).unwrap(), @@ -207,6 +208,7 @@ async fn faucet_rejects_non_allowlisted_input_note() -> anyhow::Result<()> { let faucet = create_existing_agglayer_faucet( builder.rng_mut().draw_word(), + "Test Token", "TEST", 8, Felt::new(1_000_000).unwrap(), diff --git a/crates/miden-testing/tests/agglayer/rbac_rotation.rs b/crates/miden-testing/tests/agglayer/rbac_rotation.rs index 75a3da4707..c39eb4b370 100644 --- a/crates/miden-testing/tests/agglayer/rbac_rotation.rs +++ b/crates/miden-testing/tests/agglayer/rbac_rotation.rs @@ -93,6 +93,7 @@ async fn faucet_accepts_rbac_config_note() -> anyhow::Result<()> { })?; let faucet = create_existing_agglayer_faucet( builder.rng_mut().draw_word(), + "AggLayer Token", "AGG", 6, Felt::from(1_000u32), diff --git a/crates/miden-testing/tests/agglayer/test_utils.rs b/crates/miden-testing/tests/agglayer/test_utils.rs index 8e21b062f6..88b6b9fcd8 100644 --- a/crates/miden-testing/tests/agglayer/test_utils.rs +++ b/crates/miden-testing/tests/agglayer/test_utils.rs @@ -26,7 +26,7 @@ use miden_processor::{ }; use miden_protocol::account::auth::AuthScheme; use miden_protocol::account::{Account, AccountBuilder, AccountId}; -use miden_protocol::asset::{AssetId, FungibleAsset}; +use miden_protocol::asset::{AssetAmount, AssetId, FungibleAsset, TokenSymbol}; use miden_protocol::block::FeeParameters; use miden_protocol::crypto::rand::FeltRng; use miden_protocol::note::{Note, NoteScriptRoot}; @@ -36,6 +36,7 @@ use miden_protocol::utils::sync::LazyLock; use miden_protocol::{Felt, ProtocolLib, Word}; use miden_standards::StandardsLib; use miden_standards::account::access::PausableStorage; +use miden_standards::account::faucets::TokenName; use miden_standards::note::{FeeSponsorshipNote, StandardNote}; use miden_testing::{Auth, MockChain, MockChainBuilder}; use miden_tx::NetworkNotePricer; @@ -173,6 +174,7 @@ pub fn create_existing_priced_bridge( pub fn priced_faucet_builder( seed: Word, + token_name: &str, token_symbol: &str, decimals: u8, max_supply: Felt, @@ -185,10 +187,11 @@ pub fn priced_faucet_builder( let faucet_admin = bridge_admin_account_id(); Ok(AggLayerFaucet::account_builder( seed, - token_symbol, + TokenName::new(token_name)?, + TokenSymbol::new(token_symbol)?, decimals, - max_supply, - initial_supply, + AssetAmount::try_from(max_supply)?, + AssetAmount::try_from(initial_supply)?, faucet_admin, faucet_admin, bridge_account_id, diff --git a/scripts/README.md b/scripts/README.md index d6255f9f63..4a79e78f65 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -12,7 +12,7 @@ The baseline is the latest release tag and it is compared against the version ab - Every exported procedure digest of the `ProtocolLib`, transaction kernel, `StandardsLib` and agglayer packages. - The kernel commitment and the two kernel programs (they cover the prologue, epilogue and procedure ordering, which the package exports do not). -- The agglayer bridge and faucet code commitments, which no package export covers. +- The agglayer bridge code commitment, which no package export covers. - Per account component: each procedure root, plus a commitment over its whole procedure set. A changed or removed root fails. An added procedure is only a warning, but an addition to the kernel or to a component still fails because it changes the corresponding commitment. diff --git a/scripts/check-masm-export-digests.rs b/scripts/check-masm-export-digests.rs index 9be94b30fa..6e21de7bff 100755 --- a/scripts/check-masm-export-digests.rs +++ b/scripts/check-masm-export-digests.rs @@ -70,7 +70,7 @@ fn compare_roots(previous: Roots, current: Roots) -> Result<(), String> { } mod current { - use miden_agglayer_current::{AggLayerBridge, agglayer_package, bridge, faucet}; + use miden_agglayer_current::{AggLayerBridge, agglayer_package, bridge}; use miden_protocol_current::account::AccountComponentCode; use miden_protocol_current::account::component::AUTH_SCRIPT_ATTRIBUTE; use miden_protocol_current::transaction::TransactionKernel; @@ -188,10 +188,6 @@ mod current { "agglayer::BRIDGE_CODE_COMMITMENT".to_string(), bridge::BRIDGE_CODE_COMMITMENT.to_hex(), ); - roots.insert( - "agglayer::FAUCET_CODE_COMMITMENT".to_string(), - faucet::FAUCET_CODE_COMMITMENT.to_hex(), - ); for code in COMPONENT_CODE { collect_component(code(), &mut roots); @@ -233,7 +229,7 @@ mod current { } mod previous { - use miden_agglayer_previous::{AggLayerBridge, agglayer_package, bridge, faucet}; + use miden_agglayer_previous::{AggLayerBridge, agglayer_package, bridge}; use miden_protocol_previous::account::AccountComponentCode; use miden_protocol_previous::account::component::AUTH_SCRIPT_ATTRIBUTE; use miden_protocol_previous::transaction::TransactionKernel; @@ -351,10 +347,6 @@ mod previous { "agglayer::BRIDGE_CODE_COMMITMENT".to_string(), bridge::BRIDGE_CODE_COMMITMENT.to_hex(), ); - roots.insert( - "agglayer::FAUCET_CODE_COMMITMENT".to_string(), - faucet::FAUCET_CODE_COMMITMENT.to_hex(), - ); for code in COMPONENT_CODE { collect_component(code(), &mut roots);