mod support;
use anyhow::{Context, Result};
use miden_protocol::account::AccountId;
use miden_protocol::asset::AssetAmount;
use miden_usdcx::account::xreserve::{XReserveStablecoinBuilder, XReserveStablecoinBuilderError};
use rstest::rstest;
use support::*;
fn builder_with_holders(
attest_admin: AccountId,
pauser: AccountId,
unpauser: AccountId,
blk_manager: AccountId,
) -> Result<XReserveStablecoinBuilder> {
XReserveStablecoinBuilder::builder()
.token_supply(AssetAmount::new(0).context("valid token supply")?)
.owner(test_account_id(1))
.attest_admin_holders(vec![attest_admin])
.pauser_holders(vec![pauser])
.unpauser_holders(vec![unpauser])
.blocklist_manager_holders(vec![blk_manager])
.fee_parameters(test_fee_parameters())
.fee_asset_id(test_fee_asset_id())
.domain(TEST_DOMAIN)
.build()
.context("the fixed-identity USDCx faucet builds")
}
#[rstest]
#[case::administrator(test_account_id(1), "ADMIN")]
#[case::attest_admin(test_account_id(5), "ATTEST_ADMIN")]
#[case::dom_pauser(test_account_id(2), "DOM_PAUSER")]
#[case::dom_unpauser(test_account_id(3), "DOM_UNPAUSER")]
fn build_rejects_blk_manager_colliding_with_a_privileged_role(
#[case] blk_manager: AccountId,
#[case] expected_role: &str,
) -> Result<()> {
let err = builder_with_holders(
test_account_id(5),
test_account_id(2),
test_account_id(3),
blk_manager,
)?
.build_components()
.expect_err("a BLK_MANAGER holder colliding with a privileged role must be rejected");
match err {
XReserveStablecoinBuilderError::BlocklistManagerNotIsolated { collides_with } => {
assert_eq!(
collides_with, expected_role,
"the rejection must name the collided privileged role"
);
},
other => panic!("expected BlocklistManagerNotIsolated{{{expected_role}}}, got {other:?}"),
}
Ok(())
}
#[rstest]
#[case::administrator(test_account_id(1), "ADMIN")]
#[case::attest_admin(test_account_id(5), "ATTEST_ADMIN")]
#[case::dom_unpauser(test_account_id(3), "DOM_UNPAUSER")]
fn build_rejects_dom_pauser_colliding_with_any_other_holder(
#[case] pauser: AccountId,
#[case] expected_role: &str,
) -> Result<()> {
let err =
builder_with_holders(test_account_id(5), pauser, test_account_id(3), test_account_id(4))?
.build_components()
.expect_err("a DOM_PAUSER holder colliding with another role must be rejected");
match err {
XReserveStablecoinBuilderError::PauserNotIsolated { collides_with } => {
assert_eq!(collides_with, expected_role, "the rejection must name the collided role");
},
other => panic!("expected PauserNotIsolated{{{expected_role}}}, got {other:?}"),
}
Ok(())
}
#[test]
fn build_accepts_isolated_blk_manager() -> Result<()> {
builder_with_holders(
test_account_id(1),
test_account_id(2),
test_account_id(3),
test_account_id(4),
)?
.build_components()
.context("a properly isolated BLK_MANAGER holder must build")?;
Ok(())
}
#[test]
fn build_seeds_every_member_of_a_multi_holder_role() -> Result<()> {
use miden_protocol::account::RoleSymbol;
use miden_usdcx::account::xreserve::{DOM_PAUSER_ROLE, build_faucet_account};
let pausers = [test_account_id(2), test_account_id(6)];
let account = build_faucet_account(
[7u8; 32],
AssetAmount::ZERO,
test_account_id(1),
vec![test_account_id(5)],
pausers.to_vec(),
vec![test_account_id(3)],
vec![test_account_id(4)],
test_fee_parameters(),
test_fee_asset_id(),
TEST_DOMAIN,
)
.context("a two-pauser composition must build")?;
let role = RoleSymbol::new(DOM_PAUSER_ROLE).context("DOM_PAUSER is a valid role symbol")?;
for pauser in pausers {
assert_eq!(
read_role_membership(&account, &role, pauser)?,
miden_protocol::Word::from([1u32, 0, 0, 0]),
"every configured DOM_PAUSER holder must be seeded as a member",
);
}
Ok(())
}
#[test]
fn build_accepts_an_empty_operational_role() -> Result<()> {
XReserveStablecoinBuilder::builder()
.token_supply(AssetAmount::ZERO)
.owner(test_account_id(1))
.attest_admin_holders(vec![test_account_id(5)])
.pauser_holders(Vec::new())
.unpauser_holders(vec![test_account_id(3)])
.blocklist_manager_holders(vec![test_account_id(4)])
.fee_parameters(test_fee_parameters())
.fee_asset_id(test_fee_asset_id())
.domain(TEST_DOMAIN)
.build()
.context("an empty DOM_PAUSER role must construct")?
.build_components()
.map_err(|e| anyhow::anyhow!("an empty DOM_PAUSER role must compose: {e}"))?;
Ok(())
}
#[test]
fn build_rejects_a_duplicate_role_member() -> Result<()> {
let err = XReserveStablecoinBuilder::builder()
.token_supply(AssetAmount::ZERO)
.owner(test_account_id(1))
.attest_admin_holders(vec![test_account_id(5)])
.pauser_holders(vec![test_account_id(2), test_account_id(2)])
.unpauser_holders(vec![test_account_id(3)])
.blocklist_manager_holders(vec![test_account_id(4)])
.fee_parameters(test_fee_parameters())
.fee_asset_id(test_fee_asset_id())
.domain(TEST_DOMAIN)
.build()
.expect_err("a duplicated role member must be rejected");
match err {
XReserveStablecoinBuilderError::DuplicateRoleMember { role } => {
assert_eq!(role, "DOM_PAUSER", "the rejection must name the role");
},
other => panic!("expected DuplicateRoleMember{{DOM_PAUSER}}, got {other:?}"),
}
Ok(())
}
#[test]
fn build_rejects_a_secondary_pauser_colliding_with_another_role() -> Result<()> {
let err = XReserveStablecoinBuilder::builder()
.token_supply(AssetAmount::ZERO)
.owner(test_account_id(1))
.attest_admin_holders(vec![test_account_id(5)])
.pauser_holders(vec![test_account_id(2), test_account_id(3)])
.unpauser_holders(vec![test_account_id(3)])
.blocklist_manager_holders(vec![test_account_id(4)])
.fee_parameters(test_fee_parameters())
.fee_asset_id(test_fee_asset_id())
.domain(TEST_DOMAIN)
.build()
.context("the two-pauser builder constructs")?
.build_components()
.expect_err("a secondary DOM_PAUSER holder colliding with DOM_UNPAUSER must be rejected");
match err {
XReserveStablecoinBuilderError::PauserNotIsolated { collides_with } => {
assert_eq!(collides_with, "DOM_UNPAUSER");
},
other => panic!("expected PauserNotIsolated{{DOM_UNPAUSER}}, got {other:?}"),
}
Ok(())
}