mod support;
use anyhow::{Context, Result};
use miden_protocol::account::{
AccountComponent,
AccountProcedureRoot,
RoleSymbol,
StorageSlotName,
};
use miden_protocol::asset::AssetAmount;
use miden_protocol::{Felt, Word};
use miden_standards::account::access::{PausableManager, PausableStorage};
use miden_standards::account::policies::{BlocklistManager, MinBurnAmount, TokenPolicyManager};
use miden_usdcx::account::xreserve::{
ATTEST_ADMIN_ROLE,
ATTESTATION_MINT_POLICY_PROC_PATH,
BLK_MANAGER_ROLE,
DOM_PAUSER_ROLE,
DOM_UNPAUSER_ROLE,
XReserveAdminAuthority,
XReserveFaucetExtension,
XReserveStablecoinBuilder,
XReserveStablecoinBuilderError,
};
use support::*;
fn production_builder() -> XReserveStablecoinBuilder {
support::production_builder(0, TEST_DOMAIN).expect("the fixed-identity USDCx faucet builds")
}
fn resolve_proc_root(components: &[AccountComponent], path: &str) -> Option<Word> {
components
.iter()
.find_map(|c| c.get_procedure_root_by_path(path))
.map(Word::from)
}
fn find_value_slot(components: &[AccountComponent], name: &StorageSlotName) -> Option<Word> {
components
.iter()
.flat_map(|c| c.storage_slots().iter())
.find(|slot| slot.name() == name)
.map(|slot| slot.value())
}
#[test]
fn build_produces_attestation_gated_public_faucet() -> Result<()> {
let components = production_builder().build_components().context(
"the default production builder must compose an attestation-gated Public faucet",
)?;
let attestation_root = resolve_proc_root(&components, ATTESTATION_MINT_POLICY_PROC_PATH)
.context("the composed set must carry the attestation mint policy proc")?;
let active = find_value_slot(&components, TokenPolicyManager::active_mint_policy_slot())
.context("the composed set must carry the active-mint-policy slot")?;
assert_eq!(
active, attestation_root,
"the ACTIVE mint policy slot must hold the attestation policy root (INV-MINT-SECURITY)"
);
Ok(())
}
#[test]
fn production_seeds_min_burn_size() -> Result<()> {
const MIN_BURN: u64 = 5_000_000_000; const _: () = assert!(
MIN_BURN > u32::MAX as u64,
"MIN_BURN must exceed u32::MAX so the encoding test catches u32 truncation",
);
let components = production_builder_verdict(
0,
TEST_DOMAIN,
Some(AssetAmount::new(MIN_BURN).context("MIN_BURN must be within AssetAmount::MAX")?),
)?
.context("the fixed-identity USDCx faucet builds")?
.build_components()
.context("production build_components must compose")?;
let floor = find_value_slot(&components, MinBurnAmount::slot_name()).with_context(|| {
format!(
"production build_components must install the stock MinBurnAmount floor slot \
'{}' (the policy companion); none of the {} composed components carries it",
MinBurnAmount::slot_name(),
components.len()
)
})?;
let expected_min_burn =
Felt::from(AssetAmount::new(MIN_BURN).context("MIN_BURN must be within AssetAmount::MAX")?);
assert_eq!(
floor,
Word::from([expected_min_burn, Felt::ZERO, Felt::ZERO, Felt::ZERO]),
"the seeded MinBurnAmount floor slot must carry the FULL-u64 [min_burn_amount, 0, 0, 0] \
(no u32 truncation)"
);
Ok(())
}
#[test]
fn build_rejects_zero_min_burn_amount() -> Result<()> {
let zero = AssetAmount::new(0).expect("a zero asset amount is representable");
let err = production_builder_verdict(0, TEST_DOMAIN, Some(zero))?.expect_err(
"a min_burn_amount of 0 must be rejected at construction (zero-floor invariant)",
);
assert!(
matches!(err, XReserveStablecoinBuilderError::MinBurnSizeBelowFloor(0)),
"expected MinBurnSizeBelowFloor(0), got {err:?}"
);
Ok(())
}
#[test]
fn build_seeds_the_domain_slot() -> Result<()> {
let components = production_builder()
.build_components()
.context("production build_components must compose")?;
let slot = |name: &StorageSlotName| -> Result<Word> {
find_value_slot(&components, name)
.with_context(|| format!("the composed set must carry the '{name}' slot"))
};
assert_eq!(
slot(XReserveFaucetExtension::domain_config_slot())?,
Word::from([TEST_DOMAIN.as_u32(), 0, 0, 0]),
"the domain slot must hold the build-seeded [domain, 0, 0, 0]"
);
Ok(())
}
#[test]
fn builder_installs_the_stock_managers_with_their_roles_assigned() -> Result<()> {
let components = production_builder()
.build_components()
.context("production build_components must compose")?;
let installed: std::collections::BTreeSet<_> = components
.iter()
.flat_map(|component| component.procedures().map(|(root, _is_auth)| root))
.collect();
let roles = XReserveAdminAuthority::new().procedure_roles().clone();
let pauser = RoleSymbol::new(DOM_PAUSER_ROLE).expect("the Domain pauser role symbol is valid");
let unpauser =
RoleSymbol::new(DOM_UNPAUSER_ROLE).expect("the Domain unpauser role symbol is valid");
let attest_admin =
RoleSymbol::new(ATTEST_ADMIN_ROLE).expect("the attester administrator role is valid");
let blocklist_manager = RoleSymbol::new(BLK_MANAGER_ROLE)
.expect("the blocklist administrator role symbol is valid");
for (what, root, role) in [
("pause", PausableManager::pause_root(), &pauser),
("unpause", PausableManager::unpause_root(), &unpauser),
("block_account", BlocklistManager::block_account_root(), &blocklist_manager),
("unblock_account", BlocklistManager::unblock_account_root(), &blocklist_manager),
(
"set_attester",
AccountProcedureRoot::from_raw(
resolve_proc_root(
&components,
"xreserve::components::faucet_extension::set_attester",
)
.context("the composed set must carry the attester setter")?,
),
&attest_admin,
),
] {
assert!(
installed.contains(&root),
"the production composition must install the gated procedure {what} — the \
config note calls that exact root"
);
assert_eq!(
roles.get(&root),
Some(role),
"the procedure {what} must be gated on its intended role, or the \
capability lands on the administrator instead"
);
}
Ok(())
}
#[test]
fn production_components_carry_is_paused_slot() -> Result<()> {
let components = production_builder()
.build_components()
.context("production build_components must compose")?;
let is_paused = PausableStorage::is_paused_slot();
assert!(
components
.iter()
.flat_map(|c| c.storage_slots().iter())
.any(|slot| slot.name() == is_paused),
"the production composition must carry the Pausable-installed is_paused slot (v0.16 #2944 \
moved it out of FungibleFaucet); its absence SILENTLY disables the mint/burn pause \
halt-gates at v0.16 (#3047 no-ops assert_not_paused when the slot is missing) — \
CIR-ADMIN-4"
);
Ok(())
}
#[test]
fn production_components_carry_mutability_config_slot() -> Result<()> {
let components = production_builder()
.build_components()
.context("production build_components must compose")?;
let mutability = StorageSlotName::new("miden::standards::faucets::mutability_config")
.context("the pinned mutability_config slot name")?;
let slot_value = components
.iter()
.flat_map(|c| c.storage_slots().iter())
.find(|slot| slot.name() == &mutability)
.map(|slot| slot.value())
.expect("the production composition must carry the faucet mutability configuration");
assert_eq!(
slot_value,
Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ONE]),
"only maximum supply is mutable",
);
Ok(())
}