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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

### Fixes

- The faucet factories now reject a `TokenPolicyManager` whose policies read a storage slot the account does not install ([#3527](https://github.com/0xMiden/protocol/pull/3527)).

## v0.16.0 (2026-08-06)

### Features
Expand Down
41 changes: 32 additions & 9 deletions crates/miden-standards/src/account/faucets/fungible/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ use crate::account::access::{AccessControl, Authority, Pausable, PausableManager
use crate::account::account_component_code;
use crate::account::auth::{AuthGuardedMultisig, AuthMultisig, AuthSingleSig, NetworkAccount};
use crate::account::fees::FeePolicyManager;
use crate::account::policies::TokenPolicyManager;
use crate::account::policies::{TokenPolicyManager, verify_policy_dependencies};
use crate::note::{BurnNote, MintNote};
use crate::procedure_root;

Expand Down Expand Up @@ -575,7 +575,9 @@ pub fn create_singlesig_user_fungible_faucet(
account_type: AccountType,
) -> Result<Account, FungibleFaucetError> {
let asset_callbacks = AssetCallbackFlag::from(token_policy_manager.has_transfer_policy());
AccountBuilder::new(init_seed)
let required_slots = token_policy_manager.required_storage_slots();

let account = AccountBuilder::new(init_seed)
.account_type(account_type)
.with_asset_callbacks(asset_callbacks)
.with_component(auth_component)
Expand All @@ -585,7 +587,11 @@ pub fn create_singlesig_user_fungible_faucet(
.with_component(Pausable::unpaused())
.with_component(PausableManager)
.build()
.map_err(FungibleFaucetError::AccountError)
.map_err(FungibleFaucetError::AccountError)?;

verify_policy_dependencies(&required_slots, account.storage())?;

Ok(account)
}

/// Creates a new **user-account** fungible faucet authenticated by a multisig approver set.
Expand All @@ -596,7 +602,9 @@ pub fn create_multisig_user_fungible_faucet(
token_policy_manager: TokenPolicyManager,
account_type: AccountType,
) -> Result<Account, FungibleFaucetError> {
AccountBuilder::new(init_seed)
let required_slots = token_policy_manager.required_storage_slots();

let account = AccountBuilder::new(init_seed)
.account_type(account_type)
.with_component(auth_component)
.with_component(faucet)
Expand All @@ -605,7 +613,11 @@ pub fn create_multisig_user_fungible_faucet(
.with_component(Pausable::unpaused())
.with_component(PausableManager)
.build()
.map_err(FungibleFaucetError::AccountError)
.map_err(FungibleFaucetError::AccountError)?;

verify_policy_dependencies(&required_slots, account.storage())?;

Ok(account)
}

/// Creates a new **user-account** fungible faucet authenticated by a guardian-backed multisig.
Expand All @@ -616,7 +628,9 @@ pub fn create_guarded_user_fungible_faucet(
token_policy_manager: TokenPolicyManager,
account_type: AccountType,
) -> Result<Account, FungibleFaucetError> {
AccountBuilder::new(init_seed)
let required_slots = token_policy_manager.required_storage_slots();

let account = AccountBuilder::new(init_seed)
.account_type(account_type)
.with_component(auth_component)
.with_component(faucet)
Expand All @@ -625,7 +639,11 @@ pub fn create_guarded_user_fungible_faucet(
.with_component(Pausable::unpaused())
.with_component(PausableManager)
.build()
.map_err(FungibleFaucetError::AccountError)
.map_err(FungibleFaucetError::AccountError)?;

verify_policy_dependencies(&required_slots, account.storage())?;

Ok(account)
}

/// Creates a new **network-style** fungible faucet. The account is always
Expand All @@ -644,8 +662,9 @@ pub fn create_network_fungible_faucet(
) -> Result<Account, FungibleFaucetError> {
let note_allowlist = [MintNote::script_root(), BurnNote::script_root()].into_iter().collect();
let asset_callbacks = AssetCallbackFlag::from(token_policy_manager.has_transfer_policy());
let required_slots = token_policy_manager.required_storage_slots();

NetworkAccount::builder(init_seed, note_allowlist, fee_policy_manager)
let account = NetworkAccount::builder(init_seed, note_allowlist, fee_policy_manager)
.expect("MintNote + BurnNote allowlist is non-empty")
.with_asset_callbacks(asset_callbacks)
.with_component(faucet)
Expand All @@ -654,5 +673,9 @@ pub fn create_network_fungible_faucet(
.with_component(Pausable::unpaused())
.with_component(PausableManager)
.build()
.map_err(FungibleFaucetError::AccountError)
.map_err(FungibleFaucetError::AccountError)?;

verify_policy_dependencies(&required_slots, account.storage())?;

Ok(account)
}
82 changes: 81 additions & 1 deletion crates/miden-standards/src/account/faucets/fungible/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use super::{
create_network_fungible_faucet,
create_singlesig_user_fungible_faucet,
};
use crate::account::access::{AccessControl, Authority};
use crate::account::access::{AccessControl, Authority, Ownable2Step};
use crate::account::auth::{
Approver,
AuthGuardedMultisig,
Expand Down Expand Up @@ -289,3 +289,83 @@ fn get_faucet_procedures() {
let _set_send_policy_root = TokenPolicyManager::set_send_policy_root();
let _set_receive_policy_root = TokenPolicyManager::set_receive_policy_root();
}

/// A user faucet installs no `Ownable2Step` component, so an owner-only mint policy has no owner
/// slot to read and every mint would abort.
#[test]
fn user_fungible_faucet_rejects_owner_only_mint_policy() {
let auth_component = AuthSingleSig::new(Approver::new(
Word::new([Felt::ONE; 4]).into(),
AuthScheme::Falcon512Poseidon2,
));

let token_policy_manager = TokenPolicyManager::builder()
.active_mint_policy(MintPolicy::owner_only())
.active_burn_policy(BurnPolicy::allow_all())
.build();

let err = create_singlesig_user_fungible_faucet(
[11u8; 32],
sample_faucet(),
auth_component,
token_policy_manager,
AccountType::Private,
)
.expect_err("owner-only mint policy without Ownable2Step should be rejected");

assert_matches!(err, FungibleFaucetError::PolicyDependency(err) => {
assert_eq!(err.slot_name(), Ownable2Step::slot_name());
});
}

#[test]
fn user_fungible_faucet_rejects_reserved_owner_only_burn_policy() {
let auth_component = AuthSingleSig::new(Approver::new(
Word::new([Felt::ONE; 4]).into(),
AuthScheme::Falcon512Poseidon2,
));

let token_policy_manager = TokenPolicyManager::builder()
.active_mint_policy(MintPolicy::allow_all())
.active_burn_policy(BurnPolicy::allow_all())
.allowed_burn_policy(BurnPolicy::owner_only())
.build();

let err = create_singlesig_user_fungible_faucet(
[12u8; 32],
sample_faucet(),
auth_component,
token_policy_manager,
AccountType::Private,
)
.expect_err("reserved owner-only burn policy without Ownable2Step should be rejected");

assert_matches!(err, FungibleFaucetError::PolicyDependency(err) => {
assert_eq!(err.slot_name(), Ownable2Step::slot_name());
});
}

/// A network faucet installs `Ownable2Step` through its access control, so the owner-only policy
/// family finds the owner slot and the dependency check passes.
#[test]
fn network_fungible_faucet_accepts_owner_only_policies() {
use miden_protocol::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE;

let owner = AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE).unwrap();

let token_policy_manager = TokenPolicyManager::builder()
.active_mint_policy(MintPolicy::owner_only())
.active_burn_policy(BurnPolicy::owner_only())
.build();

let account = create_network_fungible_faucet(
[13u8; 32],
sample_faucet(),
AccessControl::Ownable2Step { owner },
token_policy_manager,
FeePolicyManager::mock(FungibleAsset::mock_issuer()),
)
.expect("owner-only policies are satisfied by the Ownable2Step access control");

assert_eq!(Ownable2Step::try_from_storage(account.storage()).unwrap().owner(), Some(owner));
}
5 changes: 5 additions & 0 deletions crates/miden-standards/src/account/faucets/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use miden_protocol::errors::{AccountError, TokenSymbolError};
use thiserror::Error;

use crate::account::access::Ownable2StepError;
use crate::account::policies::MissingPolicyDependency;
use crate::utils::FixedWidthStringError;

mod fungible;
Expand Down Expand Up @@ -78,6 +79,8 @@ pub enum FungibleFaucetError {
OwnershipError(#[source] Ownable2StepError),
#[error(transparent)]
TokenMetadata(#[from] TokenMetadataError),
#[error(transparent)]
PolicyDependency(#[from] MissingPolicyDependency),
}

// NON-FUNGIBLE FAUCET ERROR
Expand All @@ -94,4 +97,6 @@ pub enum NonFungibleFaucetError {
InvalidAssetStatus { status: u64 },
#[error(transparent)]
TokenMetadata(#[from] TokenMetadataError),
#[error(transparent)]
PolicyDependency(#[from] MissingPolicyDependency),
}
22 changes: 17 additions & 5 deletions crates/miden-standards/src/account/faucets/non_fungible/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ use crate::account::access::{AccessControl, Authority, Pausable, PausableManager
use crate::account::account_component_code;
use crate::account::auth::{AuthSingleSig, NetworkAccount};
use crate::account::fees::FeePolicyManager;
use crate::account::policies::TokenPolicyManager;
use crate::account::policies::{TokenPolicyManager, verify_policy_dependencies};
use crate::note::{BurnNote, MintNote};
use crate::procedure_root;

Expand Down Expand Up @@ -519,7 +519,9 @@ pub fn create_user_non_fungible_faucet(
token_policy_manager: TokenPolicyManager,
account_type: AccountType,
) -> Result<Account, NonFungibleFaucetError> {
AccountBuilder::new(init_seed)
let required_slots = token_policy_manager.required_storage_slots();

let account = AccountBuilder::new(init_seed)
.account_type(account_type)
.with_component(auth_component)
.with_component(faucet)
Expand All @@ -528,7 +530,11 @@ pub fn create_user_non_fungible_faucet(
.with_component(Pausable::unpaused())
.with_component(PausableManager)
.build()
.map_err(NonFungibleFaucetError::AccountCreationFailed)
.map_err(NonFungibleFaucetError::AccountCreationFailed)?;

verify_policy_dependencies(&required_slots, account.storage())?;

Ok(account)
}

/// Creates a new **network-style** non-fungible faucet. The account is always
Expand All @@ -549,13 +555,19 @@ pub fn create_network_non_fungible_faucet(
) -> Result<Account, NonFungibleFaucetError> {
let note_allowlist = [MintNote::script_root(), BurnNote::script_root()].into_iter().collect();

NetworkAccount::builder(init_seed, note_allowlist, fee_policy_manager)
let required_slots = token_policy_manager.required_storage_slots();

let account = NetworkAccount::builder(init_seed, note_allowlist, fee_policy_manager)
.expect("MintNote + BurnNote allowlist is non-empty")
.with_component(faucet)
.with_components(access_control)
.with_components(token_policy_manager)
.with_component(Pausable::unpaused())
.with_component(PausableManager)
.build()
.map_err(NonFungibleFaucetError::AccountCreationFailed)
.map_err(NonFungibleFaucetError::AccountCreationFailed)?;

verify_policy_dependencies(&required_slots, account.storage())?;
Comment on lines +560 to +570

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A preliminary question: should this check be actually happening in the AccountBuilder? This would be a more general solution, but probably a bit more difficult to implement (or at least to design).

If we do decide to how this route, the way to implement this is to specify dependencies at the AccountComponentMetadata level. Then, the builder would be able to read dependencies of all components and verify that they are all satisfied.

I think the main complexity here would come from figuring out how to specify dependencies. The approach taken in this PR is to use just slot names. Maybe this is enough, but maybe we want to list actual components as dependencies too.


Ok(account)
}
44 changes: 42 additions & 2 deletions crates/miden-standards/src/account/faucets/non_fungible/tests.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
use assert_matches::assert_matches;
use miden_protocol::account::AccountType;
use miden_protocol::account::auth::AuthScheme;
use miden_protocol::asset::TokenSymbol;
use miden_protocol::{Felt, Word};

use super::NonFungibleFaucet;
use crate::account::faucets::TokenName;
use super::{NonFungibleFaucet, create_user_non_fungible_faucet};
use crate::account::access::Ownable2Step;
use crate::account::auth::{Approver, AuthSingleSig};
use crate::account::faucets::{NonFungibleFaucetError, TokenName};
use crate::account::policies::{BurnPolicy, MintPolicy, TokenPolicyManager};

/// Building a faucet exposes the configured fields.
#[test]
Expand Down Expand Up @@ -32,3 +39,36 @@ fn compute_asset_commitment_is_salt_sensitive() {
assert_eq!(c_a, NonFungibleFaucet::compute_asset_commitment(data, salt_a));
assert_ne!(c_a, c_b);
}

/// A user faucet installs no `Ownable2Step` component, so an owner-only mint policy has no owner
/// slot to read and every mint would abort. The factory must reject that configuration.
#[test]
fn user_non_fungible_faucet_rejects_owner_only_mint_policy() {
let faucet = NonFungibleFaucet::builder()
.name(TokenName::new("Example Collection").unwrap())
.symbol(TokenSymbol::new("EC").unwrap())
.build();

let auth_component = AuthSingleSig::new(Approver::new(
Word::new([Felt::ONE; 4]).into(),
AuthScheme::Falcon512Poseidon2,
));

let token_policy_manager = TokenPolicyManager::builder()
.active_mint_policy(MintPolicy::owner_only())
.active_burn_policy(BurnPolicy::allow_all())
.build();

let err = create_user_non_fungible_faucet(
[21u8; 32],
faucet,
auth_component,
token_policy_manager,
AccountType::Private,
)
.expect_err("owner-only mint policy without Ownable2Step should be rejected");

assert_matches!(err, NonFungibleFaucetError::PolicyDependency(err) => {
assert_eq!(err.slot_name(), Ownable2Step::slot_name());
});
}
Loading
Loading