Skip to content

Bridging out: AggLayerFungibleFaucet contract (only bridging out functionality) #1893

@mmagician

Description

@mmagician

Bridging summary
The overall flow for bridging out from Miden -> AggLayer

  1. The user creates a Bridge-to-AggLayer (B2AGG) note with ASSET to be bridged out, and sends it to the AggLayerBridgeOut contract
  2. The AggLayerBridgeOut account verifies that the faucet for the ASSET in the note is in the faucet registry it maintains. It then,
    1. Makes an FPI call to this AggLayerFungibleFaucet to call its convert_asset procedure and get the Ethereum-native representation of the token's amount + address.
    2. Hashes the information about the asset to be bridged out to obtain a hash value, and appends it to a Local Exit Tree (a Merkle Tree) it maintains - only the frontier branch is needed at any given time, not the full tree.
    3. Sends a burn note with the original asset to the AggLayerFungibleFaucet
    4. The faucet will consume the note and burn the contained asset.

AggLayerFungibleFaucet interface

We need to first implement "network faucets".

Storage

  • Slot 0(value): metadata: just like BasicFungibleFaucet. Note: we might need to include the "token name" there as well, see here (this could be a useful addition to our standard faucet, too)
  • Slot 1(value): native_address. 20-byte address representation of the token on the L1
  • (optional, see below) Slot 2 (value): Amount scaling parameter

Procedure exports

  • convert_asset. Expects ASSET on the stack. Returns [NATIVE_AMOUNT, NATIVE_IDENTIFIER] of this asset on the L1.
  • burn: consume the BURN note, burn the asset

The NATIVE_AMOUNT is actually a 256 bit value, but since we handle the conversion from Felt -> uint256 ourselves, we can always ensure this fits into a single field element (upon bridging-in, the uint256 amounts have to be truncated to a single Felt, so we can always convert back).

Miden amounts <> L1 amounts

The question is whether the amount conversion "Miden amounts -> Ethereum amounts" will always be the same regardless of the token?

The answer is no, it depends on the decimals that the native (ETH or ERC20) token specifies. Ethereum tokens can freely choose a uint8 (0-255) value for the #decimals.

We have three approaches:

Fixed scaling for all tokens with fixed-point arithmetic

leads to loss of whole tokens on one end (low decimals() value, users with high balances), or loss of important precision (high decimals() value, low balances in user wallets)

The faucet contract doesn't need to store this explicitly (can be hardcoded in the conversion function).

Dynamic scaling (fixed-point scaling)

Dynamic scaling s.t. the maximum supply of the (native) token fits into a single field element, so as to minimize dust lost upon conversion:

  1. Pick scale s.t. total_supply_of_native_token / 2**scale <= Felt::max()
  2. Miden balances (on bridging in): miden_balance = native_balance / 2**scale
  3. Bridging out: native_balance = miden_balance * 2**scale

Note: scale plays the same role as decimals from ERC20, but I use a different name to avoid confusion.

The faucet contract has to store scale in storage (it's different per token).
This lets us account for most (?) tokens out there (TODO confirm this!)
With this approach, however, we are still susceptible to losing whole tokens if the native faucet has a very low decimals() value.

Fixed scaling for all tokens with floating-point arithmetic**

Instead of a single fixed divisor, represent balances as (mantissa, exponent) pairs, similar to IEEE floating point representation.
E.g. 11 bits exponent, 52 bits mantissa like the double float format (leading bit always fixed to 0, we don't need negative balances).

Bridging in:

  • Normalize native_balance into mantissa * 10^exponent such that mantissa fits into the mantissa field.
    Store (mantissa, exponent) packed into a single field element to represent the asset amount.

Bridging out:
Reconstruct native_balance ≈ mantissa * 10^exponent.

This guarantees that even tokens with decimals = 0 and very large supplies can be represented, at the cost of more complex arithmetic (not just upon bridging in/out, but also for wallets etc.).

The split #mantissa_bits <> #exponent_bits would be fixed (Miden floating token standard?), and therefore the faucet contract doesn't need to track it.

Clearly the last approach is the most complex and has the widest repercussions into the wallet/explorer land.

FPI calls from AggLayerBridgeOut vs. local conversion

An alternative design is to forego the FPI call to this faucet contract, and instead store the Ethereum-native token address in AggLayerBridgeOut's faucet_registry map (since the value of the map is currently unused).

Regardless of the approach for scaling the amounts, we can always fit the conversion information into the faucet_registry map (we're only using 20 bytes there, so the last field element is completely unused):

  • we need no extra info if we choose "fixed scaling, fixed-point" approach
  • there's a scale parameter needed for "dynamic scaling, fixed-point". But we can use u8 which easily fits in the faucet_registry map
  • we need no extra info for "fixed scaling, floating point"

Therefore, we could instead add an internal procedure to AggLayerBridgeOut to handle that and avoid the FPI call entirely.

Unless I'm missing something, we should be able to follow this alternative design, in which case I don't think there are any outstanding differences between the AggLayerFungibleFaucet and NetworkFungibleFaucet described in #1891, and we could close this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    agglayerPRs or issues related to AggLayer bridging integrationstandardsRelated to standard note scripts or account components

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions