From f7ce4989ab4ddf2b53842ea788e392ee549c0ce1 Mon Sep 17 00:00:00 2001 From: onurinanc Date: Fri, 3 Apr 2026 18:18:10 +0200 Subject: [PATCH 1/6] add pausable component --- .../account_components/utils/pausable.masm | 9 + .../asm/standards/utils/pausable.masm | 189 ++++++++++ .../src/account/components/mod.rs | 14 + crates/miden-standards/src/account/mod.rs | 1 + .../miden-standards/src/account/pausable.rs | 148 ++++++++ crates/miden-testing/tests/scripts/mod.rs | 1 + .../miden-testing/tests/scripts/pausable.rs | 345 ++++++++++++++++++ 7 files changed, 707 insertions(+) create mode 100644 crates/miden-standards/asm/account_components/utils/pausable.masm create mode 100644 crates/miden-standards/asm/standards/utils/pausable.masm create mode 100644 crates/miden-standards/src/account/pausable.rs create mode 100644 crates/miden-testing/tests/scripts/pausable.rs diff --git a/crates/miden-standards/asm/account_components/utils/pausable.masm b/crates/miden-standards/asm/account_components/utils/pausable.masm new file mode 100644 index 0000000000..d291358279 --- /dev/null +++ b/crates/miden-standards/asm/account_components/utils/pausable.masm @@ -0,0 +1,9 @@ +# The MASM code of the Pausable account component. +# +# Re-exports pause/unpause and asset callbacks from `miden::standards::utils::pausable`. +# Authorization for pause/unpause must be enforced by composing with access-control components. + +pub use ::miden::standards::utils::pausable::pause +pub use ::miden::standards::utils::pausable::unpause +pub use ::miden::standards::utils::pausable::on_before_asset_added_to_account +pub use ::miden::standards::utils::pausable::on_before_asset_added_to_note diff --git a/crates/miden-standards/asm/standards/utils/pausable.masm b/crates/miden-standards/asm/standards/utils/pausable.masm new file mode 100644 index 0000000000..803e42ac23 --- /dev/null +++ b/crates/miden-standards/asm/standards/utils/pausable.masm @@ -0,0 +1,189 @@ +# miden::standards::utils::pausable +# +# Minimal pause flag storage and helpers. Pause/unpause procedures do not perform authorization. +# Compose with ownable2step or role-based access control in a higher layer. +# +# Asset callbacks enforce "not paused" when the issuing faucet has callbacks enabled on the asset. + +use miden::core::word +use miden::protocol::active_account +use miden::protocol::native_account + +# CONSTANTS +# ================================================================================================ + +# The slot where the paused flag is stored as a single word. +# Unpaused: [0, 0, 0, 0]. Paused: [1, 0, 0, 0]. +const IS_PAUSED_CONFIG_SLOT = word("miden::standards::utils::pausable::is_paused_config") + +const PAUSED_WORD = [1, 0, 0, 0] + +const UNPAUSED_WORD = [0, 0, 0, 0] + +# ERRORS +# ================================================================================================ + +const ERR_PAUSABLE_ENFORCED_PAUSE = "the contract is paused" + +const ERR_PAUSABLE_EXPECTED_PAUSE = "the contract is not paused" + +# PUBLIC INTERFACE +# ================================================================================================ + +#! Sets the paused flag. Fails if already paused. +#! +#! This procedure does not verify the caller. Wrap with access control in the account component +#! composition if only privileged accounts should pause. +#! +#! Inputs: [pad(16)] +#! Outputs: [pad(16)] +#! +#! Invocation: call +pub proc pause + exec.assert_not_paused + # => [pad(16)] + + push.PAUSED_WORD + # => [1, 0, 0, 0, pad(16)] + + push.IS_PAUSED_CONFIG_SLOT[0..2] + # => [slot_suffix, slot_prefix, 1, 0, 0, 0, pad(16)] + + exec.native_account::set_item + # => [OLD_WORD, pad(16)] + + dropw + # => [pad(16)] +end + +#! Clears the paused flag. Fails if not paused. +#! +#! This procedure does not verify the caller. +#! +#! Inputs: [pad(16)] +#! Outputs: [pad(16)] +#! +#! Invocation: call +pub proc unpause + exec.assert_paused + # => [pad(16)] + + push.UNPAUSED_WORD + # => [0, 0, 0, 0, pad(16)] + + push.IS_PAUSED_CONFIG_SLOT[0..2] + # => [slot_suffix, slot_prefix, 0, 0, 0, 0, pad(16)] + + exec.native_account::set_item + # => [OLD_WORD, pad(16)] + + dropw + # => [pad(16)] +end + +#! Requires the contract to be unpaused (storage word is the zero word). +#! +#! Reads [`IS_PAUSED_CONFIG_SLOT`] on the active account via [`load_is_paused_word`], then uses +#! [`word::eqz`]. If the stored word is non-zero (paused), panics with +#! [`ERR_PAUSABLE_ENFORCED_PAUSE`]. +#! +#! Use from other modules or transaction scripts to guard logic that must not run while paused. +#! In asset callback foreign context, the active account is the issuing faucet. +#! +#! Inputs: [] +#! Outputs: [] +#! +#! Panics if: +#! - the paused-state word is not the zero word. +#! +#! Invocation: exec +pub proc assert_not_paused + exec.load_is_paused_word + # => [w0, w1, w2, w3] + + exec.word::eqz + # => [is_unpaused] + + assert.err=ERR_PAUSABLE_ENFORCED_PAUSE + # => [] +end + +#! Requires the contract to be paused (storage word is not the zero word). +#! +#! Reads [`IS_PAUSED_CONFIG_SLOT`] on the active account, then [`word::eqz`] and inverts. If the +#! stored word is zero (unpaused), panics with [`ERR_PAUSABLE_EXPECTED_PAUSE`]. +#! +#! Typical use: guard `unpause` so clearing the flag only happens from a paused state. +#! +#! Inputs: [] +#! Outputs: [] +#! +#! Panics if: +#! - the paused-state word is the zero word. +#! +#! Invocation: exec +pub proc assert_paused + exec.load_is_paused_word + # => [w0, w1, w2, w3] + + exec.word::eqz not + # => [is_paused] + + assert.err=ERR_PAUSABLE_EXPECTED_PAUSE + # => [] +end + +# CALLBACKS +# ================================================================================================ + +#! Callback when a callbacks-enabled asset is added to an account vault. +#! +#! Panics if this faucet account is paused (reads `IS_PAUSED_CONFIG_SLOT` on the active account, +#! which is the issuing faucet in the callback foreign context). +#! +#! Inputs: [ASSET_KEY, ASSET_VALUE, pad(8)] +#! Outputs: [ASSET_VALUE, pad(12)] +#! +#! Invocation: call +pub proc on_before_asset_added_to_account + exec.assert_not_paused + # => [ASSET_KEY, ASSET_VALUE, pad(8)] + + dropw + # => [ASSET_VALUE, pad(12)] +end + +#! Callback when a callbacks-enabled asset is added to an output note. +#! +#! Panics if this faucet account is paused. +#! +#! Inputs: [ASSET_KEY, ASSET_VALUE, note_idx, pad(7)] +#! Outputs: [ASSET_VALUE, pad(12)] +#! +#! Invocation: call +pub proc on_before_asset_added_to_note + exec.assert_not_paused + # => [ASSET_KEY, ASSET_VALUE, note_idx, pad(7)] + + dropw + # => [ASSET_VALUE, note_idx, pad(7)] +end + +# HELPER PROCEDURES +# ================================================================================================ + +#! Loads the paused-state word from storage. +#! +#! Inputs: [] +#! Outputs: [w0, w1, w2, w3] +#! +#! Where: +#! - w0..w3 are the four felts of the value stored at [`IS_PAUSED_CONFIG_SLOT`] on the active +#! account. +#! +#! Invocation: exec +proc load_is_paused_word + push.IS_PAUSED_CONFIG_SLOT[0..2] + exec.active_account::get_item + # => [w0, w1, w2, w3] +end diff --git a/crates/miden-standards/src/account/components/mod.rs b/crates/miden-standards/src/account/components/mod.rs index a14d3ce523..667eedcb73 100644 --- a/crates/miden-standards/src/account/components/mod.rs +++ b/crates/miden-standards/src/account/components/mod.rs @@ -97,6 +97,15 @@ static NETWORK_FUNGIBLE_FAUCET_LIBRARY: LazyLock = LazyLock::new(|| { Library::read_from_bytes(bytes).expect("Shipped Network Fungible Faucet library is well-formed") }); +// Initialize the Pausable library only once. +static PAUSABLE_LIBRARY: LazyLock = LazyLock::new(|| { + let bytes = include_bytes!(concat!( + env!("OUT_DIR"), + "/assets/account_components/utils/pausable.masl" + )); + Library::read_from_bytes(bytes).expect("Shipped Pausable library is well-formed") +}); + // Initialize the Mint Policy Owner Controlled library only once. static MINT_POLICY_OWNER_CONTROLLED_LIBRARY: LazyLock = LazyLock::new(|| { let bytes = include_bytes!(concat!( @@ -140,6 +149,11 @@ pub fn network_fungible_faucet_library() -> Library { NETWORK_FUNGIBLE_FAUCET_LIBRARY.clone() } +/// Returns the Pausable component library. +pub fn pausable_library() -> Library { + PAUSABLE_LIBRARY.clone() +} + /// Returns the Mint Policy Owner Controlled Library. pub fn owner_controlled_library() -> Library { MINT_POLICY_OWNER_CONTROLLED_LIBRARY.clone() diff --git a/crates/miden-standards/src/account/mod.rs b/crates/miden-standards/src/account/mod.rs index 9580c185f8..b48f08d755 100644 --- a/crates/miden-standards/src/account/mod.rs +++ b/crates/miden-standards/src/account/mod.rs @@ -7,6 +7,7 @@ pub mod faucets; pub mod interface; pub mod metadata; pub mod mint_policies; +pub mod pausable; pub mod wallets; pub use metadata::AccountBuilderSchemaCommitmentExt; diff --git a/crates/miden-standards/src/account/pausable.rs b/crates/miden-standards/src/account/pausable.rs new file mode 100644 index 0000000000..46d1d588cc --- /dev/null +++ b/crates/miden-standards/src/account/pausable.rs @@ -0,0 +1,148 @@ +use alloc::vec::Vec; + +use miden_protocol::account::component::{ + AccountComponentMetadata, FeltSchema, StorageSchema, StorageSlotSchema, +}; +use miden_protocol::account::{AccountComponent, AccountType, StorageSlot, StorageSlotName}; +use miden_protocol::asset::AssetCallbacks; +use miden_protocol::utils::sync::LazyLock; +use miden_protocol::{Felt, Word}; + +use crate::account::components::pausable_library; +use crate::procedure_digest; + +// PAUSABLE ACCOUNT COMPONENT +// ================================================================================================ + +static IS_PAUSED_CONFIG_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::utils::pausable::is_paused_config") + .expect("storage slot name should be valid") +}); + +procedure_digest!( + PAUSABLE_PAUSE, + Pausable::NAME, + Pausable::PAUSE_PROC_NAME, + pausable_library +); + +procedure_digest!( + PAUSABLE_UNPAUSE, + Pausable::NAME, + Pausable::UNPAUSE_PROC_NAME, + pausable_library +); + +procedure_digest!( + PAUSABLE_ON_BEFORE_ASSET_ADDED_TO_ACCOUNT, + Pausable::NAME, + Pausable::ON_BEFORE_ASSET_ADDED_TO_ACCOUNT_PROC_NAME, + pausable_library +); + +procedure_digest!( + PAUSABLE_ON_BEFORE_ASSET_ADDED_TO_NOTE, + Pausable::NAME, + Pausable::ON_BEFORE_ASSET_ADDED_TO_NOTE_PROC_NAME, + pausable_library +); + +/// Account component that stores a pause flag and registers asset callbacks that reject transfers +/// while paused. +/// +/// `pause` and `unpause` do not authenticate the caller; compose with access-control components +/// for production deployments. +/// +/// ## Storage +/// +/// - [`Self::is_paused_config_slot()`]: single word; all zeros means unpaused, `[1,0,0,0]` means +/// paused (see MASM `miden::standards::utils::pausable`). +/// - Protocol callback slots from [`AssetCallbacks`] when built via [`From`]. +pub struct Pausable; + +impl Pausable { + /// Component library path (merged account module name). + pub const NAME: &'static str = "miden::standards::components::utils::pausable"; + + const PAUSE_PROC_NAME: &'static str = "pause"; + const UNPAUSE_PROC_NAME: &'static str = "unpause"; + const ON_BEFORE_ASSET_ADDED_TO_ACCOUNT_PROC_NAME: &'static str = + "on_before_asset_added_to_account"; + const ON_BEFORE_ASSET_ADDED_TO_NOTE_PROC_NAME: &'static str = "on_before_asset_added_to_note"; + + /// Storage slot name for the pause flag word. + pub fn is_paused_config_slot() -> &'static StorageSlotName { + &IS_PAUSED_CONFIG_SLOT_NAME + } + + /// Schema entry for the pause flag slot (documentation / tooling). + pub fn is_paused_slot_schema() -> (StorageSlotName, StorageSlotSchema) { + ( + Self::is_paused_config_slot().clone(), + StorageSlotSchema::value( + "Pause flag word; zero is unpaused, canonical paused encoding is [1,0,0,0]", + [ + FeltSchema::felt("w0").with_default(Felt::ZERO), + FeltSchema::felt("w1").with_default(Felt::ZERO), + FeltSchema::felt("w2").with_default(Felt::ZERO), + FeltSchema::felt("w3").with_default(Felt::ZERO), + ], + ), + ) + } + + /// Metadata for accounts that include this component (faucet types that may issue + /// callback-enabled assets). + pub fn component_metadata() -> AccountComponentMetadata { + let storage_schema = + StorageSchema::new([Self::is_paused_slot_schema()]).expect("storage schema should be valid"); + + AccountComponentMetadata::new( + Self::NAME, + [AccountType::FungibleFaucet, AccountType::NonFungibleFaucet], + ) + .with_description( + "Pausable component: pause/unpause and on_before_asset_added callbacks without auth", + ) + .with_storage_schema(storage_schema) + } + + pub fn pause_digest() -> Word { + *PAUSABLE_PAUSE + } + + pub fn unpause_digest() -> Word { + *PAUSABLE_UNPAUSE + } + + pub fn on_before_asset_added_to_account_digest() -> Word { + *PAUSABLE_ON_BEFORE_ASSET_ADDED_TO_ACCOUNT + } + + pub fn on_before_asset_added_to_note_digest() -> Word { + *PAUSABLE_ON_BEFORE_ASSET_ADDED_TO_NOTE + } +} + +impl From for AccountComponent { + fn from(_: Pausable) -> Self { + let is_paused_slot = StorageSlot::with_value( + Pausable::is_paused_config_slot().clone(), + Word::default(), + ); + let callback_slots = AssetCallbacks::new() + .on_before_asset_added_to_account(Pausable::on_before_asset_added_to_account_digest()) + .on_before_asset_added_to_note(Pausable::on_before_asset_added_to_note_digest()) + .into_storage_slots(); + + let mut storage_slots = Vec::with_capacity(1 + callback_slots.len()); + storage_slots.push(is_paused_slot); + storage_slots.extend(callback_slots); + + let metadata = Pausable::component_metadata(); + + AccountComponent::new(pausable_library(), storage_slots, metadata).expect( + "pausable component should satisfy the requirements of a valid account component", + ) + } +} diff --git a/crates/miden-testing/tests/scripts/mod.rs b/crates/miden-testing/tests/scripts/mod.rs index 8d15402744..3506254302 100644 --- a/crates/miden-testing/tests/scripts/mod.rs +++ b/crates/miden-testing/tests/scripts/mod.rs @@ -1,6 +1,7 @@ mod faucet; mod fee; mod ownable2step; +mod pausable; mod p2id; mod p2ide; mod send_note; diff --git a/crates/miden-testing/tests/scripts/pausable.rs b/crates/miden-testing/tests/scripts/pausable.rs new file mode 100644 index 0000000000..f661e00685 --- /dev/null +++ b/crates/miden-testing/tests/scripts/pausable.rs @@ -0,0 +1,345 @@ +//! Tests for [`miden_standards::account::pausable::Pausable`] asset callbacks and pause/unpause scripts. + +extern crate alloc; + +use miden_protocol::account::auth::AuthScheme; +use miden_protocol::account::{ + Account, AccountBuilder, AccountComponent, AccountId, AccountStorageMode, AccountType, +}; +use miden_protocol::asset::{ + Asset, AssetCallbackFlag, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails, +}; +use miden_protocol::errors::MasmError; +use miden_protocol::note::{NoteTag, NoteType}; +use miden_protocol::{Felt, Word}; +use miden_standards::account::faucets::BasicFungibleFaucet; +use miden_standards::account::pausable::Pausable; +use miden_standards::code_builder::CodeBuilder; +use miden_standards::testing::account_component::MockFaucetComponent; +use miden_testing::{AccountState, Auth, MockChain, MockChainBuilder, assert_transaction_executor_error}; + +const ERR_PAUSABLE_ENFORCED_PAUSE: MasmError = MasmError::from_static_str("the contract is paused"); + +const ERR_PAUSABLE_EXPECTED_PAUSE: MasmError = MasmError::from_static_str("the contract is not paused"); + +fn add_faucet_with_pausable(builder: &mut MockChainBuilder) -> anyhow::Result { + let basic_faucet = BasicFungibleFaucet::new("SYM".try_into()?, 8, Felt::new(1_000_000))?; + + let account_builder = AccountBuilder::new([43u8; 32]) + .storage_mode(AccountStorageMode::Public) + .account_type(AccountType::FungibleFaucet) + .with_component(basic_faucet) + .with_component(Pausable); + + builder.add_account_from_builder( + Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + }, + account_builder, + AccountState::Exists, + ) +} + +fn add_faucet_with_pausable_for_account_type( + builder: &mut MockChainBuilder, + account_type: AccountType, +) -> anyhow::Result { + if !account_type.is_faucet() { + anyhow::bail!("account type must be a faucet"); + } + + let faucet_component: AccountComponent = match account_type { + AccountType::FungibleFaucet => BasicFungibleFaucet::new("SYM".try_into()?, 8, Felt::new(1_000_000))? + .into(), + AccountType::NonFungibleFaucet => MockFaucetComponent.into(), + _ => anyhow::bail!("pausable tests only use fungible or non-fungible faucet account types"), + }; + + let account_builder = AccountBuilder::new([43u8; 32]) + .storage_mode(AccountStorageMode::Public) + .account_type(account_type) + .with_component(faucet_component) + .with_component(Pausable); + + builder.add_account_from_builder( + Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + }, + account_builder, + AccountState::Exists, + ) +} + +async fn execute_faucet_pause(mock_chain: &mut MockChain, faucet_id: AccountId) -> anyhow::Result<()> { + let pause_script = r#" + begin + padw padw push.0 + call.::miden::standards::utils::pausable::pause + dropw dropw dropw dropw + end + "#; + let tx_script = CodeBuilder::default().compile_tx_script(pause_script)?; + let executed = mock_chain + .build_tx_context(faucet_id, &[], &[])? + .tx_script(tx_script) + .build()? + .execute() + .await?; + mock_chain.add_pending_executed_transaction(&executed)?; + mock_chain.prove_next_block()?; + Ok(()) +} + +async fn execute_faucet_unpause( + mock_chain: &mut MockChain, + faucet_id: AccountId, +) -> anyhow::Result<()> { + let unpause_script = r#" + begin + padw padw push.0 + call.::miden::standards::utils::pausable::unpause + dropw dropw dropw dropw + end + "#; + let tx_script = CodeBuilder::default().compile_tx_script(unpause_script)?; + let executed = mock_chain + .build_tx_context(faucet_id, &[], &[])? + .tx_script(tx_script) + .build()? + .execute() + .await?; + mock_chain.add_pending_executed_transaction(&executed)?; + mock_chain.prove_next_block()?; + Ok(()) +} + +#[rstest::rstest] +#[case::fungible( + AccountType::FungibleFaucet, + |faucet_id| { + Ok(FungibleAsset::new(faucet_id, 100)?.with_callbacks(AssetCallbackFlag::Enabled).into()) + } +)] +#[case::non_fungible( + AccountType::NonFungibleFaucet, + |faucet_id| { + let details = NonFungibleAssetDetails::new(faucet_id, vec![1, 2, 3, 4])?; + Ok(NonFungibleAsset::new(&details)?.with_callbacks(AssetCallbackFlag::Enabled).into()) + } +)] +#[tokio::test] +async fn pausable_receive_asset_succeeds_when_unpaused( + #[case] account_type: AccountType, + #[case] create_asset: impl FnOnce(AccountId) -> anyhow::Result, +) -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let target_account = builder.add_existing_wallet(Auth::IncrNonce)?; + + let faucet = add_faucet_with_pausable_for_account_type(&mut builder, account_type)?; + + let note = builder.add_p2id_note( + faucet.id(), + target_account.id(), + &[create_asset(faucet.id())?], + NoteType::Public, + )?; + + let mut mock_chain = builder.build()?; + mock_chain.prove_next_block()?; + + let faucet_inputs = mock_chain.get_foreign_account_inputs(faucet.id())?; + + mock_chain + .build_tx_context(target_account.id(), &[note.id()], &[])? + .foreign_accounts(vec![faucet_inputs]) + .build()? + .execute() + .await?; + + Ok(()) +} + +#[rstest::rstest] +#[case::fungible( + AccountType::FungibleFaucet, + |faucet_id| { + Ok(FungibleAsset::new(faucet_id, 100)?.with_callbacks(AssetCallbackFlag::Enabled).into()) + } +)] +#[case::non_fungible( + AccountType::NonFungibleFaucet, + |faucet_id| { + let details = NonFungibleAssetDetails::new(faucet_id, vec![1, 2, 3, 4])?; + Ok(NonFungibleAsset::new(&details)?.with_callbacks(AssetCallbackFlag::Enabled).into()) + } +)] +#[tokio::test] +async fn pausable_receive_asset_fails_when_paused( + #[case] account_type: AccountType, + #[case] create_asset: impl FnOnce(AccountId) -> anyhow::Result, +) -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let target_account = builder.add_existing_wallet(Auth::IncrNonce)?; + + let faucet = add_faucet_with_pausable_for_account_type(&mut builder, account_type)?; + + let note = builder.add_p2id_note( + faucet.id(), + target_account.id(), + &[create_asset(faucet.id())?], + NoteType::Public, + )?; + + let mut mock_chain = builder.build()?; + mock_chain.prove_next_block()?; + + execute_faucet_pause(&mut mock_chain, faucet.id()).await?; + + let faucet_inputs = mock_chain.get_foreign_account_inputs(faucet.id())?; + + let result = mock_chain + .build_tx_context(target_account.id(), &[note.id()], &[])? + .foreign_accounts(vec![faucet_inputs]) + .build()? + .execute() + .await; + + assert_transaction_executor_error!(result, ERR_PAUSABLE_ENFORCED_PAUSE); + + Ok(()) +} + +#[rstest::rstest] +#[case::fungible( + AccountType::FungibleFaucet, + |faucet_id| { + Ok(FungibleAsset::new(faucet_id, 100)?.with_callbacks(AssetCallbackFlag::Enabled).into()) + } +)] +#[case::non_fungible( + AccountType::NonFungibleFaucet, + |faucet_id| { + let details = NonFungibleAssetDetails::new(faucet_id, vec![1, 2, 3, 4])?; + Ok(NonFungibleAsset::new(&details)?.with_callbacks(AssetCallbackFlag::Enabled).into()) + } +)] +#[tokio::test] +async fn pausable_add_asset_to_note_fails_when_paused( + #[case] account_type: AccountType, + #[case] create_asset: impl FnOnce(AccountId) -> anyhow::Result, +) -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let target_account = builder.add_existing_wallet(Auth::IncrNonce)?; + + let faucet = add_faucet_with_pausable_for_account_type(&mut builder, account_type)?; + + let asset = create_asset(faucet.id())?; + + let mut mock_chain = builder.build()?; + mock_chain.prove_next_block()?; + + execute_faucet_pause(&mut mock_chain, faucet.id()).await?; + + let recipient = Word::from([0u32, 1, 2, 3]); + let script_code = format!( + r#" + use miden::protocol::output_note + + begin + push.{recipient} + push.{note_type} + push.{tag} + exec.output_note::create + + push.{asset_value} + push.{asset_key} + exec.output_note::add_asset + end + "#, + recipient = recipient, + note_type = NoteType::Private as u8, + tag = NoteTag::default(), + asset_value = asset.to_value_word(), + asset_key = asset.to_key_word(), + ); + + let tx_script = CodeBuilder::with_mock_libraries().compile_tx_script(&script_code)?; + + let faucet_inputs = mock_chain.get_foreign_account_inputs(faucet.id())?; + + let result = mock_chain + .build_tx_context(target_account.id(), &[], &[])? + .tx_script(tx_script) + .foreign_accounts(vec![faucet_inputs]) + .build()? + .execute() + .await; + + assert_transaction_executor_error!(result, ERR_PAUSABLE_ENFORCED_PAUSE); + + Ok(()) +} + +#[tokio::test] +async fn pausable_pause_then_unpause_then_receive_succeeds() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let target_account = builder.add_existing_wallet(Auth::IncrNonce)?; + let faucet = add_faucet_with_pausable(&mut builder)?; + + let amount: u64 = 50; + let fungible_asset = + FungibleAsset::new(faucet.id(), amount)?.with_callbacks(AssetCallbackFlag::Enabled); + let note = builder.add_p2id_note( + faucet.id(), + target_account.id(), + &[Asset::Fungible(fungible_asset)], + NoteType::Public, + )?; + + let mut mock_chain = builder.build()?; + mock_chain.prove_next_block()?; + + execute_faucet_pause(&mut mock_chain, faucet.id()).await?; + execute_faucet_unpause(&mut mock_chain, faucet.id()).await?; + + let faucet_inputs = mock_chain.get_foreign_account_inputs(faucet.id())?; + + mock_chain + .build_tx_context(target_account.id(), &[note.id()], &[])? + .foreign_accounts(vec![faucet_inputs]) + .build()? + .execute() + .await?; + + Ok(()) +} + +#[tokio::test] +async fn pausable_unpause_while_unpaused_fails() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let _wallet = builder.add_existing_wallet(Auth::IncrNonce)?; + let faucet = add_faucet_with_pausable(&mut builder)?; + + let mut mock_chain = builder.build()?; + mock_chain.prove_next_block()?; + + let unpause_script = r#" + begin + padw padw push.0 + call.::miden::standards::utils::pausable::unpause + dropw dropw dropw dropw + end + "#; + let tx_script = CodeBuilder::default().compile_tx_script(unpause_script)?; + + let result = mock_chain + .build_tx_context(faucet.id(), &[], &[])? + .tx_script(tx_script) + .build()? + .execute() + .await; + + assert_transaction_executor_error!(result, ERR_PAUSABLE_EXPECTED_PAUSE); + + Ok(()) +} From 66c9d47967e8bf60a9070031ebf7ff96a0f5b129 Mon Sep 17 00:00:00 2001 From: onurinanc Date: Mon, 20 Apr 2026 11:59:20 +0200 Subject: [PATCH 2/6] add Pausable for composition component wrappers --- .../account_components/utils/pausable.masm | 5 +- .../asm/standards/utils/pausable.masm | 50 +++++--- .../src/account/components/mod.rs | 6 +- .../miden-standards/src/account/pausable.rs | 108 +++++++++++------- crates/miden-testing/tests/scripts/mod.rs | 2 +- .../miden-testing/tests/scripts/pausable.rs | 37 ++++-- 6 files changed, 140 insertions(+), 68 deletions(-) diff --git a/crates/miden-standards/asm/account_components/utils/pausable.masm b/crates/miden-standards/asm/account_components/utils/pausable.masm index d291358279..97f52ba1a9 100644 --- a/crates/miden-standards/asm/account_components/utils/pausable.masm +++ b/crates/miden-standards/asm/account_components/utils/pausable.masm @@ -1,8 +1,9 @@ # The MASM code of the Pausable account component. # -# Re-exports pause/unpause and asset callbacks from `miden::standards::utils::pausable`. -# Authorization for pause/unpause must be enforced by composing with access-control components. +# NOTE: This is a temporary no-auth variant of the component for testing purposes. +# It is intended to be replaced by dedicated owner and role-based access control wrappers. +pub use ::miden::standards::utils::pausable::is_paused pub use ::miden::standards::utils::pausable::pause pub use ::miden::standards::utils::pausable::unpause pub use ::miden::standards::utils::pausable::on_before_asset_added_to_account diff --git a/crates/miden-standards/asm/standards/utils/pausable.masm b/crates/miden-standards/asm/standards/utils/pausable.masm index 803e42ac23..e8475b76b3 100644 --- a/crates/miden-standards/asm/standards/utils/pausable.masm +++ b/crates/miden-standards/asm/standards/utils/pausable.masm @@ -14,7 +14,7 @@ use miden::protocol::native_account # The slot where the paused flag is stored as a single word. # Unpaused: [0, 0, 0, 0]. Paused: [1, 0, 0, 0]. -const IS_PAUSED_CONFIG_SLOT = word("miden::standards::utils::pausable::is_paused_config") +const IS_PAUSED_SLOT = word("miden::standards::utils::pausable::is_paused") const PAUSED_WORD = [1, 0, 0, 0] @@ -30,6 +30,26 @@ const ERR_PAUSABLE_EXPECTED_PAUSE = "the contract is not paused" # PUBLIC INTERFACE # ================================================================================================ +#! Returns whether the account is currently paused. +#! +#! Reads [`IS_PAUSED_SLOT`] on the active account and returns `1` if the stored word is non-zero +#! (paused) or `0` if it is the zero word (unpaused). +#! +#! Inputs: [pad(16)] +#! Outputs: [is_paused, pad(15)] +#! +#! Invocation: call +pub proc is_paused + exec.load_is_paused_word + # => [is_paused, 0, 0, 0, pad(16)] + + exec.word::eqz not + # => [is_paused, pad(16)] + + swap drop + # => [is_paused, pad(15)] +end + #! Sets the paused flag. Fails if already paused. #! #! This procedure does not verify the caller. Wrap with access control in the account component @@ -46,7 +66,7 @@ pub proc pause push.PAUSED_WORD # => [1, 0, 0, 0, pad(16)] - push.IS_PAUSED_CONFIG_SLOT[0..2] + push.IS_PAUSED_SLOT[0..2] # => [slot_suffix, slot_prefix, 1, 0, 0, 0, pad(16)] exec.native_account::set_item @@ -71,7 +91,7 @@ pub proc unpause push.UNPAUSED_WORD # => [0, 0, 0, 0, pad(16)] - push.IS_PAUSED_CONFIG_SLOT[0..2] + push.IS_PAUSED_SLOT[0..2] # => [slot_suffix, slot_prefix, 0, 0, 0, 0, pad(16)] exec.native_account::set_item @@ -83,7 +103,7 @@ end #! Requires the contract to be unpaused (storage word is the zero word). #! -#! Reads [`IS_PAUSED_CONFIG_SLOT`] on the active account via [`load_is_paused_word`], then uses +#! Reads [`IS_PAUSED_SLOT`] on the active account via [`load_is_paused_word`], then uses #! [`word::eqz`]. If the stored word is non-zero (paused), panics with #! [`ERR_PAUSABLE_ENFORCED_PAUSE`]. #! @@ -99,7 +119,7 @@ end #! Invocation: exec pub proc assert_not_paused exec.load_is_paused_word - # => [w0, w1, w2, w3] + # => [is_paused, 0, 0, 0] exec.word::eqz # => [is_unpaused] @@ -110,7 +130,7 @@ end #! Requires the contract to be paused (storage word is not the zero word). #! -#! Reads [`IS_PAUSED_CONFIG_SLOT`] on the active account, then [`word::eqz`] and inverts. If the +#! Reads [`IS_PAUSED_SLOT`] on the active account, then [`word::eqz`] and inverts. If the #! stored word is zero (unpaused), panics with [`ERR_PAUSABLE_EXPECTED_PAUSE`]. #! #! Typical use: guard `unpause` so clearing the flag only happens from a paused state. @@ -124,7 +144,7 @@ end #! Invocation: exec pub proc assert_paused exec.load_is_paused_word - # => [w0, w1, w2, w3] + # => [is_paused, 0, 0, 0] exec.word::eqz not # => [is_paused] @@ -138,7 +158,7 @@ end #! Callback when a callbacks-enabled asset is added to an account vault. #! -#! Panics if this faucet account is paused (reads `IS_PAUSED_CONFIG_SLOT` on the active account, +#! Panics if this faucet account is paused (reads `IS_PAUSED_SLOT` on the active account, #! which is the issuing faucet in the callback foreign context). #! #! Inputs: [ASSET_KEY, ASSET_VALUE, pad(8)] @@ -172,18 +192,20 @@ end # HELPER PROCEDURES # ================================================================================================ -#! Loads the paused-state word from storage. +#! Loads the paused state word from storage. #! #! Inputs: [] -#! Outputs: [w0, w1, w2, w3] +#! Outputs: [is_paused, 0, 0, 0] #! #! Where: -#! - w0..w3 are the four felts of the value stored at [`IS_PAUSED_CONFIG_SLOT`] on the active -#! account. +#! - is_paused is the flag felt at the top of [`IS_PAUSED_SLOT`] (`1` when paused, `0` when +#! unpaused). +#! - The remaining three felts of the word are always zero by construction (see [`PAUSED_WORD`] +#! and [`UNPAUSED_WORD`]). #! #! Invocation: exec proc load_is_paused_word - push.IS_PAUSED_CONFIG_SLOT[0..2] + push.IS_PAUSED_SLOT[0..2] exec.active_account::get_item - # => [w0, w1, w2, w3] + # => [is_paused, 0, 0, 0] end diff --git a/crates/miden-standards/src/account/components/mod.rs b/crates/miden-standards/src/account/components/mod.rs index 667eedcb73..9928c6833f 100644 --- a/crates/miden-standards/src/account/components/mod.rs +++ b/crates/miden-standards/src/account/components/mod.rs @@ -99,10 +99,8 @@ static NETWORK_FUNGIBLE_FAUCET_LIBRARY: LazyLock = LazyLock::new(|| { // Initialize the Pausable library only once. static PAUSABLE_LIBRARY: LazyLock = LazyLock::new(|| { - let bytes = include_bytes!(concat!( - env!("OUT_DIR"), - "/assets/account_components/utils/pausable.masl" - )); + let bytes = + include_bytes!(concat!(env!("OUT_DIR"), "/assets/account_components/utils/pausable.masl")); Library::read_from_bytes(bytes).expect("Shipped Pausable library is well-formed") }); diff --git a/crates/miden-standards/src/account/pausable.rs b/crates/miden-standards/src/account/pausable.rs index 46d1d588cc..2d6fef5155 100644 --- a/crates/miden-standards/src/account/pausable.rs +++ b/crates/miden-standards/src/account/pausable.rs @@ -1,7 +1,10 @@ use alloc::vec::Vec; use miden_protocol::account::component::{ - AccountComponentMetadata, FeltSchema, StorageSchema, StorageSlotSchema, + AccountComponentMetadata, + FeltSchema, + StorageSchema, + StorageSlotSchema, }; use miden_protocol::account::{AccountComponent, AccountType, StorageSlot, StorageSlotName}; use miden_protocol::asset::AssetCallbacks; @@ -14,24 +17,21 @@ use crate::procedure_digest; // PAUSABLE ACCOUNT COMPONENT // ================================================================================================ -static IS_PAUSED_CONFIG_SLOT_NAME: LazyLock = LazyLock::new(|| { - StorageSlotName::new("miden::standards::utils::pausable::is_paused_config") +static IS_PAUSED_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::utils::pausable::is_paused") .expect("storage slot name should be valid") }); procedure_digest!( - PAUSABLE_PAUSE, + PAUSABLE_IS_PAUSED, Pausable::NAME, - Pausable::PAUSE_PROC_NAME, + Pausable::IS_PAUSED_PROC_NAME, pausable_library ); -procedure_digest!( - PAUSABLE_UNPAUSE, - Pausable::NAME, - Pausable::UNPAUSE_PROC_NAME, - pausable_library -); +procedure_digest!(PAUSABLE_PAUSE, Pausable::NAME, Pausable::PAUSE_PROC_NAME, pausable_library); + +procedure_digest!(PAUSABLE_UNPAUSE, Pausable::NAME, Pausable::UNPAUSE_PROC_NAME, pausable_library); procedure_digest!( PAUSABLE_ON_BEFORE_ASSET_ADDED_TO_ACCOUNT, @@ -50,35 +50,78 @@ procedure_digest!( /// Account component that stores a pause flag and registers asset callbacks that reject transfers /// while paused. /// -/// `pause` and `unpause` do not authenticate the caller; compose with access-control components -/// for production deployments. +/// `pause` and `unpause` do not authenticate the caller — this is an intentional choice: +/// the core mechanism is kept without access control so that auth wrappers (e.g. +/// `PausableOwnerControlled` backed by [`Ownable2Step`] or `PausableRoleControlled` backed by +/// role-based access control) can be implemented on top without duplicating the pause/unpause +/// bodies. Compose this component with access control components. /// /// ## Storage /// -/// - [`Self::is_paused_config_slot()`]: single word; all zeros means unpaused, `[1,0,0,0]` means -/// paused (see MASM `miden::standards::utils::pausable`). -/// - Protocol callback slots from [`AssetCallbacks`] when built via [`From`]. +/// - [`Self::is_paused_slot()`]: single word; all zeros means unpaused, `[1,0,0,0]` means paused +/// (see MASM `miden::standards::utils::pausable`). +/// - Protocol callback slots from [`AssetCallbacks`] registered by every constructor. +#[derive(Debug, Clone, Copy, Default)] pub struct Pausable; impl Pausable { /// Component library path (merged account module name). pub const NAME: &'static str = "miden::standards::components::utils::pausable"; + const IS_PAUSED_PROC_NAME: &'static str = "is_paused"; const PAUSE_PROC_NAME: &'static str = "pause"; const UNPAUSE_PROC_NAME: &'static str = "unpause"; const ON_BEFORE_ASSET_ADDED_TO_ACCOUNT_PROC_NAME: &'static str = "on_before_asset_added_to_account"; const ON_BEFORE_ASSET_ADDED_TO_NOTE_PROC_NAME: &'static str = "on_before_asset_added_to_note"; + /// Builds a Pausable [`AccountComponent`] with the initial paused state or unpaused state. + pub fn new(is_paused: bool) -> AccountComponent { + let initial_word = if is_paused { + Word::from([1u32, 0, 0, 0]) + } else { + Word::default() + }; + + let is_paused_slot = StorageSlot::with_value(Self::is_paused_slot().clone(), initial_word); + let callback_slots = AssetCallbacks::new() + .on_before_asset_added_to_account(Self::on_before_asset_added_to_account_digest()) + .on_before_asset_added_to_note(Self::on_before_asset_added_to_note_digest()) + .into_storage_slots(); + + let mut storage_slots = Vec::with_capacity(1 + callback_slots.len()); + storage_slots.push(is_paused_slot); + storage_slots.extend(callback_slots); + + let metadata = Self::component_metadata(); + + AccountComponent::new(pausable_library(), storage_slots, metadata).expect( + "pausable component should satisfy the requirements of a valid account component", + ) + } + + /// Builds a Pausable [`AccountComponent`] that starts in the paused state. + pub fn paused() -> AccountComponent { + Self::new(true) + } + + /// Builds a Pausable [`AccountComponent`] that starts in the unpaused state. + /// + /// Equivalent to `AccountComponent::from(Pausable)`; provided as an explicit literal form + /// for call sites that prefer spelling out the initial state. + pub fn unpaused() -> AccountComponent { + Self::new(false) + } + /// Storage slot name for the pause flag word. - pub fn is_paused_config_slot() -> &'static StorageSlotName { - &IS_PAUSED_CONFIG_SLOT_NAME + pub fn is_paused_slot() -> &'static StorageSlotName { + &IS_PAUSED_SLOT_NAME } /// Schema entry for the pause flag slot (documentation / tooling). pub fn is_paused_slot_schema() -> (StorageSlotName, StorageSlotSchema) { ( - Self::is_paused_config_slot().clone(), + Self::is_paused_slot().clone(), StorageSlotSchema::value( "Pause flag word; zero is unpaused, canonical paused encoding is [1,0,0,0]", [ @@ -94,8 +137,8 @@ impl Pausable { /// Metadata for accounts that include this component (faucet types that may issue /// callback-enabled assets). pub fn component_metadata() -> AccountComponentMetadata { - let storage_schema = - StorageSchema::new([Self::is_paused_slot_schema()]).expect("storage schema should be valid"); + let storage_schema = StorageSchema::new([Self::is_paused_slot_schema()]) + .expect("storage schema should be valid"); AccountComponentMetadata::new( Self::NAME, @@ -107,6 +150,10 @@ impl Pausable { .with_storage_schema(storage_schema) } + pub fn is_paused_digest() -> Word { + *PAUSABLE_IS_PAUSED + } + pub fn pause_digest() -> Word { *PAUSABLE_PAUSE } @@ -126,23 +173,6 @@ impl Pausable { impl From for AccountComponent { fn from(_: Pausable) -> Self { - let is_paused_slot = StorageSlot::with_value( - Pausable::is_paused_config_slot().clone(), - Word::default(), - ); - let callback_slots = AssetCallbacks::new() - .on_before_asset_added_to_account(Pausable::on_before_asset_added_to_account_digest()) - .on_before_asset_added_to_note(Pausable::on_before_asset_added_to_note_digest()) - .into_storage_slots(); - - let mut storage_slots = Vec::with_capacity(1 + callback_slots.len()); - storage_slots.push(is_paused_slot); - storage_slots.extend(callback_slots); - - let metadata = Pausable::component_metadata(); - - AccountComponent::new(pausable_library(), storage_slots, metadata).expect( - "pausable component should satisfy the requirements of a valid account component", - ) + Pausable::new(false) } } diff --git a/crates/miden-testing/tests/scripts/mod.rs b/crates/miden-testing/tests/scripts/mod.rs index 3506254302..7239e3378f 100644 --- a/crates/miden-testing/tests/scripts/mod.rs +++ b/crates/miden-testing/tests/scripts/mod.rs @@ -1,8 +1,8 @@ mod faucet; mod fee; mod ownable2step; -mod pausable; mod p2id; mod p2ide; +mod pausable; mod send_note; mod swap; diff --git a/crates/miden-testing/tests/scripts/pausable.rs b/crates/miden-testing/tests/scripts/pausable.rs index f661e00685..7c266c7a34 100644 --- a/crates/miden-testing/tests/scripts/pausable.rs +++ b/crates/miden-testing/tests/scripts/pausable.rs @@ -1,13 +1,23 @@ -//! Tests for [`miden_standards::account::pausable::Pausable`] asset callbacks and pause/unpause scripts. +//! Tests for [`miden_standards::account::pausable::Pausable`] asset callbacks and pause/unpause +//! scripts. extern crate alloc; use miden_protocol::account::auth::AuthScheme; use miden_protocol::account::{ - Account, AccountBuilder, AccountComponent, AccountId, AccountStorageMode, AccountType, + Account, + AccountBuilder, + AccountComponent, + AccountId, + AccountStorageMode, + AccountType, }; use miden_protocol::asset::{ - Asset, AssetCallbackFlag, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails, + Asset, + AssetCallbackFlag, + FungibleAsset, + NonFungibleAsset, + NonFungibleAssetDetails, }; use miden_protocol::errors::MasmError; use miden_protocol::note::{NoteTag, NoteType}; @@ -16,11 +26,18 @@ use miden_standards::account::faucets::BasicFungibleFaucet; use miden_standards::account::pausable::Pausable; use miden_standards::code_builder::CodeBuilder; use miden_standards::testing::account_component::MockFaucetComponent; -use miden_testing::{AccountState, Auth, MockChain, MockChainBuilder, assert_transaction_executor_error}; +use miden_testing::{ + AccountState, + Auth, + MockChain, + MockChainBuilder, + assert_transaction_executor_error, +}; const ERR_PAUSABLE_ENFORCED_PAUSE: MasmError = MasmError::from_static_str("the contract is paused"); -const ERR_PAUSABLE_EXPECTED_PAUSE: MasmError = MasmError::from_static_str("the contract is not paused"); +const ERR_PAUSABLE_EXPECTED_PAUSE: MasmError = + MasmError::from_static_str("the contract is not paused"); fn add_faucet_with_pausable(builder: &mut MockChainBuilder) -> anyhow::Result { let basic_faucet = BasicFungibleFaucet::new("SYM".try_into()?, 8, Felt::new(1_000_000))?; @@ -49,8 +66,9 @@ fn add_faucet_with_pausable_for_account_type( } let faucet_component: AccountComponent = match account_type { - AccountType::FungibleFaucet => BasicFungibleFaucet::new("SYM".try_into()?, 8, Felt::new(1_000_000))? - .into(), + AccountType::FungibleFaucet => { + BasicFungibleFaucet::new("SYM".try_into()?, 8, Felt::new(1_000_000))?.into() + }, AccountType::NonFungibleFaucet => MockFaucetComponent.into(), _ => anyhow::bail!("pausable tests only use fungible or non-fungible faucet account types"), }; @@ -70,7 +88,10 @@ fn add_faucet_with_pausable_for_account_type( ) } -async fn execute_faucet_pause(mock_chain: &mut MockChain, faucet_id: AccountId) -> anyhow::Result<()> { +async fn execute_faucet_pause( + mock_chain: &mut MockChain, + faucet_id: AccountId, +) -> anyhow::Result<()> { let pause_script = r#" begin padw padw push.0 From 6be1f6574247e5c8dbdfea83eab633bb3391f456 Mon Sep 17 00:00:00 2001 From: onurinanc Date: Mon, 20 Apr 2026 12:10:37 +0200 Subject: [PATCH 3/6] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2bc47e1c8..b93873e3ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - Added `BlockNumber::saturating_sub()` ([#2660](https://github.com/0xMiden/protocol/issues/2660)). - [BREAKING] Added cycle counts to notes returned by `NoteConsumptionInfo` and removed public fields from related types ([#2772](https://github.com/0xMiden/miden-base/issues/2772)). - Automatically enable `concurrent` feature in `miden-tx` for `std` context ([#2791](https://github.com/0xMiden/protocol/pull/2791)). +- Added `Pausable` standard component with `pause`, `unpause`, `is_paused` procedures and `on_before_asset_added_to_account`, `on_before_asset_added_to_note` callbacks ([#2793](https://github.com/0xMiden/protocol/pull/2793)). ### Fixes From 2915639413f4bd8829f00cb0bf413cec50b2f9fa Mon Sep 17 00:00:00 2001 From: onurinanc Date: Mon, 20 Apr 2026 12:23:13 +0200 Subject: [PATCH 4/6] clippy --- .../miden-standards/src/account/pausable.rs | 77 ++++++++++--------- .../miden-testing/tests/scripts/pausable.rs | 4 +- 2 files changed, 43 insertions(+), 38 deletions(-) diff --git a/crates/miden-standards/src/account/pausable.rs b/crates/miden-standards/src/account/pausable.rs index 2d6fef5155..2ceac7ab70 100644 --- a/crates/miden-standards/src/account/pausable.rs +++ b/crates/miden-standards/src/account/pausable.rs @@ -51,10 +51,8 @@ procedure_digest!( /// while paused. /// /// `pause` and `unpause` do not authenticate the caller — this is an intentional choice: -/// the core mechanism is kept without access control so that auth wrappers (e.g. -/// `PausableOwnerControlled` backed by [`Ownable2Step`] or `PausableRoleControlled` backed by -/// role-based access control) can be implemented on top without duplicating the pause/unpause -/// bodies. Compose this component with access control components. +/// the core mechanism is kept without access control so that owner and role-based access control +/// can be implemented on top without duplicating the pause/unpause. /// /// ## Storage /// @@ -62,7 +60,9 @@ procedure_digest!( /// (see MASM `miden::standards::utils::pausable`). /// - Protocol callback slots from [`AssetCallbacks`] registered by every constructor. #[derive(Debug, Clone, Copy, Default)] -pub struct Pausable; +pub struct Pausable { + initial_state: bool, +} impl Pausable { /// Component library path (merged account module name). @@ -75,41 +75,25 @@ impl Pausable { "on_before_asset_added_to_account"; const ON_BEFORE_ASSET_ADDED_TO_NOTE_PROC_NAME: &'static str = "on_before_asset_added_to_note"; - /// Builds a Pausable [`AccountComponent`] with the initial paused state or unpaused state. - pub fn new(is_paused: bool) -> AccountComponent { - let initial_word = if is_paused { - Word::from([1u32, 0, 0, 0]) - } else { - Word::default() - }; - - let is_paused_slot = StorageSlot::with_value(Self::is_paused_slot().clone(), initial_word); - let callback_slots = AssetCallbacks::new() - .on_before_asset_added_to_account(Self::on_before_asset_added_to_account_digest()) - .on_before_asset_added_to_note(Self::on_before_asset_added_to_note_digest()) - .into_storage_slots(); - - let mut storage_slots = Vec::with_capacity(1 + callback_slots.len()); - storage_slots.push(is_paused_slot); - storage_slots.extend(callback_slots); - - let metadata = Self::component_metadata(); - - AccountComponent::new(pausable_library(), storage_slots, metadata).expect( - "pausable component should satisfy the requirements of a valid account component", - ) + /// Creates a new [`Pausable`] with the given initial paused state. + /// + /// Use this constructor when the flag comes from configuration, CLI input, a registry, etc. + /// For literal values prefer [`Self::paused`] / [`Self::unpaused`] (or [`Self::default`] for + /// the unpaused default). + pub const fn new(initial_state: bool) -> Self { + Self { initial_state } } - /// Builds a Pausable [`AccountComponent`] that starts in the paused state. - pub fn paused() -> AccountComponent { + /// Creates a new [`Pausable`] that starts in the paused state. + pub const fn paused() -> Self { Self::new(true) } - /// Builds a Pausable [`AccountComponent`] that starts in the unpaused state. + /// Creates a new [`Pausable`] that starts in the unpaused state. /// - /// Equivalent to `AccountComponent::from(Pausable)`; provided as an explicit literal form - /// for call sites that prefer spelling out the initial state. - pub fn unpaused() -> AccountComponent { + /// Equivalent to [`Self::default`]; provided as an explicit literal form for call sites that + /// prefer spelling out the initial state. + pub const fn unpaused() -> Self { Self::new(false) } @@ -172,7 +156,28 @@ impl Pausable { } impl From for AccountComponent { - fn from(_: Pausable) -> Self { - Pausable::new(false) + fn from(pausable: Pausable) -> Self { + let initial_word = if pausable.initial_state { + Word::from([1u32, 0, 0, 0]) + } else { + Word::default() + }; + + let is_paused_slot = + StorageSlot::with_value(Pausable::is_paused_slot().clone(), initial_word); + let callback_slots = AssetCallbacks::new() + .on_before_asset_added_to_account(Pausable::on_before_asset_added_to_account_digest()) + .on_before_asset_added_to_note(Pausable::on_before_asset_added_to_note_digest()) + .into_storage_slots(); + + let mut storage_slots = Vec::with_capacity(1 + callback_slots.len()); + storage_slots.push(is_paused_slot); + storage_slots.extend(callback_slots); + + let metadata = Pausable::component_metadata(); + + AccountComponent::new(pausable_library(), storage_slots, metadata).expect( + "pausable component should satisfy the requirements of a valid account component", + ) } } diff --git a/crates/miden-testing/tests/scripts/pausable.rs b/crates/miden-testing/tests/scripts/pausable.rs index 7c266c7a34..c5c542dd07 100644 --- a/crates/miden-testing/tests/scripts/pausable.rs +++ b/crates/miden-testing/tests/scripts/pausable.rs @@ -46,7 +46,7 @@ fn add_faucet_with_pausable(builder: &mut MockChainBuilder) -> anyhow::Result Date: Wed, 22 Apr 2026 14:48:34 +0200 Subject: [PATCH 5/6] fix tests after merge --- .../miden-testing/tests/scripts/pausable.rs | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/crates/miden-testing/tests/scripts/pausable.rs b/crates/miden-testing/tests/scripts/pausable.rs index c5c542dd07..eb9c94473f 100644 --- a/crates/miden-testing/tests/scripts/pausable.rs +++ b/crates/miden-testing/tests/scripts/pausable.rs @@ -3,6 +3,7 @@ extern crate alloc; +use miden_protocol::Word; use miden_protocol::account::auth::AuthScheme; use miden_protocol::account::{ Account, @@ -21,8 +22,8 @@ use miden_protocol::asset::{ }; use miden_protocol::errors::MasmError; use miden_protocol::note::{NoteTag, NoteType}; -use miden_protocol::{Felt, Word}; use miden_standards::account::faucets::BasicFungibleFaucet; +use miden_standards::account::metadata::{FungibleTokenMetadataBuilder, TokenName}; use miden_standards::account::pausable::Pausable; use miden_standards::code_builder::CodeBuilder; use miden_standards::testing::account_component::MockFaucetComponent; @@ -40,12 +41,19 @@ const ERR_PAUSABLE_EXPECTED_PAUSE: MasmError = MasmError::from_static_str("the contract is not paused"); fn add_faucet_with_pausable(builder: &mut MockChainBuilder) -> anyhow::Result { - let basic_faucet = BasicFungibleFaucet::new("SYM".try_into()?, 8, Felt::new(1_000_000))?; + let faucet_metadata = FungibleTokenMetadataBuilder::new( + TokenName::new("SYM")?, + "SYM".try_into()?, + 8, + 1_000_000u64, + ) + .build()?; let account_builder = AccountBuilder::new([43u8; 32]) .storage_mode(AccountStorageMode::Public) .account_type(AccountType::FungibleFaucet) - .with_component(basic_faucet) + .with_component(faucet_metadata) + .with_component(BasicFungibleFaucet) .with_component(Pausable::default()); builder.add_account_from_builder( @@ -65,19 +73,28 @@ fn add_faucet_with_pausable_for_account_type( anyhow::bail!("account type must be a faucet"); } - let faucet_component: AccountComponent = match account_type { + let faucet_components: Vec = match account_type { AccountType::FungibleFaucet => { - BasicFungibleFaucet::new("SYM".try_into()?, 8, Felt::new(1_000_000))?.into() + let faucet_metadata = FungibleTokenMetadataBuilder::new( + TokenName::new("SYM")?, + "SYM".try_into()?, + 8, + 1_000_000u64, + ) + .build()?; + vec![faucet_metadata.into(), BasicFungibleFaucet.into()] }, - AccountType::NonFungibleFaucet => MockFaucetComponent.into(), + AccountType::NonFungibleFaucet => vec![MockFaucetComponent.into()], _ => anyhow::bail!("pausable tests only use fungible or non-fungible faucet account types"), }; - let account_builder = AccountBuilder::new([43u8; 32]) + let mut account_builder = AccountBuilder::new([43u8; 32]) .storage_mode(AccountStorageMode::Public) - .account_type(account_type) - .with_component(faucet_component) - .with_component(Pausable::default()); + .account_type(account_type); + for component in faucet_components { + account_builder = account_builder.with_component(component); + } + account_builder = account_builder.with_component(Pausable::default()); builder.add_account_from_builder( Auth::BasicAuth { From 3c1d069593e565ea350ff32bf17c4e91d6ecdddd Mon Sep 17 00:00:00 2001 From: onurinanc Date: Thu, 23 Apr 2026 15:30:26 +0200 Subject: [PATCH 6/6] remove load procedure --- .../asm/standards/utils/pausable.masm | 35 +++++-------------- 1 file changed, 8 insertions(+), 27 deletions(-) diff --git a/crates/miden-standards/asm/standards/utils/pausable.masm b/crates/miden-standards/asm/standards/utils/pausable.masm index e8475b76b3..054619d85a 100644 --- a/crates/miden-standards/asm/standards/utils/pausable.masm +++ b/crates/miden-standards/asm/standards/utils/pausable.masm @@ -40,7 +40,8 @@ const ERR_PAUSABLE_EXPECTED_PAUSE = "the contract is not paused" #! #! Invocation: call pub proc is_paused - exec.load_is_paused_word + push.IS_PAUSED_SLOT[0..2] + exec.active_account::get_item # => [is_paused, 0, 0, 0, pad(16)] exec.word::eqz not @@ -103,9 +104,8 @@ end #! Requires the contract to be unpaused (storage word is the zero word). #! -#! Reads [`IS_PAUSED_SLOT`] on the active account via [`load_is_paused_word`], then uses -#! [`word::eqz`]. If the stored word is non-zero (paused), panics with -#! [`ERR_PAUSABLE_ENFORCED_PAUSE`]. +#! Reads [`IS_PAUSED_SLOT`] on the active account and applies [`word::eqz`]. If the stored word +#! is non-zero (paused), panics with [`ERR_PAUSABLE_ENFORCED_PAUSE`]. #! #! Use from other modules or transaction scripts to guard logic that must not run while paused. #! In asset callback foreign context, the active account is the issuing faucet. @@ -118,7 +118,8 @@ end #! #! Invocation: exec pub proc assert_not_paused - exec.load_is_paused_word + push.IS_PAUSED_SLOT[0..2] + exec.active_account::get_item # => [is_paused, 0, 0, 0] exec.word::eqz @@ -143,7 +144,8 @@ end #! #! Invocation: exec pub proc assert_paused - exec.load_is_paused_word + push.IS_PAUSED_SLOT[0..2] + exec.active_account::get_item # => [is_paused, 0, 0, 0] exec.word::eqz not @@ -188,24 +190,3 @@ pub proc on_before_asset_added_to_note dropw # => [ASSET_VALUE, note_idx, pad(7)] end - -# HELPER PROCEDURES -# ================================================================================================ - -#! Loads the paused state word from storage. -#! -#! Inputs: [] -#! Outputs: [is_paused, 0, 0, 0] -#! -#! Where: -#! - is_paused is the flag felt at the top of [`IS_PAUSED_SLOT`] (`1` when paused, `0` when -#! unpaused). -#! - The remaining three felts of the word are always zero by construction (see [`PAUSED_WORD`] -#! and [`UNPAUSED_WORD`]). -#! -#! Invocation: exec -proc load_is_paused_word - push.IS_PAUSED_SLOT[0..2] - exec.active_account::get_item - # => [is_paused, 0, 0, 0] -end