Skip to content
Closed
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
65 changes: 65 additions & 0 deletions crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const ERR_FAUCET_NOT_REGISTERED = "faucet is not registered in the bridge's fauc
const ERR_TOKEN_NOT_REGISTERED = "token address is not registered in the bridge's token registry"
const ERR_SENDER_NOT_BRIDGE_ADMIN = "note sender is not the bridge admin"
const ERR_SENDER_NOT_GER_MANAGER = "note sender is not the global exit root manager"
const ERR_BRIDGE_IS_PAUSED = "bridge is currently paused"
const ERR_INVALID_PAUSE_FLAG = "paused flag must be 0 or 1"

# CONSTANTS
# =================================================================================================
Expand All @@ -22,6 +24,7 @@ const GER_MANAGER_SLOT = word("agglayer::bridge::ger_manager_account_id")
const GER_MAP_STORAGE_SLOT = word("agglayer::bridge::ger_map")
const FAUCET_REGISTRY_MAP_SLOT = word("agglayer::bridge::faucet_registry_map")
const TOKEN_REGISTRY_MAP_SLOT = word("agglayer::bridge::token_registry_map")
const PAUSED_SLOT = word("agglayer::bridge::paused")

# Flags
const GER_KNOWN_FLAG = 1
Expand All @@ -42,10 +45,14 @@ const TOKEN_ADDR_HASH_PTR = 0
#! Outputs: [pad(16)]
#!
#! Panics if:
#! - the bridge is currently paused.
#! - the note sender is not the global exit root manager.
#!
#! Invocation: call
pub proc update_ger
# assert the bridge is not paused.
exec.assert_not_paused

# assert the note sender is the global exit root manager.
exec.assert_sender_is_ger_manager
# => [GER_LOWER[4], GER_UPPER[4], pad(8)]
Expand Down Expand Up @@ -113,10 +120,14 @@ end
#! Outputs: [pad(16)]
#!
#! Panics if:
#! - the bridge is currently paused.
#! - the note sender is not the bridge admin.
#!
#! Invocation: call
pub proc register_faucet
# assert the bridge is not paused.
exec.assert_not_paused

# assert the note sender is the bridge admin.
exec.assert_sender_is_bridge_admin
# => [origin_token_addr(5), faucet_id_suffix, faucet_id_prefix, pad(9)]
Expand Down Expand Up @@ -228,9 +239,63 @@ proc lookup_faucet_by_token_address
# => [faucet_id_suffix, faucet_id_prefix]
end

#! Sets or clears the emergency paused flag in bridge storage.
#!
#! Only the bridge admin can invoke this procedure. NOT guarded by assert_not_paused so the admin
#! can always unpause the bridge.
#!
#! Inputs: [paused_flag, pad(15)]
#! Outputs: [pad(16)]
#!
#! Where:
#! - paused_flag: 1 to pause, 0 to unpause.
#!
#! Panics if:
#! - the note sender is not the bridge admin.
#! - paused_flag is not 0 or 1.
#!
#! Invocation: call
pub proc set_emergency_paused
# assert the note sender is the bridge admin.
exec.assert_sender_is_bridge_admin
# => [paused_flag, pad(15)]

# validate paused_flag is 0 or 1
dup u32lt.2 assert.err=ERR_INVALID_PAUSE_FLAG
# => [paused_flag, pad(15)]

# The top 4 elements are already [paused_flag, 0, 0, 0] - the VALUE word to store.
push.PAUSED_SLOT[0..2]
exec.native_account::set_item
# => [OLD_VALUE, pad(12)]

dropw
# => [pad(16)]
end

# HELPER PROCEDURES
# =================================================================================================

#! Asserts that the bridge is not currently paused.
#!
#! Reads the paused flag from storage and panics if it is non-zero.
#!
#! Inputs: [any(16)]
#! Outputs: [any(16)]
#!
#! Panics if:
#! - the bridge is currently paused.
#!
#! Invocation: exec
proc assert_not_paused
push.PAUSED_SLOT[0..2]
exec.active_account::get_item
# => [paused_flag, 0, 0, 0, any(16)]

assertz.err=ERR_BRIDGE_IS_PAUSED drop drop drop
# => [any(16)]
end

