Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion crates/miden-agglayer/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ The crate `miden-agglayer` implements the AggLayer bridging protocol on the Mide
A user initiates a bridge-out by creating a [`B2AGG`](#41-b2agg) note containing a single fungible
asset and the destination network/address. The bridge account consumes this note:

1. Validates that the asset's faucet is registered in the faucet registry.
1. Validates that the asset's faucet is registered in the faucet registry, and that the
destination network is not Miden's AggLayer network ID.
2. FPIs to the faucet (`agglayer_faucet::asset_to_origin_asset`) to obtain the scaled
U256 amount, origin token address, and origin network.
3. FPIs to the faucet (`agglayer_faucet::get_metadata_hash`) to obtain the metadata hash.
Expand Down
32 changes: 32 additions & 0 deletions crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,19 @@ use miden::standards::note::execution_hint::ALWAYS
use miden::protocol::types::MemoryAddress
use miden::protocol::output_note
use miden::core::crypto::hashes::poseidon2
use agglayer::common::constants::MIDEN_NETWORK_ID
use agglayer::common::utils
use agglayer::faucet -> agglayer_faucet
use agglayer::bridge::bridge_config
use agglayer::bridge::leaf_utils
use agglayer::bridge::merkle_tree_frontier
use agglayer::common::eth_address::EthereumAddressFormat

# ERRORS
# =================================================================================================

const ERR_B2AGG_DESTINATION_NETWORK_IS_MIDEN = "B2AGG note destination network ID must not be Miden's AggLayer network ID"

# CONSTANTS
# =================================================================================================

Expand Down Expand Up @@ -100,6 +106,9 @@ const BURN_NOTE_NUM_STORAGE_ITEMS=0
#! - dest_network_id is the u32 destination network/chain ID.
#! - dest_address(5) are 5 u32 values representing a 20-byte Ethereum address.
#!
#! Panics if:
#! - destination network ID is Miden's AggLayer network ID.
#!
#! Invocation: call
@locals(14)
pub proc bridge_out
Expand All @@ -110,6 +119,9 @@ pub proc bridge_out
exec.asset::store
# => [dest_network_id, dest_address(5), pad(10)]

dup exec.assert_destination_id_not_miden_id
# => [dest_network_id, dest_address(5), pad(10)]

loc_store.DESTINATION_NETWORK_LOC
loc_store.DESTINATION_ADDRESS_0_LOC
loc_store.DESTINATION_ADDRESS_1_LOC
Expand Down Expand Up @@ -222,6 +234,26 @@ end
# HELPER PROCEDURES
# =================================================================================================

#! Asserts that the bridge-out destination network ID is not equal to the Miden's AggLayer network
#! ID.
#!
#! Inputs: [dest_network_id]
#! Outputs: []
#!
#! Panics if:
#! - the destination network ID equals `MIDEN_NETWORK_ID`.
#!
#! Invocation: exec
proc assert_destination_id_not_miden_id
# change the endianness to BE to compare it with the Miden network ID
exec.utils::swap_u32_bytes
# => [destination_network_id_be]

# assert that the destination network ID is not equal to the Miden network ID
push.MIDEN_NETWORK_ID neq assert.err=ERR_B2AGG_DESTINATION_NETWORK_IS_MIDEN
# => []
end

#! Validates that a faucet is registered in the bridge's faucet registry, then performs an FPI call
#! to the faucet's `asset_to_origin_asset` procedure to obtain the scaled amount, origin token
#! address, and origin network.
Expand Down
1 change: 1 addition & 0 deletions crates/miden-agglayer/asm/note_scripts/b2agg.masm
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const ERR_B2AGG_TARGET_ACCOUNT_MISMATCH="B2AGG note attachment target account do
#! - The note does not contain exactly 6 storage items.
#! - The note does not contain exactly 1 asset.
#! - The note attachment does not target the consuming account.
#! - The destination network ID equals Miden's AggLayer network ID.
begin
dropw
# => [pad(16)]
Expand Down
122 changes: 121 additions & 1 deletion crates/miden-testing/tests/agglayer/bridge_out.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
extern crate alloc;

use miden_agglayer::errors::{ERR_B2AGG_TARGET_ACCOUNT_MISMATCH, ERR_FAUCET_NOT_REGISTERED};
use miden_agglayer::errors::{
ERR_B2AGG_DESTINATION_NETWORK_IS_MIDEN,
ERR_B2AGG_TARGET_ACCOUNT_MISMATCH,
ERR_FAUCET_NOT_REGISTERED,
};
use miden_agglayer::{
AggLayerBridge,
B2AggNote,
Expand Down Expand Up @@ -352,6 +356,122 @@ async fn test_bridge_out_fails_with_unregistered_faucet() -> anyhow::Result<()>
Ok(())
}

/// B2AGG / bridge-out must reject a note whose `destination_network` equals the Miden network ID,
/// even when the faucet is registered and the rest of the bridge-out path would otherwise succeed.
#[tokio::test]
async fn test_bridge_out_fails_when_destination_is_miden_network() -> anyhow::Result<()> {
let mut builder = MockChain::builder();

// CREATE BRIDGE ADMIN ACCOUNT (sends CONFIG_AGG_BRIDGE notes)
// --------------------------------------------------------------------------------------------
let bridge_admin = builder.add_existing_wallet(Auth::BasicAuth {
auth_scheme: AuthScheme::Falcon512Poseidon2,
})?;

// CREATE GER MANAGER ACCOUNT (not used for GER in this test, but distinct from admin)
// --------------------------------------------------------------------------------------------
let ger_manager = builder.add_existing_wallet(Auth::BasicAuth {
auth_scheme: AuthScheme::Falcon512Poseidon2,
})?;

// CREATE BRIDGE ACCOUNT
// --------------------------------------------------------------------------------------------
let mut bridge_account = create_existing_bridge_account(
builder.rng_mut().draw_word(),
bridge_admin.id(),
ger_manager.id(),
);
builder.add_account(bridge_account.clone())?;

// CREATE AGGLAYER FAUCET ACCOUNT (with conversion metadata for FPI)
// Use MTF vector token metadata and a fixed origin network compatible with the vectors.
// --------------------------------------------------------------------------------------------
let vectors = &*SOLIDITY_MTF_VECTORS;
let origin_token_address =
EthAddress::from_hex(&vectors.origin_token_address).expect("valid origin token address");
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_symbol,
vectors.token_decimals,
Felt::new(FungibleAsset::MAX_AMOUNT),
Felt::new(100),
bridge_account.id(),
&origin_token_address,
64u32,
0u8,
metadata_hash,
);
builder.add_account(faucet.clone())?;

// CREATE CONFIG_AGG_BRIDGE NOTE (registers faucet + token address in bridge)
// --------------------------------------------------------------------------------------------
let config_note = ConfigAggBridgeNote::create(
faucet.id(),
&origin_token_address,
bridge_admin.id(),
bridge_account.id(),
builder.rng_mut(),
)?;
builder.add_output_note(RawOutputNote::Full(config_note.clone()));

// CREATE B2AGG NOTE (targets the bridge)
// Set destination_network to exactly `AggLayerBridge::MIDEN_NETWORK_ID` so `bridge_out`
// fails immediately.
// --------------------------------------------------------------------------------------------
let amount = Felt::new(100);
let bridge_asset: Asset =
FungibleAsset::new(faucet.id(), amount.as_canonical_u64()).unwrap().into();
let eth_address =
EthAddress::from_hex(&vectors.destination_addresses[0]).expect("valid destination address");

let b2agg_note = B2AggNote::create(
AggLayerBridge::MIDEN_NETWORK_ID,
eth_address,
NoteAssets::new(vec![bridge_asset])?,
bridge_account.id(),
faucet.id(),
builder.rng_mut(),
)?;

builder.add_output_note(RawOutputNote::Full(b2agg_note.clone()));

// BUILD MOCK CHAIN WITH ALL ACCOUNTS AND PENDING OUTPUT NOTES
// --------------------------------------------------------------------------------------------
let mut mock_chain = builder.build()?;
mock_chain.prove_next_block()?;

// TX0: EXECUTE CONFIG_AGG_BRIDGE NOTE TO REGISTER FAUCET IN BRIDGE
// --------------------------------------------------------------------------------------------
let config_executed = mock_chain
.build_tx_context(bridge_account.id(), &[config_note.id()], &[])?
.build()?
.execute()
.await?;
bridge_account.apply_delta(config_executed.account_delta())?;
mock_chain.add_pending_executed_transaction(&config_executed)?;
mock_chain.prove_next_block()?;

// TX1: EXECUTE B2AGG NOTE AGAINST BRIDGE (must fail: destination_network is Miden's ID)
// --------------------------------------------------------------------------------------------
let foreign_account_inputs = mock_chain.get_foreign_account_inputs(faucet.id())?;

let result = mock_chain
.build_tx_context(bridge_account.id(), &[b2agg_note.id()], &[])?
.foreign_accounts(vec![foreign_account_inputs])
.build()?
.execute()
.await;

assert_transaction_executor_error!(result, ERR_B2AGG_DESTINATION_NETWORK_IS_MIDEN);

Ok(())
}

/// Tests the B2AGG (Bridge to AggLayer) note script reclaim functionality.
///
/// This test covers the "reclaim" branch where the note creator consumes their own B2AGG note.
Expand Down
Loading