use bon::bon;
use miden_protocol::account::{AccountComponent, AccountId, RoleSymbol};
use miden_protocol::asset::{AssetAmount, AssetId};
use miden_protocol::block::FeeParameters;
use miden_protocol::crypto::dsa::ecdsa_k256_keccak::PublicKey;
use miden_standards::account::access::{
Pausable,
PausableManager,
RoleBasedAccessControl,
RoleConfig,
};
use miden_standards::account::faucets::FungibleFaucet;
use miden_standards::account::fees::ConstantFeeManager;
use miden_standards::account::policies::{
BlocklistManager,
BurnPolicy,
MinBurnAmount,
MintPolicy,
TokenPolicyManager,
TokenPolicyManagerV2,
TransferPolicy,
};
use miden_standards::account::upgrade::UpgradeManager;
use crate::account::xreserve::XReserveAdminAuthority;
use crate::xreserve::encoding::CircleDomain;
mod construction;
mod error;
mod network_auth;
use construction::build_usdcx_faucet;
pub use construction::{
XReserveFaucetExtension,
build_faucet_account,
record_used_nonces,
upgrade_to_token_policy_manager_v2,
};
pub use error::XReserveStablecoinBuilderError;
pub const DOM_PAUSER_ROLE: &str = "DOM_PAUSER";
pub const ATTEST_ADMIN_ROLE: &str = "ATTEST_ADMIN";
pub const DOM_UNPAUSER_ROLE: &str = "DOM_UNPAUSER";
pub const BLK_MANAGER_ROLE: &str = "BLK_MANAGER";
pub const ATTESTATION_MINT_POLICY_PROC_PATH: &str =
"xreserve::components::faucet_extension::check_policy";
pub const XRESERVE_SET_ATTESTER_PROC_PATH: &str =
"xreserve::components::faucet_extension::set_attester";
pub const XRESERVE_BURN_POLICY_PROC_PATH: &str =
"xreserve::components::faucet_burn_policy::check_burn_policy";
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum PolicyManagerVersion {
#[default]
V1,
V2,
}
pub const MIN_BURN_SIZE_FLOOR: u64 = 1;
pub const USDCX_TOKEN_SYMBOL: &str = "USDCX";
pub const USDCX_DECIMALS: u8 = 6;
#[derive(Debug)]
pub struct XReserveStablecoinBuilder {
faucet: FungibleFaucet,
owner: AccountId,
attest_admin_holders: Vec<AccountId>,
pauser_holders: Vec<AccountId>,
unpauser_holders: Vec<AccountId>,
blocklist_manager_holders: Vec<AccountId>,
fee_parameters: FeeParameters,
fee_asset_id: AssetId,
min_burn_amount: AssetAmount,
faucet_extension: XReserveFaucetExtension,
policy_manager: PolicyManagerVersion,
}
#[bon]
impl XReserveStablecoinBuilder {
#[builder]
pub fn new(
token_supply: AssetAmount,
owner: AccountId,
attest_admin_holders: Vec<AccountId>,
pauser_holders: Vec<AccountId>,
unpauser_holders: Vec<AccountId>,
blocklist_manager_holders: Vec<AccountId>,
fee_parameters: FeeParameters,
fee_asset_id: AssetId,
domain: CircleDomain,
#[builder(default)] attesters: Vec<PublicKey>,
min_burn_amount: Option<AssetAmount>,
#[builder(default)] policy_manager: PolicyManagerVersion,
) -> Result<Self, XReserveStablecoinBuilderError> {
let min_burn_amount = min_burn_amount.unwrap_or(
AssetAmount::new(MIN_BURN_SIZE_FLOOR)
.expect("the shipped burn floor is a valid asset amount"),
);
if min_burn_amount.as_u64() < MIN_BURN_SIZE_FLOOR {
return Err(XReserveStablecoinBuilderError::MinBurnSizeBelowFloor(
min_burn_amount.as_u64(),
));
}
let faucet_extension = XReserveFaucetExtension::new(domain, &attesters)
.map_err(XReserveStablecoinBuilderError::AttesterAllowlist)?;
for (role, members) in [
("ATTEST_ADMIN", &attest_admin_holders),
("DOM_PAUSER", &pauser_holders),
("DOM_UNPAUSER", &unpauser_holders),
("BLK_MANAGER", &blocklist_manager_holders),
] {
let mut seen = std::collections::BTreeSet::new();
if !members.iter().all(|member| seen.insert(member)) {
return Err(XReserveStablecoinBuilderError::DuplicateRoleMember { role });
}
}
Ok(Self {
faucet: build_usdcx_faucet(token_supply)?,
owner,
attest_admin_holders,
pauser_holders,
unpauser_holders,
blocklist_manager_holders,
fee_parameters,
fee_asset_id,
min_burn_amount,
faucet_extension,
policy_manager,
})
}
}
impl XReserveStablecoinBuilder {
pub fn build_components(
&self,
) -> Result<Vec<AccountComponent>, XReserveStablecoinBuilderError> {
self.assert_roles_isolated()?;
let manager = self.token_policy_manager()?;
match self.policy_manager {
PolicyManagerVersion::V1 => self.compose_components(manager),
PolicyManagerVersion::V2 => self.compose_components(TokenPolicyManagerV2::new(manager)),
}
}
fn assert_roles_isolated(&self) -> Result<(), XReserveStablecoinBuilderError> {
let overlaps = |left: &[AccountId], right: &[AccountId]| {
left.iter().any(|member| right.contains(member))
};
let owner_holders = [self.owner];
for (collides_with, holders) in [
("ADMIN", owner_holders.as_slice()),
("ATTEST_ADMIN", &self.attest_admin_holders),
("DOM_PAUSER", &self.pauser_holders),
("DOM_UNPAUSER", &self.unpauser_holders),
] {
if overlaps(&self.blocklist_manager_holders, holders) {
return Err(XReserveStablecoinBuilderError::BlocklistManagerNotIsolated {
collides_with,
});
}
}
for (collides_with, holders) in [
("ADMIN", owner_holders.as_slice()),
("ATTEST_ADMIN", &self.attest_admin_holders),
("DOM_UNPAUSER", &self.unpauser_holders),
] {
if overlaps(&self.pauser_holders, holders) {
return Err(XReserveStablecoinBuilderError::PauserNotIsolated { collides_with });
}
}
Ok(())
}
fn compose_components(
&self,
manager: impl IntoIterator<Item = AccountComponent>,
) -> Result<Vec<AccountComponent>, XReserveStablecoinBuilderError> {
let mut components = Vec::new();
components.push(self.faucet.clone().into());
components.push(Pausable::unpaused().into());
components.extend(manager);
components.push(PausableManager.into());
components.push(BlocklistManager.into());
components.push(ConstantFeeManager::for_basic_constant_fee_policy().into());
components.push(UpgradeManager.into());
components.push(seeded_dom_roles_rbac(
self.owner,
&self.attest_admin_holders,
&self.pauser_holders,
&self.unpauser_holders,
&self.blocklist_manager_holders,
));
components.push(XReserveAdminAuthority::new().into());
Ok(components)
}
fn token_policy_manager(&self) -> Result<TokenPolicyManager, XReserveStablecoinBuilderError> {
let xreserve_component = AccountComponent::from(self.faucet_extension.clone());
let burn_policy_component = Self::burn_policy_component();
let burn_root = burn_policy_component
.get_procedure_root_by_path(XRESERVE_BURN_POLICY_PROC_PATH)
.ok_or(XReserveStablecoinBuilderError::BurnPolicyProcNotFound)?;
Ok(TokenPolicyManager::builder()
.active_mint_policy(
MintPolicy::custom(
xreserve_component
.get_procedure_root_by_path(ATTESTATION_MINT_POLICY_PROC_PATH)
.ok_or(XReserveStablecoinBuilderError::AttestationPolicyProcNotFound)?,
[xreserve_component],
)
.map_err(XReserveStablecoinBuilderError::MintPolicy)?,
)
.active_burn_policy(BurnPolicy::custom(
burn_root,
[burn_policy_component, MinBurnAmount::new(self.min_burn_amount).into()],
)?)
.active_send_policy(TransferPolicy::empty_basic_blocklist())
.active_receive_policy(TransferPolicy::empty_basic_blocklist())
.build())
}
}
fn seeded_dom_roles_rbac(
owner: AccountId,
attest_admin_holders: &[AccountId],
pauser_holders: &[AccountId],
unpauser_holders: &[AccountId],
blocklist_manager_holders: &[AccountId],
) -> AccountComponent {
let pauser =
RoleSymbol::new(DOM_PAUSER_ROLE).expect("DOM_PAUSER is a fixed valid role symbol (≤12)");
let attest_admin = RoleSymbol::new(ATTEST_ADMIN_ROLE)
.expect("ATTEST_ADMIN is a fixed valid role symbol (≤12)");
let unpauser = RoleSymbol::new(DOM_UNPAUSER_ROLE)
.expect("DOM_UNPAUSER is a fixed valid role symbol (≤12)");
let blk_manager =
RoleSymbol::new(BLK_MANAGER_ROLE).expect("BLK_MANAGER is a fixed valid role symbol (≤12)");
let admin = RoleBasedAccessControl::admin_role();
let mut builder = RoleBasedAccessControl::builder();
for (role, holders) in [
(pauser, pauser_holders),
(attest_admin, attest_admin_holders),
(unpauser, unpauser_holders),
(blk_manager, blocklist_manager_holders),
] {
if !holders.is_empty() {
builder = builder.role(RoleConfig::new(role).with_members(holders.iter().copied()));
}
}
builder
.role(RoleConfig::new(admin).with_member(owner))
.build()
.expect("the seeded DOM-roles RBAC configuration should be valid")
.into()
}