#! Hashes a 5-felt origin token address using Poseidon2.
#!
#! Writes the 5 felts to memory and computes the Poseidon2 hash.
Expand Down
4 changes: 4 additions & 0 deletions crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm
Original file line number Diff line number Diff line change
Expand Up @@ -192,13 +192,17 @@ const CLAIM_DEST_ID_SUFFIX_LOCAL = 1
#! }
#!
#! Panics if:
#! - the bridge is currently paused.
#! - the leaf type is not 0 (not an asset claim).
#! - the Merkle proof validation fails.
#! - the origin token address is not registered in the bridge's token registry.
#!
#! Invocation: call
@locals(2) # 0: dest_prefix, 1: dest_suffix
pub proc claim
# assert the bridge is not paused.
exec.bridge_config::assert_not_paused

# Write output note faucet amount to memory
movup.8 mem_store.CLAIM_OUTPUT_NOTE_FAUCET_AMOUNT
# => [PROOF_DATA_KEY, LEAF_DATA_KEY, pad(8)]
Expand Down
6 changes: 6 additions & 0 deletions crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,17 @@ 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:
#! - the bridge is currently paused.
#!
#! Invocation: call
@locals(14)
pub proc bridge_out
# => [ASSET_KEY, ASSET_VALUE, dest_network_id, dest_address(5), pad(2)]

# assert the bridge is not paused.
exec.bridge_config::assert_not_paused

# Save ASSET to local memory for later BURN note creation
locaddr.BRIDGE_OUT_BURN_ASSET_LOC
exec.asset::store
Expand Down
2 changes: 2 additions & 0 deletions crates/miden-agglayer/asm/components/bridge.masm
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
# The bridge exposes:
# - `register_faucet` from the bridge_config module
# - `update_ger` from the bridge_config module
# - `set_emergency_paused` from the bridge_config module
# - `claim` for bridge-in
# - `bridge_out` for bridge-out

pub use ::agglayer::bridge::bridge_config::register_faucet
pub use ::agglayer::bridge::bridge_config::set_emergency_paused
pub use ::agglayer::bridge::bridge_config::update_ger
pub use ::agglayer::bridge::bridge_in::claim
pub use ::agglayer::bridge::bridge_out::bridge_out
63 changes: 63 additions & 0 deletions crates/miden-agglayer/asm/note_scripts/emergency_pause.masm
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
use agglayer::bridge::bridge_config
use miden::protocol::active_note
use miden::standards::attachments::network_account_target

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

const EMERGENCY_PAUSE_NOTE_NUM_STORAGE_ITEMS = 1
const STORAGE_PTR = 0

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

const ERR_EMERGENCY_PAUSE_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS = "EMERGENCY_PAUSE script expects exactly 1 note storage item"
const ERR_EMERGENCY_PAUSE_TARGET_ACCOUNT_MISMATCH = "EMERGENCY_PAUSE note attachment target account does not match consuming account"

# NOTE SCRIPT
# =================================================================================================

#! Agglayer Bridge EMERGENCY_PAUSE script: sets or clears the emergency paused flag by calling
#! bridge_config::set_emergency_paused.
#!
#! This note can only be consumed by the specific agglayer bridge account whose ID is provided
#! in the note attachment (target_account_id), and only if the note was sent by the bridge admin.
#!
#! Requires that the account exposes:
#! - agglayer::bridge_config::set_emergency_paused procedure.
#!
#! Inputs: [ARGS, pad(12)]
#! Outputs: [pad(16)]
#!
#! NoteStorage layout (1 felt total):
#! - paused_flag [0] : 1 felt (1 = pause, 0 = unpause)
#!
#! Panics if:
#! - account does not expose set_emergency_paused procedure.
#! - target account ID does not match the consuming account ID.
#! - number of note storage items is not exactly 1.
#! - the note sender is not the bridge admin.
begin
dropw
# => [pad(16)]

# Ensure note attachment targets the consuming bridge account.
exec.network_account_target::active_account_matches_target_account
assert.err=ERR_EMERGENCY_PAUSE_TARGET_ACCOUNT_MISMATCH
# => [pad(16)]

# Load note storage to memory
push.STORAGE_PTR exec.active_note::get_storage
# => [num_storage_items, dest_ptr, pad(16)]

