From c09f5e4b50744fba8a75de2e17765314f4fcabbe Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Wed, 11 Mar 2026 11:03:47 +0000 Subject: [PATCH 1/9] docs(agglayer): add Section 6 - Faucet Registry to SPEC.md --- crates/miden-agglayer/SPEC.md | 150 ++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) diff --git a/crates/miden-agglayer/SPEC.md b/crates/miden-agglayer/SPEC.md index 924330d031..7e15be7fcc 100644 --- a/crates/miden-agglayer/SPEC.md +++ b/crates/miden-agglayer/SPEC.md @@ -689,3 +689,153 @@ the little-endian bytes within each limb in `NoteStorage` and the big-endian-byt The encoding is a bijection over the set of valid `AccountId` values: for every valid `AccountId`, `from_account_id` followed by `to_account_id` (or the MASM equivalent) recovers the original. + +--- + +## 6. Faucet Registry + +The AggLayer bridge connects multiple chains, each with its own native token ecosystem. +When tokens move between chains, they need a representation on the destination chain. +This section describes how tokens are registered for bridging and the role of the +faucet registry. + +Terminology: + +- Native token: a token originally issued on a given chain. For example, USDC on Ethereum + is native to Ethereum; a fungible faucet created directly on Miden is native to Miden. +- Non-native (wrapped) token: a representation of a foreign token, created to track + bridged balances. On Miden, each non-native ERC20 is represented by a dedicated + AggLayer faucet. On EVM chains, each non-native Miden token would be represented by a + deployed wrapped ERC20 contract. + +A faucet must be registered in the bridge's faucet registry before it can participate in +bridging. The registry is a map in the bridge account's storage +(`miden::agglayer::bridge::faucet_registry`) that records which faucet account IDs are +authorized for bridge-out operations. Without registration, `bridge_out::bridge_out` +rejects the asset (see `bridge_config::assert_faucet_registered`). + +### 6.1 Bridging-in: Registering non-native faucets on Miden + +When a new ERC20 token is bridged to Miden for the first time, a corresponding AggLayer +faucet account must be created and registered. The faucet serves as the mint/burn +authority for the wrapped token on Miden. + +The `AggLayerFaucet` struct (Rust, `src/lib.rs`) captures the full faucet configuration: + +- Token metadata: symbol, decimals, max_supply (stored in the standard + `NetworkFungibleFaucet` metadata slot) +- Bridge account ID: the bridge this faucet is paired with + (`miden::agglayer::faucet`) +- Origin token address: the ERC20 contract address on the origin chain + (`miden::agglayer::faucet::conversion_info_1`, `conversion_info_2`) +- Origin network: the chain ID of the origin chain (stored in `conversion_info_2`) +- Scale factor: the exponent used to convert between EVM U256 amounts and Miden felt + amounts (stored in `conversion_info_2`) +- Metadata hash: `keccak256(abi.encode(name, symbol, decimals))`, stored across two + value slots (`miden::agglayer::faucet::metadata_hash_lo`, + `miden::agglayer::faucet::metadata_hash_hi`) + +Registration is performed via `CONFIG_AGG_BRIDGE` notes (see Section 3.3). The bridge +operator creates a `CONFIG_AGG_BRIDGE` note containing the faucet's account ID and sends +it to the bridge account. On consumption, the note script calls +`bridge_config::register_faucet`, which: + +1. Asserts the note sender matches the bridge admin stored in + `miden::agglayer::bridge::admin`. +2. Writes the faucet ID into the `faucet_registry` map with value `[1, 0, 0, 0]`. + +The bridge admin is a trusted role. There is currently no on-chain verification that the +faucet's stored metadata (origin address, scale, metadata hash) is correct. The bridge +operator is trusted to deploy faucets with accurate configuration. + +Implementation status: + +- Implemented: Faucet creation with metadata hash, faucet storage layout, FPI retrieval + of metadata hash via `agglayer_faucet::get_metadata_hash`, registration via + `CONFIG_AGG_BRIDGE` notes, faucet registry lookup in `bridge_config::assert_faucet_registered`. +- Not yet implemented: On-chain verification of the metadata hash during registration. + This would require the token name to be available in faucet storage and + `abi.encode(string, string, uint8)` to be implemented in MASM, so the bridge could + recompute `keccak256(abi.encode(name, symbol, decimals))` and compare it against the + stored hash. See the TODO in `bridge_config::register_faucet`. +- Not yet implemented: Token name storage in the faucet. Currently only the symbol is + stored in the standard faucet metadata slot; the full name is not persisted on-chain. + A separate PR is in progress to add this. + +### 6.2 Bridging-out: How Miden-native tokens are registered on other chains + +When an asset is bridged out from Miden, `bridge_out::bridge_out` constructs a leaf for +the Local Exit Tree. The leaf includes the metadata hash, which the bridge fetches from +the faucet via FPI (`agglayer_faucet::get_metadata_hash`). The full leaf structure +contains: leaf type, origin network, origin token address, destination network, +destination address, amount (U256), and metadata hash (see `bridge_out.masm`, +`add_leaf_bridge`). + +On the EVM side, when a user claims the bridged asset via +`PolygonZkEVMBridgeV2.claimAsset()`, the wrapped token is deployed lazily on first claim. +The claimer provides the raw metadata bytes (the ABI-encoded name, symbol, and decimals) +as a parameter to `claimAsset()`. The EVM bridge verifies that +`keccak256(metadata_bytes) == metadataHash` from the Merkle leaf. If the hash matches and +no wrapped token exists yet, the bridge deploys a new `TokenWrapped` ERC20 using the +decoded name, symbol, and decimals from the metadata bytes. + +Not yet implemented - registration of Miden-native faucets for bridging out: + +The current implementation assumes all registered faucets wrap foreign (ERC20) tokens. +For a Miden-native faucet (one that does not wrap an ERC20) to bridge out to an EVM +chain, it would need to: + +- Store a metadata hash computed from its token name, symbol, and decimals. +- Be registered in the bridge's faucet registry. +- Provide origin token address and origin network values that make sense for a + Miden-native asset. + +The design for how a Miden-native faucet registers itself for outbound bridging has not +been finalized. An issue should be created to design this registration flow, covering +questions such as: what origin network ID represents Miden, how the origin token address +is derived for Miden-native tokens, and whether the bridge admin must approve registration +or if self-registration is possible. + +### 6.3 Metadata hash + +The metadata hash serves two purposes in the AggLayer bridge: + +1. Leaf verification: the metadata hash is included in each Local Exit Tree leaf. When a + claim is verified on the destination chain, the leaf's metadata hash must match the + keccak256 of the caller-provided metadata bytes. This binds the token identity to the + Merkle proof. +2. Wrapped token deployment: on EVM chains, the raw metadata bytes (name, symbol, + decimals) provided during `claimAsset()` are used to deploy the wrapped ERC20 contract. + The hash verification ensures the deployed token has the correct name, symbol, and + decimals. + +The metadata hash is computed as: + +``` +keccak256(abi.encode(string name, string symbol, uint8 decimals)) +``` + +This matches the encoding produced by the Solidity bridge's `getTokenMetadata` function. +The `abi.encode` format for `(string, string, uint8)` is: + +- 3 x 32-byte head slots: offset to name, offset to symbol, decimals value +- Name: 32-byte length word + data padded to 32-byte boundary +- Symbol: 32-byte length word + data padded to 32-byte boundary + +In the Rust implementation, `MetadataHash::from_token_info(name, symbol, decimals)` +(defined in `src/eth_types/metadata_hash.rs`) performs this encoding and hashing. The +helper `encode_token_metadata` produces the raw ABI-encoded bytes, and +`MetadataHash::from_abi_encoded` computes the keccak256 digest. + +The metadata hash is computed off-chain at faucet creation time and stored pre-computed +in faucet storage across two value slots (`metadata_hash_lo` and `metadata_hash_hi`, +each holding 4 u32 felts). During bridge-out, the bridge retrieves the hash via FPI +(`agglayer_faucet::get_metadata_hash`) rather than recomputing it on-chain. + +Correctness argument: if a faucet stores an incorrect metadata hash, the EVM-side +`claimAsset()` call will fail. The claimer must provide metadata bytes whose keccak256 +matches the leaf's metadata hash. Since keccak256 is preimage-resistant, the claimer +cannot produce valid metadata bytes for an incorrect hash. This means a misconfigured +faucet would result in unclaimable assets on the destination chain, providing a strong +incentive for the bridge operator to set the correct metadata hash at faucet creation +time. From 5d3b9540fb17b65401a8d5f71100f92a2ac1e53c Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Wed, 11 Mar 2026 11:44:39 +0000 Subject: [PATCH 2/9] docs(agglayer): add issue references to SPEC Section 6 Link #2585 (token name storage) and #2586 (on-chain metadata hash verification) in the implementation status notes. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/miden-agglayer/SPEC.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/miden-agglayer/SPEC.md b/crates/miden-agglayer/SPEC.md index 7e15be7fcc..a60941dca1 100644 --- a/crates/miden-agglayer/SPEC.md +++ b/crates/miden-agglayer/SPEC.md @@ -753,14 +753,17 @@ Implementation status: - Implemented: Faucet creation with metadata hash, faucet storage layout, FPI retrieval of metadata hash via `agglayer_faucet::get_metadata_hash`, registration via `CONFIG_AGG_BRIDGE` notes, faucet registry lookup in `bridge_config::assert_faucet_registered`. -- Not yet implemented: On-chain verification of the metadata hash during registration. +- Not yet implemented: On-chain verification of the metadata hash during registration + ([#2586](https://github.com/0xMiden/protocol/issues/2586)). This would require the token name to be available in faucet storage and `abi.encode(string, string, uint8)` to be implemented in MASM, so the bridge could recompute `keccak256(abi.encode(name, symbol, decimals))` and compare it against the stored hash. See the TODO in `bridge_config::register_faucet`. -- Not yet implemented: Token name storage in the faucet. Currently only the symbol is - stored in the standard faucet metadata slot; the full name is not persisted on-chain. - A separate PR is in progress to add this. +- Not yet implemented: Token name storage in the faucet + ([#2585](https://github.com/0xMiden/protocol/issues/2585), + related: [PR #2439](https://github.com/0xMiden/protocol/pull/2439)). + Currently only the symbol is stored in the standard faucet metadata slot; the full + name is not persisted on-chain. ### 6.2 Bridging-out: How Miden-native tokens are registered on other chains From 10be65699bc4b0a6747503e7e6fd956c6d0c036e Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Fri, 20 Mar 2026 08:27:55 +0000 Subject: [PATCH 3/9] docs(agglayer): update Section 6 to reflect two-step faucet registration Section 6 (Faucet Registry) now accurately describes the current implementation: - Two registries: faucet_registry_map + token_registry_map - register_faucet performs atomic two-step registration - lookup_faucet_by_token_address for bridge-in token resolution - Updated storage slot names (removed miden:: prefix) - AggLayerFaucet struct moved to src/faucet.rs - Companion components (Ownable2Step, OwnerControlled) documented - CONFIG_AGG_BRIDGE note now carries origin_token_address (7 felts) Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/miden-agglayer/SPEC.md | 70 ++++++++++++++++++++++++----------- 1 file changed, 48 insertions(+), 22 deletions(-) diff --git a/crates/miden-agglayer/SPEC.md b/crates/miden-agglayer/SPEC.md index a60941dca1..8c1652f295 100644 --- a/crates/miden-agglayer/SPEC.md +++ b/crates/miden-agglayer/SPEC.md @@ -697,7 +697,7 @@ recovers the original. The AggLayer bridge connects multiple chains, each with its own native token ecosystem. When tokens move between chains, they need a representation on the destination chain. This section describes how tokens are registered for bridging and the role of the -faucet registry. +faucet and token registries. Terminology: @@ -708,11 +708,18 @@ Terminology: AggLayer faucet. On EVM chains, each non-native Miden token would be represented by a deployed wrapped ERC20 contract. -A faucet must be registered in the bridge's faucet registry before it can participate in -bridging. The registry is a map in the bridge account's storage -(`miden::agglayer::bridge::faucet_registry`) that records which faucet account IDs are -authorized for bridge-out operations. Without registration, `bridge_out::bridge_out` -rejects the asset (see `bridge_config::assert_faucet_registered`). +A faucet must be registered in the bridge before it can participate in bridging. The +bridge maintains two registry maps: + +- **Faucet registry** (`agglayer::bridge::faucet_registry_map`): maps faucet account IDs + to a registration flag. Used during bridge-out to verify an asset's faucet is authorized + (see `bridge_config::assert_faucet_registered`). +- **Token registry** (`agglayer::bridge::token_registry_map`): maps Poseidon2 hashes of + origin token addresses to faucet account IDs. Used during bridge-in to look up the + correct faucet for a given origin token (see + `bridge_config::lookup_faucet_by_token_address`). + +Both registries are populated atomically by `bridge_config::register_faucet`. ### 6.1 Bridging-in: Registering non-native faucets on Miden @@ -720,29 +727,46 @@ When a new ERC20 token is bridged to Miden for the first time, a corresponding A faucet account must be created and registered. The faucet serves as the mint/burn authority for the wrapped token on Miden. -The `AggLayerFaucet` struct (Rust, `src/lib.rs`) captures the full faucet configuration: +The `AggLayerFaucet` struct (Rust, `src/faucet.rs`) captures the faucet-specific +configuration: -- Token metadata: symbol, decimals, max_supply (stored in the standard - `NetworkFungibleFaucet` metadata slot) -- Bridge account ID: the bridge this faucet is paired with - (`miden::agglayer::faucet`) +- Token metadata: symbol, decimals, max_supply, token_supply (stored in the standard + `TokenMetadata` slot) - Origin token address: the ERC20 contract address on the origin chain - (`miden::agglayer::faucet::conversion_info_1`, `conversion_info_2`) + (`agglayer::faucet::conversion_info_1`, `conversion_info_2`) - Origin network: the chain ID of the origin chain (stored in `conversion_info_2`) - Scale factor: the exponent used to convert between EVM U256 amounts and Miden felt amounts (stored in `conversion_info_2`) - Metadata hash: `keccak256(abi.encode(name, symbol, decimals))`, stored across two - value slots (`miden::agglayer::faucet::metadata_hash_lo`, - `miden::agglayer::faucet::metadata_hash_hi`) + value slots (`agglayer::faucet::metadata_hash_lo`, + `agglayer::faucet::metadata_hash_hi`) + +The faucet account also requires two companion components: + +- `Ownable2Step`: stores the bridge account ID as the faucet's owner, used for + authorization of mint operations. +- `OwnerControlled`: provides mint policy management (active policy proc root, allowed + policy proc roots, policy authority). Registration is performed via `CONFIG_AGG_BRIDGE` notes (see Section 3.3). The bridge -operator creates a `CONFIG_AGG_BRIDGE` note containing the faucet's account ID and sends -it to the bridge account. On consumption, the note script calls -`bridge_config::register_faucet`, which: +operator creates a `CONFIG_AGG_BRIDGE` note containing the faucet's account ID and the +origin token address, then sends it to the bridge account. On consumption, the note +script calls `bridge_config::register_faucet`, which performs a two-step registration: 1. Asserts the note sender matches the bridge admin stored in - `miden::agglayer::bridge::admin`. -2. Writes the faucet ID into the `faucet_registry` map with value `[1, 0, 0, 0]`. + `agglayer::bridge::admin_account_id`. +2. Writes the faucet ID into the `faucet_registry_map`: + key `[0, 0, faucet_id_suffix, faucet_id_prefix]`, value `[1, 0, 0, 0]`. +3. Hashes the origin token address (5 felts) using `Poseidon2::hash_elements` and writes + the mapping into the `token_registry_map`: + key `hash(origin_token_addr)`, value `[0, 0, faucet_id_suffix, faucet_id_prefix]`. + +The token registry enables the bridge to resolve which faucet corresponds to a given +origin token address during CLAIM note processing. When the bridge's `claim` procedure +processes a CLAIM note, it reads the origin token address from the leaf data and calls +`bridge_config::lookup_faucet_by_token_address` to find the registered faucet. This +lookup hashes the address with Poseidon2 and retrieves the faucet ID from the token +registry map. If the token address is not registered, the lookup panics. The bridge admin is a trusted role. There is currently no on-chain verification that the faucet's stored metadata (origin address, scale, metadata hash) is correct. The bridge @@ -751,14 +775,16 @@ operator is trusted to deploy faucets with accurate configuration. Implementation status: - Implemented: Faucet creation with metadata hash, faucet storage layout, FPI retrieval - of metadata hash via `agglayer_faucet::get_metadata_hash`, registration via - `CONFIG_AGG_BRIDGE` notes, faucet registry lookup in `bridge_config::assert_faucet_registered`. + of metadata hash via `agglayer_faucet::get_metadata_hash`, two-step registration via + `CONFIG_AGG_BRIDGE` notes (faucet registry + token registry), faucet registry lookup in + `bridge_config::assert_faucet_registered`, token registry lookup in + `bridge_config::lookup_faucet_by_token_address`. - Not yet implemented: On-chain verification of the metadata hash during registration ([#2586](https://github.com/0xMiden/protocol/issues/2586)). This would require the token name to be available in faucet storage and `abi.encode(string, string, uint8)` to be implemented in MASM, so the bridge could recompute `keccak256(abi.encode(name, symbol, decimals))` and compare it against the - stored hash. See the TODO in `bridge_config::register_faucet`. + stored hash. - Not yet implemented: Token name storage in the faucet ([#2585](https://github.com/0xMiden/protocol/issues/2585), related: [PR #2439](https://github.com/0xMiden/protocol/pull/2439)). From c6e6f2f2e24b062390a284742de6bd550c05ffee Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Mon, 23 Mar 2026 08:31:05 +0000 Subject: [PATCH 4/9] docs(agglayer): clarify Miden-native faucet bridging in Section 6.2 Replace the "not yet implemented" placeholder with a concrete description of how Miden-native faucets use the same registration and bridging flow as wrapped faucets. The origin_token_address is the faucet's own AccountId embedded as an Ethereum address via EthAddressFormat::from_account_id. The EVM bridge deploys a TokenWrapped ERC20 deterministically on first claim. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/miden-agglayer/SPEC.md | 51 ++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/crates/miden-agglayer/SPEC.md b/crates/miden-agglayer/SPEC.md index 06bf92b777..fa123db008 100644 --- a/crates/miden-agglayer/SPEC.md +++ b/crates/miden-agglayer/SPEC.md @@ -808,22 +808,41 @@ as a parameter to `claimAsset()`. The EVM bridge verifies that no wrapped token exists yet, the bridge deploys a new `TokenWrapped` ERC20 using the decoded name, symbol, and decimals from the metadata bytes. -Not yet implemented - registration of Miden-native faucets for bridging out: - -The current implementation assumes all registered faucets wrap foreign (ERC20) tokens. -For a Miden-native faucet (one that does not wrap an ERC20) to bridge out to an EVM -chain, it would need to: - -- Store a metadata hash computed from its token name, symbol, and decimals. -- Be registered in the bridge's faucet registry. -- Provide origin token address and origin network values that make sense for a - Miden-native asset. - -The design for how a Miden-native faucet registers itself for outbound bridging has not -been finalized. An issue should be created to design this registration flow, covering -questions such as: what origin network ID represents Miden, how the origin token address -is derived for Miden-native tokens, and whether the bridge admin must approve registration -or if self-registration is possible. +#### Miden-native faucets + +A Miden-native faucet (one that does not wrap a foreign ERC20) uses the same storage +layout and registration flow as a wrapped faucet. The key difference is what values are +stored in the conversion metadata: + +- `origin_token_address`: the faucet's own `AccountId` encoded as a 20-byte Ethereum + address via `EthAddressFormat::from_account_id(faucet_id)` (see Section 5.2 for the + embedded format). This produces `[0x00000000 | prefix (8B) | suffix (8B)]`. +- `origin_network`: Miden's network ID as assigned by AggLayer (currently unassigned). +- `metadata_hash`: `keccak256(abi.encode(name, symbol, decimals))` - same as for wrapped + faucets. + +On the EVM side, `claimAsset()` sees `originNetwork != networkID` (Miden is not the local +EVM chain), so it follows the wrapped token path: computes +`tokenInfoHash = keccak256(abi.encodePacked(originNetwork, originTokenAddress))`, deploys +a new `TokenWrapped` ERC20 via `CREATE2` on first claim, and mints on it for subsequent +claims. The wrapper address is deterministic from the `(originNetwork, originTokenAddress)` +pair. + +When bridging back from EVM to Miden, the EVM bridge resolves the wrapped token to its +original `(originNetwork, originTokenAddress)` via `wrappedTokenToTokenInfo`. These values +go into the deposit leaf. On the Miden side, `bridge_config::lookup_faucet_by_token_address` +resolves the origin token address (the embedded faucet AccountId) back to the faucet. + +The bridge code does not distinguish between Miden-native and wrapped faucets - both use +the same registration, bridge-out, and bridge-in paths. The faucet's conversion storage +determines the leaf contents, and the EVM bridge handles token deployment transparently. + +Not yet implemented: + +- Miden's network ID has not been assigned by AggLayer. +- The bridge admin must still approve registration of Miden-native faucets via + `CONFIG_AGG_BRIDGE` notes. Whether self-registration should be possible is an open + design question. ### 6.3 Metadata hash From 4f1a8b98f9e00b926efc8077894a12f3dbd7ea6d Mon Sep 17 00:00:00 2001 From: Marti Date: Mon, 23 Mar 2026 09:32:17 +0000 Subject: [PATCH 5/9] chore: polish sec 6 --- crates/miden-agglayer/SPEC.md | 154 ++++++---------------------------- 1 file changed, 27 insertions(+), 127 deletions(-) diff --git a/crates/miden-agglayer/SPEC.md b/crates/miden-agglayer/SPEC.md index fa123db008..0377bac69a 100644 --- a/crates/miden-agglayer/SPEC.md +++ b/crates/miden-agglayer/SPEC.md @@ -708,7 +708,7 @@ Terminology: AggLayer faucet. On EVM chains, each non-native Miden token would be represented by a deployed wrapped ERC20 contract. -A faucet must be registered in the bridge before it can participate in bridging. The +A faucet must be registered in the [Bridge Contract](#21-bridge-account-component) before it can participate in bridging. The bridge maintains two registry maps: - **Faucet registry** (`agglayer::bridge::faucet_registry_map`): maps faucet account IDs @@ -719,7 +719,7 @@ bridge maintains two registry maps: correct faucet for a given origin token (see `bridge_config::lookup_faucet_by_token_address`). -Both registries are populated atomically by `bridge_config::register_faucet`. +Both registries are populated atomically by `bridge_config::register_faucet` during the [`CONFIG_AGG_BRIDGE`](#33-config_agg_bridge) note consumption. ### 6.1 Bridging-in: Registering non-native faucets on Miden @@ -730,77 +730,41 @@ authority for the wrapped token on Miden. The `AggLayerFaucet` struct (Rust, `src/faucet.rs`) captures the faucet-specific configuration: -- Token metadata: symbol, decimals, max_supply, token_supply (stored in the standard - `TokenMetadata` slot) +- Token metadata: symbol, decimals, max_supply, token_supply (TODO Missing information about the token name([#2585](https://github.com/0xMiden/protocol/issues/2585))) - Origin token address: the ERC20 contract address on the origin chain - (`agglayer::faucet::conversion_info_1`, `conversion_info_2`) -- Origin network: the chain ID of the origin chain (stored in `conversion_info_2`) -- Scale factor: the exponent used to convert between EVM U256 amounts and Miden felt - amounts (stored in `conversion_info_2`) -- Metadata hash: `keccak256(abi.encode(name, symbol, decimals))`, stored across two - value slots (`agglayer::faucet::metadata_hash_lo`, - `agglayer::faucet::metadata_hash_hi`) - -The faucet account also requires two companion components: - -- `Ownable2Step`: stores the bridge account ID as the faucet's owner, used for - authorization of mint operations. -- `OwnerControlled`: provides mint policy management (active policy proc root, allowed - policy proc roots, policy authority). - -Registration is performed via `CONFIG_AGG_BRIDGE` notes (see Section 3.3). The bridge +- Origin network: the chain ID of the origin chain +- Scale factor: the exponent used to convert between EVM U256 amounts and Field elements on Miden +- Metadata hash: `keccak256(abi.encode(name, symbol, decimals))`. This is precomputed by the bridge admin at faucet creation time and is currently not verified onchain (TODO Verify metadata hash onchain ([#2586](https://github.com/0xMiden/protocol/issues/2586))) + +Registration is performed via [`CONFIG_AGG_BRIDGE`](#33-config_agg_bridge) notes. The bridge operator creates a `CONFIG_AGG_BRIDGE` note containing the faucet's account ID and the origin token address, then sends it to the bridge account. On consumption, the note script calls `bridge_config::register_faucet`, which performs a two-step registration: -1. Asserts the note sender matches the bridge admin stored in - `agglayer::bridge::admin_account_id`. -2. Writes the faucet ID into the `faucet_registry_map`: - key `[0, 0, faucet_id_suffix, faucet_id_prefix]`, value `[1, 0, 0, 0]`. -3. Hashes the origin token address (5 felts) using `Poseidon2::hash_elements` and writes +1. Writes the faucet ID into the `faucet_registry_map`: + `[0, 0, faucet_id_suffix, faucet_id_prefix]` -> `[1, 0, 0, 0]`. +2. Hashes the origin token address using Poseidon2 and writes the mapping into the `token_registry_map`: - key `hash(origin_token_addr)`, value `[0, 0, faucet_id_suffix, faucet_id_prefix]`. + `hash(origin_token_addr)` -> `[0, 0, faucet_id_suffix, faucet_id_prefix]`. -The token registry enables the bridge to resolve which faucet corresponds to a given -origin token address during CLAIM note processing. When the bridge's `claim` procedure -processes a CLAIM note, it reads the origin token address from the leaf data and calls +The token registry enables the bridge to resolve which Miden-side faucet corresponds to a given +origin token address during CLAIM note processing. When the bridge +processes a [`CLAIM`](#32-claim) note, it reads the origin token address from the leaf data and calls `bridge_config::lookup_faucet_by_token_address` to find the registered faucet. This lookup hashes the address with Poseidon2 and retrieves the faucet ID from the token -registry map. If the token address is not registered, the lookup panics. - -The bridge admin is a trusted role. There is currently no on-chain verification that the -faucet's stored metadata (origin address, scale, metadata hash) is correct. The bridge -operator is trusted to deploy faucets with accurate configuration. - -Implementation status: - -- Implemented: Faucet creation with metadata hash, faucet storage layout, FPI retrieval - of metadata hash via `agglayer_faucet::get_metadata_hash`, two-step registration via - `CONFIG_AGG_BRIDGE` notes (faucet registry + token registry), faucet registry lookup in - `bridge_config::assert_faucet_registered`, token registry lookup in - `bridge_config::lookup_faucet_by_token_address`. -- Not yet implemented: On-chain verification of the metadata hash during registration - ([#2586](https://github.com/0xMiden/protocol/issues/2586)). - This would require the token name to be available in faucet storage and - `abi.encode(string, string, uint8)` to be implemented in MASM, so the bridge could - recompute `keccak256(abi.encode(name, symbol, decimals))` and compare it against the - stored hash. -- Not yet implemented: Token name storage in the faucet - ([#2585](https://github.com/0xMiden/protocol/issues/2585), - related: [PR #2439](https://github.com/0xMiden/protocol/pull/2439)). - Currently only the symbol is stored in the standard faucet metadata slot; the full - name is not persisted on-chain. +registry map. If the token address is not registered, the `CLAIM` note consumption will fail. + +This means that the bridge admin must register the faucet on the Miden side before the corresponding tokens can be bridged in. + +The bridge admin is a trusted role, and is the sole entity that can register faucets on the Miden side (due to the caller restriction on [`bridge_config::register_faucet`](#bridge_configregister_faucet)). ### 6.2 Bridging-out: How Miden-native tokens are registered on other chains -When an asset is bridged out from Miden, `bridge_out::bridge_out` constructs a leaf for +When an asset is bridged out from Miden, [`bridge_out::bridge_out`](#bridge_outbridge_out) constructs a leaf for the Local Exit Tree. The leaf includes the metadata hash, which the bridge fetches from -the faucet via FPI (`agglayer_faucet::get_metadata_hash`). The full leaf structure -contains: leaf type, origin network, origin token address, destination network, -destination address, amount (U256), and metadata hash (see `bridge_out.masm`, -`add_leaf_bridge`). +the faucet via FPI (`agglayer_faucet::get_metadata_hash`), as well as the other leaf data fields, including origin network and origin token address. -On the EVM side, when a user claims the bridged asset via +On the EVM destination chain, when a user claims the bridged asset via `PolygonZkEVMBridgeV2.claimAsset()`, the wrapped token is deployed lazily on first claim. The claimer provides the raw metadata bytes (the ABI-encoded name, symbol, and decimals) as a parameter to `claimAsset()`. The EVM bridge verifies that @@ -810,80 +774,16 @@ decoded name, symbol, and decimals from the metadata bytes. #### Miden-native faucets -A Miden-native faucet (one that does not wrap a foreign ERC20) uses the same storage +A Miden-native faucet uses the same storage layout and registration flow as a wrapped faucet. The key difference is what values are stored in the conversion metadata: -- `origin_token_address`: the faucet's own `AccountId` encoded as a 20-byte Ethereum - address via `EthAddressFormat::from_account_id(faucet_id)` (see Section 5.2 for the - embedded format). This produces `[0x00000000 | prefix (8B) | suffix (8B)]`. +- `origin_token_address`: the faucet's own `AccountId` as per the [Embedded Format](#52-embedded-format). - `origin_network`: Miden's network ID as assigned by AggLayer (currently unassigned). - `metadata_hash`: `keccak256(abi.encode(name, symbol, decimals))` - same as for wrapped faucets. -On the EVM side, `claimAsset()` sees `originNetwork != networkID` (Miden is not the local -EVM chain), so it follows the wrapped token path: computes +On the EVM side, `claimAsset()` sees `originNetwork != networkID` (foreign asset), so it follows the wrapped token path: computes `tokenInfoHash = keccak256(abi.encodePacked(originNetwork, originTokenAddress))`, deploys -a new `TokenWrapped` ERC20 via `CREATE2` on first claim, and mints on it for subsequent -claims. The wrapper address is deterministic from the `(originNetwork, originTokenAddress)` +a new `TokenWrapped` ERC20 via `CREATE2` on first claim, with the metadata hash as the salt, and mints. The wrapper address is deterministic from the `(originNetwork, originTokenAddress)` pair. - -When bridging back from EVM to Miden, the EVM bridge resolves the wrapped token to its -original `(originNetwork, originTokenAddress)` via `wrappedTokenToTokenInfo`. These values -go into the deposit leaf. On the Miden side, `bridge_config::lookup_faucet_by_token_address` -resolves the origin token address (the embedded faucet AccountId) back to the faucet. - -The bridge code does not distinguish between Miden-native and wrapped faucets - both use -the same registration, bridge-out, and bridge-in paths. The faucet's conversion storage -determines the leaf contents, and the EVM bridge handles token deployment transparently. - -Not yet implemented: - -- Miden's network ID has not been assigned by AggLayer. -- The bridge admin must still approve registration of Miden-native faucets via - `CONFIG_AGG_BRIDGE` notes. Whether self-registration should be possible is an open - design question. - -### 6.3 Metadata hash - -The metadata hash serves two purposes in the AggLayer bridge: - -1. Leaf verification: the metadata hash is included in each Local Exit Tree leaf. When a - claim is verified on the destination chain, the leaf's metadata hash must match the - keccak256 of the caller-provided metadata bytes. This binds the token identity to the - Merkle proof. -2. Wrapped token deployment: on EVM chains, the raw metadata bytes (name, symbol, - decimals) provided during `claimAsset()` are used to deploy the wrapped ERC20 contract. - The hash verification ensures the deployed token has the correct name, symbol, and - decimals. - -The metadata hash is computed as: - -``` -keccak256(abi.encode(string name, string symbol, uint8 decimals)) -``` - -This matches the encoding produced by the Solidity bridge's `getTokenMetadata` function. -The `abi.encode` format for `(string, string, uint8)` is: - -- 3 x 32-byte head slots: offset to name, offset to symbol, decimals value -- Name: 32-byte length word + data padded to 32-byte boundary -- Symbol: 32-byte length word + data padded to 32-byte boundary - -In the Rust implementation, `MetadataHash::from_token_info(name, symbol, decimals)` -(defined in `src/eth_types/metadata_hash.rs`) performs this encoding and hashing. The -helper `encode_token_metadata` produces the raw ABI-encoded bytes, and -`MetadataHash::from_abi_encoded` computes the keccak256 digest. - -The metadata hash is computed off-chain at faucet creation time and stored pre-computed -in faucet storage across two value slots (`metadata_hash_lo` and `metadata_hash_hi`, -each holding 4 u32 felts). During bridge-out, the bridge retrieves the hash via FPI -(`agglayer_faucet::get_metadata_hash`) rather than recomputing it on-chain. - -Correctness argument: if a faucet stores an incorrect metadata hash, the EVM-side -`claimAsset()` call will fail. The claimer must provide metadata bytes whose keccak256 -matches the leaf's metadata hash. Since keccak256 is preimage-resistant, the claimer -cannot produce valid metadata bytes for an incorrect hash. This means a misconfigured -faucet would result in unclaimable assets on the destination chain, providing a strong -incentive for the bridge operator to set the correct metadata hash at faucet creation -time. From e31088c9977eac06989d087f7977f6a5c80d83af Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Mon, 23 Mar 2026 09:47:09 +0000 Subject: [PATCH 6/9] docs(agglayer): fix CREATE2 salt description in Miden-native faucets The CREATE2 salt is tokenInfoHash (derived from originNetwork + originTokenAddress), not the metadata hash. The metadata bytes are used to initialize the wrapped token's name, symbol, and decimals. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/miden-agglayer/SPEC.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/miden-agglayer/SPEC.md b/crates/miden-agglayer/SPEC.md index 0377bac69a..082c594d91 100644 --- a/crates/miden-agglayer/SPEC.md +++ b/crates/miden-agglayer/SPEC.md @@ -783,7 +783,11 @@ stored in the conversion metadata: - `metadata_hash`: `keccak256(abi.encode(name, symbol, decimals))` - same as for wrapped faucets. -On the EVM side, `claimAsset()` sees `originNetwork != networkID` (foreign asset), so it follows the wrapped token path: computes -`tokenInfoHash = keccak256(abi.encodePacked(originNetwork, originTokenAddress))`, deploys -a new `TokenWrapped` ERC20 via `CREATE2` on first claim, with the metadata hash as the salt, and mints. The wrapper address is deterministic from the `(originNetwork, originTokenAddress)` -pair. +On the EVM side, `claimAsset()` sees `originNetwork != networkID` (foreign asset), so it +follows the wrapped token path: computes +`tokenInfoHash = keccak256(abi.encodePacked(originNetwork, originTokenAddress))`, and +deploys a new `TokenWrapped` ERC20 via `CREATE2` on first claim, minting on subsequent +claims. The `CREATE2` salt is `tokenInfoHash`, so the wrapper address is deterministic +from the `(originNetwork, originTokenAddress)` pair. The metadata bytes provided by the +claimer (which must hash to the leaf's `metadataHash`) are used to initialize the wrapped +token's name, symbol, and decimals. From f03474b63b5bffba4f6722ed4ab311e6aaed90c1 Mon Sep 17 00:00:00 2001 From: Marti Date: Mon, 23 Mar 2026 11:03:10 +0100 Subject: [PATCH 7/9] Update crates/miden-agglayer/SPEC.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- crates/miden-agglayer/SPEC.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/miden-agglayer/SPEC.md b/crates/miden-agglayer/SPEC.md index 082c594d91..b87ddcbb6e 100644 --- a/crates/miden-agglayer/SPEC.md +++ b/crates/miden-agglayer/SPEC.md @@ -730,7 +730,7 @@ authority for the wrapped token on Miden. The `AggLayerFaucet` struct (Rust, `src/faucet.rs`) captures the faucet-specific configuration: -- Token metadata: symbol, decimals, max_supply, token_supply (TODO Missing information about the token name([#2585](https://github.com/0xMiden/protocol/issues/2585))) +- Token metadata: symbol, decimals, max_supply, token_supply (TODO Missing information about the token name ([#2585](https://github.com/0xMiden/protocol/issues/2585))) - Origin token address: the ERC20 contract address on the origin chain - Origin network: the chain ID of the origin chain - Scale factor: the exponent used to convert between EVM U256 amounts and Field elements on Miden From 273b45132f049b0a9e4e5a3d4ff71d171e5c35f7 Mon Sep 17 00:00:00 2001 From: Marti Date: Mon, 23 Mar 2026 11:03:42 +0100 Subject: [PATCH 8/9] Update crates/miden-agglayer/SPEC.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- crates/miden-agglayer/SPEC.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/miden-agglayer/SPEC.md b/crates/miden-agglayer/SPEC.md index b87ddcbb6e..b5339d83cc 100644 --- a/crates/miden-agglayer/SPEC.md +++ b/crates/miden-agglayer/SPEC.md @@ -741,7 +741,7 @@ operator creates a `CONFIG_AGG_BRIDGE` note containing the faucet's account ID a origin token address, then sends it to the bridge account. On consumption, the note script calls `bridge_config::register_faucet`, which performs a two-step registration: -1. Writes the faucet ID into the `faucet_registry_map`: +1. Writes a registration flag under the faucet ID key in the `faucet_registry_map`: `[0, 0, faucet_id_suffix, faucet_id_prefix]` -> `[1, 0, 0, 0]`. 2. Hashes the origin token address using Poseidon2 and writes the mapping into the `token_registry_map`: From ed5395341ceeefa2053cb853538da4ff7e8111a1 Mon Sep 17 00:00:00 2001 From: Marti Date: Mon, 23 Mar 2026 12:29:51 +0100 Subject: [PATCH 9/9] Update crates/miden-agglayer/SPEC.md Co-authored-by: Alexander John Lee <77119221+partylikeits1983@users.noreply.github.com> --- crates/miden-agglayer/SPEC.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/miden-agglayer/SPEC.md b/crates/miden-agglayer/SPEC.md index b5339d83cc..e53f292ab4 100644 --- a/crates/miden-agglayer/SPEC.md +++ b/crates/miden-agglayer/SPEC.md @@ -715,7 +715,7 @@ bridge maintains two registry maps: to a registration flag. Used during bridge-out to verify an asset's faucet is authorized (see `bridge_config::assert_faucet_registered`). - **Token registry** (`agglayer::bridge::token_registry_map`): maps Poseidon2 hashes of - origin token addresses to faucet account IDs. Used during bridge-in to look up the + native token addresses to faucet account IDs. Used during bridge-in to look up the correct faucet for a given origin token (see `bridge_config::lookup_faucet_by_token_address`).