-
Notifications
You must be signed in to change notification settings - Fork 124
Description
Bridging summary
The overall flow for bridging out from Miden -> AggLayer
- The user creates a Bridge-to-AggLayer (
B2AGG) note withASSETto be bridged out, and sends it to theAggLayerBridgeOutcontract - The
AggLayerBridgeOutaccount verifies that the faucet for theASSETin the note is in the faucet registry it maintains. It then,- Makes an FPI call to this
AggLayerFungibleFaucetto call itsconvert_assetprocedure and get the Ethereum-native representation of the token's amount + address. - 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.
- Sends a burn note with the original asset to the
AggLayerFungibleFaucet - The faucet will consume the note and burn the contained asset.
- Makes an FPI call to this
AggLayerFungibleFaucet interface
We need to first implement "network faucets".
Storage
- Slot 0(value):
metadata: just likeBasicFungibleFaucet. 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. ExpectsASSETon the stack. Returns[NATIVE_AMOUNT, NATIVE_IDENTIFIER]of this asset on the L1.burn: consume theBURNnote, 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:
- Pick
scales.t.total_supply_of_native_token / 2**scale <= Felt::max() - Miden balances (on bridging in):
miden_balance = native_balance / 2**scale - 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_balanceinto 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
scaleparameter needed for "dynamic scaling, fixed-point". But we can useu8which easily fits in thefaucet_registrymap - 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.