Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
### Fixes

- Fixed `LocalTransactionProver` accumulating `MastForest` entries across `prove()` calls, causing `capacity_overflow` panics in WASM environments where linear memory fragmentation prevents subsequent allocations ([#2918](https://github.com/0xMiden/protocol/pull/2918)).
- Fixed `create_fungible_faucet` leaving authority-gated setters unauthenticated under `AccessControl::AuthControlled`: the `AuthSingleSigAcl` trigger list now contains every authority-gated setter root (`set_max_supply`, `set_description`, `set_logo_uri`, `set_external_link`, `set_mint_policy`, `set_burn_policy`, `set_send_policy`, `set_receive_policy`) in addition to `mint_and_send`. ([#2958](https://github.com/0xMiden/protocol/pull/2958)).
- Made deserialization of `AccountCode` more robust ([#2788](https://github.com/0xMiden/protocol/pull/2788)).
- Validated `PartialBlockchain` invariants on deserialization ([#2888](https://github.com/0xMiden/protocol/pull/2888)).
- Fixed `output_note::add_asset` and `output_note::set_attachment` to no longer accept invalid note indices ([#2824](https://github.com/0xMiden/protocol/pull/2824)).
Expand Down
6 changes: 6 additions & 0 deletions crates/miden-standards/src/account/access/authority.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ const RBAC_CONTROLLED: u8 = 2;
/// the MASM helper `authority::assert_authorized`. Installing the [`Authority`] component on an
/// account thus selects the gating mode for *all* such procedures in one place.
///
/// # Safety invariant for [`Authority::AuthControlled`]
///
/// Because `assert_authorized` is a no-op under `AuthControlled`, the account's auth component
/// is the **sole** gate for every authority-gated setter. The auth component MUST therefore
/// authenticate every such setter root, otherwise the setters become permissionless.
///
/// Storage layout: `[authority, role_symbol_or_zero, 0, 0]` — single Word.
#[repr(u8)]
#[derive(Debug, Clone, PartialEq, Eq)]
Expand Down
210 changes: 166 additions & 44 deletions crates/miden-standards/src/account/faucets/fungible/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,34 @@ procedure_root!(
FungibleFaucet::code()
);

procedure_root!(
FUNGIBLE_FAUCET_SET_MAX_SUPPLY,
FungibleFaucet::NAME,
FungibleFaucet::SET_MAX_SUPPLY_PROC_NAME,
FungibleFaucet::code()
);

procedure_root!(
FUNGIBLE_FAUCET_SET_DESCRIPTION,
FungibleFaucet::NAME,
FungibleFaucet::SET_DESCRIPTION_PROC_NAME,
FungibleFaucet::code()
);

procedure_root!(
FUNGIBLE_FAUCET_SET_LOGO_URI,
FungibleFaucet::NAME,
FungibleFaucet::SET_LOGO_URI_PROC_NAME,
FungibleFaucet::code()
);

procedure_root!(
FUNGIBLE_FAUCET_SET_EXTERNAL_LINK,
FungibleFaucet::NAME,
FungibleFaucet::SET_EXTERNAL_LINK_PROC_NAME,
FungibleFaucet::code()
);

/// An [`AccountComponent`] implementing a fungible faucet.
///
/// This component bundles the asset minting/burning procedures and the token metadata
Expand Down Expand Up @@ -195,6 +223,10 @@ impl FungibleFaucet {

const MINT_PROC_NAME: &'static str = "mint_and_send";
const RECEIVE_AND_BURN_PROC_NAME: &'static str = "receive_and_burn";
const SET_MAX_SUPPLY_PROC_NAME: &'static str = "set_max_supply";
const SET_DESCRIPTION_PROC_NAME: &'static str = "set_description";
const SET_LOGO_URI_PROC_NAME: &'static str = "set_logo_uri";
const SET_EXTERNAL_LINK_PROC_NAME: &'static str = "set_external_link";

// CONSTRUCTORS
// --------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -251,6 +283,26 @@ impl FungibleFaucet {
*FUNGIBLE_FAUCET_RECEIVE_AND_BURN
}

/// Returns the procedure root of the `set_max_supply` account procedure.
pub fn set_max_supply_root() -> AccountProcedureRoot {
*FUNGIBLE_FAUCET_SET_MAX_SUPPLY
}

/// Returns the procedure root of the `set_description` account procedure.
pub fn set_description_root() -> AccountProcedureRoot {
*FUNGIBLE_FAUCET_SET_DESCRIPTION
}

/// Returns the procedure root of the `set_logo_uri` account procedure.
pub fn set_logo_uri_root() -> AccountProcedureRoot {
*FUNGIBLE_FAUCET_SET_LOGO_URI
}

/// Returns the procedure root of the `set_external_link` account procedure.
pub fn set_external_link_root() -> AccountProcedureRoot {
*FUNGIBLE_FAUCET_SET_EXTERNAL_LINK
}

/// Returns the [`StorageSlotName`] holding the token config word
/// `[token_supply, max_supply, decimals, token_symbol]`.
pub fn token_config_slot() -> &'static StorageSlotName {
Expand Down Expand Up @@ -500,20 +552,40 @@ impl TryFrom<&Account> for FungibleFaucet {
// FACTORY
// ================================================================================================

/// Every authority-gated procedure root that must require a signature when
/// [`AccessControl::AuthControlled`] is paired with [`AuthMethod::SingleSig`]. Includes
/// `mint_and_send` so that minting always requires a signature regardless of the access
/// control variant.
fn all_authority_gated_setter_roots() -> Vec<AccountProcedureRoot> {
vec![
FungibleFaucet::mint_and_send_root(),
FungibleFaucet::set_max_supply_root(),
FungibleFaucet::set_description_root(),
FungibleFaucet::set_logo_uri_root(),
FungibleFaucet::set_external_link_root(),
TokenPolicyManager::set_mint_policy_root(),
TokenPolicyManager::set_burn_policy_root(),
TokenPolicyManager::set_send_policy_root(),
TokenPolicyManager::set_receive_policy_root(),
]
}
Comment on lines +555 to +571

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.

Not for this PR, but I think a safer design for AuthSinglesigAcl might be something similar as what we recently did for AuthMultisig, where if a procedure is called that is not configured, the threshold is set to at least default_threshold.

Similarly for the ACL, we could flip the current logic and say that only configured procedures are exempt from a signature. This is the safer default. E.g. you could say receive_asset can run without signature, but anything else requires a signature.

It would also make the configuration less cumbersome, e.g. here we need to carefully list all procedures exhaustively and if we forget just one, we might have a security hole.

I'm not yet sure how to marry this concept together with with_allow_unauthorized_input_notes and the with_allow_unauthorized_output_notes, but maybe we'd have to remove this and require users to be more explicit (e.g. explicitly configure receive_and_burn as a no-signature procedure).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I think we can resolve this as described in the second approach here: #2964 (comment)


/// Creates a new fungible faucet account by composing the required components.
///
/// The behaviour of the resulting faucet (basic vs network-style) is determined entirely by the
/// combination of arguments passed in:
/// - `account_type`: typically [`AccountType::Public`] for basic or network faucets.
/// - `auth_method`: typically [`AuthMethod::SingleSig`] for basic faucets, or
/// [`AuthMethod::NetworkAccount`] for network-style faucets. [`AuthMethod::NoAuth`] is also
/// accepted for unauthenticated faucets.
/// - `access_control`: [`AccessControl::AuthControlled`] for auth-only faucets, or
/// [`AccessControl::Ownable2Step`] / [`AccessControl::Rbac`] for owner-controlled faucets.
/// - `token_policy_manager`: the unified [`TokenPolicyManager`] holding both mint and burn policy.
/// Only specific `(access_control, auth_method)` combinations are supported; everything else
/// is rejected at the factory level. The valid combinations are:
///
/// The faucet itself, including all token metadata, is provided in the `faucet` parameter (see
/// [`FungibleFaucet::builder`]).
/// - [`AccessControl::AuthControlled`] + [`AuthMethod::SingleSig`] — user-account faucet whose auth
/// component is the sole gate for every authority-protected setter.
/// - [`AccessControl::Ownable2Step`] / [`AccessControl::Rbac`] + [`AuthMethod::NetworkAccount`] or
/// [`AuthMethod::NoAuth`] — network-style faucet whose setter gate is enforced in-procedure by
/// the owner/role check.
///
/// All other pairings return a typed error:
/// [`FungibleFaucetError::IncompatibleAuthControlledAuth`] for `AuthControlled + NoAuth`, and
/// [`FungibleFaucetError::UnsupportedAccessControlAuthCombination`] for `AuthControlled +
/// NetworkAccount` and for `Ownable2Step`/`Rbac` + `SingleSig`. `Multisig` and `Unknown`
/// remain rejected for every variant via [`FungibleFaucetError::UnsupportedAuthMethod`].
pub fn create_fungible_faucet(
init_seed: [u8; 32],
faucet: FungibleFaucet,
Expand All @@ -522,39 +594,7 @@ pub fn create_fungible_faucet(
access_control: AccessControl,
token_policy_manager: TokenPolicyManager,
) -> Result<Account, FungibleFaucetError> {
let mint_proc_root = FungibleFaucet::mint_and_send_root();

let auth_component: AccountComponent = match auth_method {
AuthMethod::SingleSig { approver: (pub_key, auth_scheme) } => AuthSingleSigAcl::new(
pub_key,
auth_scheme,
AuthSingleSigAclConfig::new()
.with_auth_trigger_procedures(vec![mint_proc_root])
.with_allow_unauthorized_input_notes(true),
)
.map_err(FungibleFaucetError::AccountError)?
.into(),
AuthMethod::NoAuth => NoAuth::new().into(),
AuthMethod::NetworkAccount { allowed_script_roots } => {
AuthNetworkAccount::with_allowlist(allowed_script_roots)
.map_err(|err| {
FungibleFaucetError::UnsupportedAuthMethod(alloc::format!(
"invalid network account allowlist: {err}"
))
})?
.into()
},
AuthMethod::Unknown => {
return Err(FungibleFaucetError::UnsupportedAuthMethod(
"fungible faucets cannot be created with Unknown authentication method".into(),
));
},
AuthMethod::Multisig { .. } => {
return Err(FungibleFaucetError::UnsupportedAuthMethod(
"fungible faucets do not support Multisig authentication".into(),
));
},
};
let auth_component = build_auth_component(&access_control, auth_method)?;

let account = AccountBuilder::new(init_seed)
.account_type(account_type)
Expand All @@ -567,3 +607,85 @@ pub fn create_fungible_faucet(

Ok(account)
}

/// Builds the account-level auth component, validating the `(access_control, auth_method)`
/// pair. See [`create_fungible_faucet`] for the list of supported combinations.
fn build_auth_component(
access_control: &AccessControl,
auth_method: AuthMethod,
) -> Result<AccountComponent, FungibleFaucetError> {
match (access_control, auth_method) {
// AuthControlled + SingleSig: the auth component is the sole setter gate, so it
// must authenticate every authority-gated setter root.
(
AccessControl::AuthControlled,
AuthMethod::SingleSig { approver: (pub_key, auth_scheme) },
) => Ok(AuthSingleSigAcl::new(
pub_key,
auth_scheme,
AuthSingleSigAclConfig::new()
.with_auth_trigger_procedures(all_authority_gated_setter_roots())
.with_allow_unauthorized_input_notes(true),
)
.map_err(FungibleFaucetError::AccountError)?
.into()),

// AuthControlled + NetworkAccount: rejected.
(AccessControl::AuthControlled, AuthMethod::NetworkAccount { .. }) => {
Err(FungibleFaucetError::UnsupportedAccessControlAuthCombination(
"NetworkAccount is only supported with AccessControl::Ownable2Step or \
AccessControl::Rbac (network-style faucets)"
.into(),
))
},

// AuthControlled + NoAuth: rejected. NoAuth cannot authenticate setters; under
// AuthControlled the auth component is the sole gate, so this would leave every
// authority-gated setter permissionless.
(AccessControl::AuthControlled, AuthMethod::NoAuth) => {
Err(FungibleFaucetError::IncompatibleAuthControlledAuth(
"NoAuth cannot authenticate authority-gated setters".into(),
))
},

// Ownable2Step / Rbac + NetworkAccount: typical network-style faucet. Setter gating
// is enforced in-procedure; the auth component restricts which note scripts can be
// consumed against the faucet.
(
AccessControl::Ownable2Step { .. } | AccessControl::Rbac { .. },
AuthMethod::NetworkAccount { allowed_script_roots },
) => Ok(AuthNetworkAccount::with_allowlist(allowed_script_roots)
.map_err(|err| {
FungibleFaucetError::UnsupportedAuthMethod(alloc::format!(
"invalid network account allowlist: {err}"
))
})?
.into()),

// Ownable2Step / Rbac + NoAuth: valid; the setter gate is the in-procedure owner /
// role check, so the account-level auth can legitimately be NoAuth.
(AccessControl::Ownable2Step { .. } | AccessControl::Rbac { .. }, AuthMethod::NoAuth) => {
Ok(NoAuth::new().into())
},

// Ownable2Step / Rbac + SingleSig: rejected. SingleSig is for user-account faucets
// (AuthControlled); under owner/role-gated faucets it duplicates the setter check
// with a per-tx signature that doesn't add security.
(
AccessControl::Ownable2Step { .. } | AccessControl::Rbac { .. },
AuthMethod::SingleSig { .. },
) => Err(FungibleFaucetError::UnsupportedAccessControlAuthCombination(
"SingleSig is only supported with AccessControl::AuthControlled; pair \
Ownable2Step / Rbac with NetworkAccount or NoAuth instead"
.into(),
)),

// Multisig and Unknown are not supported for any access control variant.
(_, AuthMethod::Multisig { .. }) => Err(FungibleFaucetError::UnsupportedAuthMethod(
"fungible faucets do not support Multisig authentication".into(),
)),
(_, AuthMethod::Unknown) => Err(FungibleFaucetError::UnsupportedAuthMethod(
"fungible faucets cannot be created with Unknown authentication method".into(),
)),
}
}
Loading
Loading