mod support;
use anyhow::{Context, Result};
use miden_processor::crypto::random::RandomCoin;
use miden_protocol::account::{AccountId, RoleSymbol};
use miden_protocol::errors::MasmError;
use miden_protocol::note::Note;
use miden_protocol::transaction::ExecutedTransaction;
use miden_protocol::{Felt, Word};
use miden_standards::interop::eth::EthEmbeddedAccountId;
use miden_standards::note::config::{RbacConfig, RbacConfigNote};
use miden_testing::assert_transaction_executor_error;
use miden_tx::TransactionExecutorError;
use miden_usdcx::account::xreserve::DOM_PAUSER_ROLE;
use miden_usdcx::note::xreserve_admin::XReserveSetAttesterNote;
use miden_usdcx::note::xreserve_mint::DepositAttestation;
use miden_usdcx::vectors::{MiVector, load};
use miden_usdcx::xreserve::encoding::Signature;
use support::*;
fn administrator() -> AccountId {
test_account_id(1)
}
fn dom_pauser() -> AccountId {
test_account_id(2)
}
fn new_pauser() -> AccountId {
test_account_id(4)
}
fn pauser_sym() -> RoleSymbol {
RoleSymbol::new(DOM_PAUSER_ROLE).expect("DOM_PAUSER is a fixed valid role symbol (<=12)")
}
fn err_paused() -> MasmError {
MasmError::from_static_str("the contract is paused")
}
fn err_sender_lacks_role() -> MasmError {
MasmError::from_static_str("note sender does not hold the required role")
}
fn dummy_config() -> Word {
Word::from([7u32, 0, 0, 0])
}
fn placeholder_driver_src() -> String {
"#! Test driver stand-in: never invoked by this suite (the custom mint entry was deleted by\n\
#! the Wave-1 S1 recomposition); the guarded fixture only requires a compilable component.\n\
#!\n\
#! Inputs: [pad(16)]\n\
#! Outputs: [pad(16)]\n\
#!\n\
#! Invocation: call\n\
@account_procedure\n\
pub proc drive\n\
\x20\x20\x20\x20push.0 drop\n\
end\n"
.to_string()
}
fn production_faucet() -> Result<GuardedMint> {
let driver = placeholder_driver_src();
let probe = composition_supply_probe_src(0);
let domain = dummy_config();
setup_guarded_mint_account(
GuardSelection::ProductionAttestation,
1_000_000,
0,
domain,
None,
None,
&driver,
&probe,
true,
)
}
const BASE_VECTOR: &str = "mi-pos-empty-hookdata";
const MINT_AMOUNT: u64 = 250_000_000;
const MAX_FEE_RAW: u64 = 1;
const REMOTE_RECIPIENT_BYTE_OFF: usize = 19 * 4;
const REMOTE_TOKEN_BYTE_OFF: usize = 11 * 4;
const NONCE_BYTE_OFF: usize = 51 * 4;
fn mi(id: &str) -> &'static MiVector {
load()
.families
.mi
.iter()
.find(|v| v.id == id)
.unwrap_or_else(|| panic!("canonical artifact is missing mp vector {id}"))
}
fn payload_for(
recipient: AccountId,
amount: u64,
nonce_variant: u8,
faucet_id: AccountId,
) -> Vec<u8> {
let mut payload = mi(BASE_VECTOR).payload();
payload[AMOUNT_BYTE_OFF..AMOUNT_BYTE_OFF + 32].copy_from_slice(&uint256_be(amount));
payload[MAX_FEE_BYTE_OFF..MAX_FEE_BYTE_OFF + 32].copy_from_slice(&uint256_be(MAX_FEE_RAW));
payload[REMOTE_RECIPIENT_BYTE_OFF..REMOTE_RECIPIENT_BYTE_OFF + 32]
.copy_from_slice(&EthEmbeddedAccountId::from_account_id(recipient).to_bytes32());
payload[REMOTE_TOKEN_BYTE_OFF..REMOTE_TOKEN_BYTE_OFF + 32]
.copy_from_slice(&EthEmbeddedAccountId::from_account_id(faucet_id).to_bytes32());
payload[NONCE_BYTE_OFF] ^= nonce_variant;
payload
}
fn note_rng(seed: u64) -> RandomCoin {
RandomCoin::new(Word::from([
Felt::from(seed as u32),
Felt::from((seed >> 32) as u32),
Felt::from(3u32),
Felt::from(4u32),
]))
}
fn role_action_note(
sender: AccountId,
faucet_id: AccountId,
action: RbacConfig,
seed: u32,
) -> Result<Note> {
let note = RbacConfigNote::builder()
.sender(sender)
.target(faucet_id)
.config(action)
.serial_number(Word::from([seed, 3, 4, 5]))
.build()
.map_err(|e| anyhow::anyhow!("building the standard role-action note: {e}"))?;
Ok(Note::from(note))
}
fn grant_role_note(
sender: AccountId,
faucet_id: AccountId,
member: AccountId,
seed: u32,
) -> Result<Note> {
role_action_note(
sender,
faucet_id,
RbacConfig::GrantRole { role: pauser_sym(), account: member },
seed,
)
}
fn revoke_role_note(
sender: AccountId,
faucet_id: AccountId,
member: AccountId,
seed: u32,
) -> Result<Note> {
role_action_note(
sender,
faucet_id,
RbacConfig::RevokeRole { role: pauser_sym(), account: member },
seed,
)
}
fn mint_fixture(extra_notes: impl Fn(AccountId) -> Vec<Note>) -> Result<ProductionFaucet> {
setup_production_faucet(0, |recipient, faucet_id| {
let commitment =
gen_attester(1, &payload_for(recipient, MINT_AMOUNT, 0, faucet_id)).commitment;
let route = faucet_id;
let mut notes = vec![
XReserveSetAttesterNote::create(
administrator(),
route,
commitment,
1,
&mut note_rng(952),
)
.expect("building the administrator set_attester note"),
];
notes.extend(extra_notes(faucet_id));
notes
})
}
async fn bring_up(pf: &mut ProductionFaucet, count: usize) -> Result<()> {
for (i, note) in pf.seeded_notes.clone().iter().take(count).enumerate() {
let tx = pf
.mock_chain
.build_transaction(pf.faucet_id)
.authenticated_input_note(note.id())
.build()
.with_context(|| format!("bring-up note {i}: tx build"))?
.execute()
.await
.map_err(|e| anyhow::anyhow!("bring-up note {i} must succeed: {e}"))?;
pf.mock_chain.add_pending_executed_transaction(&tx)?;
pf.mock_chain.prove_next_block()?;
}
Ok(())
}
async fn consume_note(
pf: &ProductionFaucet,
note: &Note,
) -> std::result::Result<ExecutedTransaction, TransactionExecutorError> {
pf.mock_chain
.build_transaction(pf.faucet_id)
.authenticated_input_note(note.id())
.build()
.expect("building the consume tx")
.execute()
.await
}
async fn consume_and_commit(pf: &mut ProductionFaucet, note: &Note, what: &str) -> Result<()> {
let tx = consume_note(pf, note).await.map_err(|e| anyhow::anyhow!("{what}: {e}"))?;
pf.mock_chain.add_pending_executed_transaction(&tx)?;
pf.mock_chain.prove_next_block()?;
Ok(())
}
async fn emit_and_consume_mint(
pf: &mut ProductionFaucet,
payload: &[u8],
rng_seed: u64,
) -> Result<std::result::Result<ExecutedTransaction, TransactionExecutorError>> {
let attester = gen_attester(1, payload);
let note = mint_note_from_payload(
pf.producer_id,
pf.faucet_id,
payload,
DepositAttestation::new(Signature::new(attester.sig_bytes), attester.pubkey.clone()),
&mut note_rng(rng_seed),
)?;
emit_note_with_attachments(&mut pf.mock_chain, pf.producer_id, ¬e).await?;
Ok(pf
.mock_chain
.build_transaction(pf.faucet_id)
.authenticated_input_note(note.id())
.build()
.context("building the mint consume tx")?
.execute()
.await)
}
#[tokio::test]
async fn administrator_grants_pauser_then_new_pauser_halts_mint() -> Result<()> {
let mut pf = mint_fixture(|faucet_id| {
vec![
grant_role_note(administrator(), faucet_id, new_pauser(), 31)
.expect("building the administrator grant_role note"),
stock_pause_note(new_pauser(), faucet_id, 32)
.expect("building the candidate's pause note"),
]
})?;
bring_up(&mut pf, 1).await?; let grant_note = pf.seeded_notes[1].clone();
let pause_note = pf.seeded_notes[2].clone();
let pre = consume_note(&pf, &pause_note).await;
assert_transaction_executor_error!(pre, err_sender_lacks_role());
consume_and_commit(&mut pf, &grant_note, "the administrator grant must pass").await?;
let faucet = pf.mock_chain.committed_account(pf.faucet_id)?.clone();
assert_eq!(
read_role_membership(&faucet, &pauser_sym(), new_pauser())?[0],
Felt::from(1u32),
"the administrator grant landed the membership flag"
);
consume_and_commit(
&mut pf,
&pause_note,
"the newly granted DOM_PAUSER member's pause must succeed",
)
.await?;
let payload = payload_for(pf.recipient_id, MINT_AMOUNT, 1, pf.faucet_id);
let result = emit_and_consume_mint(&mut pf, &payload, 33).await?;
assert_transaction_executor_error!(result, err_paused());
Ok(())
}
#[tokio::test]
async fn administrator_revokes_pauser_then_pause_rejects() -> Result<()> {
let gm = production_faucet()?;
let account = faucet_account(&gm.harness);
let revoked = run_revoke_role_against(
&gm.harness.mock_chain,
&account,
administrator(),
&pauser_sym(),
dom_pauser(),
34,
)
.await
.expect("the administrator revoke passes");
let mut evolved = account.clone();
evolved.apply_patch(revoked.account_patch())?;
assert_eq!(
read_role_membership(&evolved, &pauser_sym(), dom_pauser())?[0],
Felt::ZERO,
"the revoked member's membership flag is cleared"
);
let config = read_role_config(&evolved, &pauser_sym())?;
assert_eq!(config[0], Felt::ZERO, "DOM_PAUSER member_count decremented to 0");
assert_eq!(
config[1],
Felt::ZERO,
"ADMIN remains the effective admin after the last-member revoke"
);
let result = run_dom_pauser_pause(&gm.harness.mock_chain, &evolved, dom_pauser(), 35).await;
assert_transaction_executor_error!(result, err_sender_lacks_role());
assert_eq!(
read_is_paused(&evolved)?[0],
Felt::ZERO,
"a failed pause leaves is_paused untouched"
);
Ok(())
}
#[tokio::test]
async fn administrator_rotates_pauser_revoke_then_grant() -> Result<()> {
let mut pf = mint_fixture(|faucet_id| {
let route = faucet_id;
vec![
revoke_role_note(administrator(), route, dom_pauser(), 36)
.expect("building the administrator revoke_role note"),
grant_role_note(administrator(), route, new_pauser(), 37)
.expect("building the administrator grant_role note"),
stock_pause_note(dom_pauser(), route, 38)
.expect("building the OLD pauser's pause note"),
stock_pause_note(new_pauser(), route, 39)
.expect("building the NEW pauser's pause note"),
]
})?;
bring_up(&mut pf, 1).await?; let revoke_note = pf.seeded_notes[1].clone();
let grant_note = pf.seeded_notes[2].clone();
let old_pause_note = pf.seeded_notes[3].clone();
let new_pause_note = pf.seeded_notes[4].clone();
consume_and_commit(&mut pf, &revoke_note, "the rotation's revoke leg must pass under ADMIN")
.await?;
consume_and_commit(
&mut pf,
&grant_note,
"the rotation's grant leg must pass through the empty role (admin config retained)",
)
.await?;
let old = consume_note(&pf, &old_pause_note).await;
assert_transaction_executor_error!(old, err_sender_lacks_role());
consume_and_commit(
&mut pf,
&new_pause_note,
"the rotated-in DOM_PAUSER member's pause must succeed",
)
.await?;
let payload = payload_for(pf.recipient_id, MINT_AMOUNT, 2, pf.faucet_id);
let result = emit_and_consume_mint(&mut pf, &payload, 40).await?;
assert_transaction_executor_error!(result, err_paused());
Ok(())
}
#[tokio::test]
async fn shipped_role_graph_reads_back() -> Result<()> {
let gm = production_faucet()?;
let account = faucet_account(&gm.harness);
let marker = Word::from([1u32, 0, 0, 0]);
for (name, holder) in [
("ADMIN", 1),
("ATTEST_ADMIN", 1),
("DOM_PAUSER", 2),
("DOM_UNPAUSER", 3),
("BLK_MANAGER", 4),
] {
let role = RoleSymbol::new(name)?;
assert_eq!(
read_role_config(&account, &role)?,
marker,
"{name} has one member and resolves directly to ADMIN"
);
assert_eq!(
read_role_membership(&account, &role, test_account_id(holder))?,
marker,
"{name} is seeded on id({holder})"
);
}
assert_eq!(
read_role_config(&account, &RoleSymbol::new("DOM_MANAGER")?)?,
Word::empty(),
"the retired role is absent"
);
Ok(())
}