# Validate the number of storage items
push.EMERGENCY_PAUSE_NOTE_NUM_STORAGE_ITEMS assert_eq.err=ERR_EMERGENCY_PAUSE_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS drop
# => [pad(16)]

# Load paused_flag word from note storage (replaces top 4, depth-neutral)
mem_loadw_le.STORAGE_PTR
# => [paused_flag, 0, 0, 0, pad(12)]

call.bridge_config::set_emergency_paused
# => [pad(16)]
end
36 changes: 36 additions & 0 deletions crates/miden-agglayer/src/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@ static TOKEN_REGISTRY_MAP_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|
.expect("token registry map storage slot name should be valid")
});

// emergency pause
// ------------------------------------------------------------------------------------------------

static PAUSED_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("agglayer::bridge::paused")
.expect("paused storage slot name should be valid")
});

// bridge in
// ------------------------------------------------------------------------------------------------

Expand Down Expand Up @@ -125,6 +133,8 @@ static LET_NUM_LEAVES_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
/// - [`Self::ger_map_slot_name`]: Stores the GERs.
/// - [`Self::faucet_registry_map_slot_name`]: Stores the faucet registry map.
/// - [`Self::token_registry_map_slot_name`]: Stores the token address → faucet ID map.
/// - [`Self::paused_slot_name`]: Stores the emergency paused flag (\[0, 0, 0, 0\] = active, \[1, 0,
/// 0, 0\] = paused).
/// - [`Self::claim_nullifiers_slot_name`]: Stores the CLAIM note nullifiers map (RPO(leaf_index,
/// source_bridge_network) → \[1, 0, 0, 0\]).
/// - [`Self::cgi_chain_hash_lo_slot_name`]: Stores the lower 128 bits of the CGI chain hash.
Expand Down Expand Up @@ -186,6 +196,13 @@ impl AggLayerBridge {
&TOKEN_REGISTRY_MAP_SLOT_NAME
}

// --- emergency pause -

/// Storage slot name for the emergency paused flag.
pub fn paused_slot_name() -> &'static StorageSlotName {
&PAUSED_SLOT_NAME
}

// --- bridge in --------

/// Storage slot name for the CLAIM note nullifiers map.
Expand Down Expand Up @@ -225,6 +242,23 @@ impl AggLayerBridge {
&LET_NUM_LEAVES_SLOT_NAME
}

/// Returns whether the bridge is currently paused.
///
/// # Errors
///
/// Returns an error if:
/// - the provided account is not an [`AggLayerBridge`] account.
pub fn is_paused(bridge_account: &Account) -> Result<bool, AgglayerBridgeError> {
Self::assert_bridge_account(bridge_account)?;

let paused_word = bridge_account
.storage()
.get_item(Self::paused_slot_name())
.expect("provided account should have AggLayer Bridge specific storage slots");

Ok(paused_word.to_vec()[0] != ZERO)
}

/// Returns a boolean indicating whether the provided GER is present in storage of the provided
/// bridge account.
///
Expand Down Expand Up @@ -416,6 +450,7 @@ impl AggLayerBridge {
&*GER_MANAGER_ID_SLOT_NAME,
&*CGI_CHAIN_HASH_LO_SLOT_NAME,
&*CGI_CHAIN_HASH_HI_SLOT_NAME,
&*PAUSED_SLOT_NAME,
&*CLAIM_NULLIFIERS_SLOT_NAME,
]
}
Expand All @@ -438,6 +473,7 @@ impl From<AggLayerBridge> for AccountComponent {
StorageSlot::with_value(GER_MANAGER_ID_SLOT_NAME.clone(), ger_manager_word),
StorageSlot::with_value(CGI_CHAIN_HASH_LO_SLOT_NAME.clone(), Word::empty()),
StorageSlot::with_value(CGI_CHAIN_HASH_HI_SLOT_NAME.clone(), Word::empty()),
StorageSlot::with_value(PAUSED_SLOT_NAME.clone(), Word::empty()),
StorageSlot::with_empty_map(CLAIM_NULLIFIERS_SLOT_NAME.clone()),
];
bridge_component(bridge_storage_slots)
Expand Down
Loading
Loading