mod support;
use anyhow::Result;
use miden_protocol::account::{Account, AccountId, RoleSymbol, StorageMapKey};
use miden_protocol::note::Note;
use miden_protocol::{Felt, Word};
use miden_standards::account::access::RoleBasedAccessControl;
use miden_standards::note::config::{RbacConfig, RbacConfigNote};
use miden_testing::assert_transaction_executor_error;
use miden_usdcx::account::xreserve::XReserveFaucetExtension;
use miden_usdcx::note::xreserve_admin::XReserveSetAttesterNote;
use support::w2admin::*;
use support::*;
const MAX_SUPPLY: u64 = 1_000_000;
fn read_role_member_count(account: &Account, role: &RoleSymbol) -> Result<Felt> {
Ok(read_role_config(account, role)?[0])
}
fn read_role_config(account: &Account, role: &RoleSymbol) -> Result<Word> {
let key = Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::from(role)]);
account
.storage()
.get_map_item(RoleBasedAccessControl::role_config_slot(), StorageMapKey::new(key))
.map_err(|e| anyhow::anyhow!("reading role_config[{role}]: {e}"))
}
fn read_role_membership(account: &Account, role: &RoleSymbol, member: AccountId) -> Result<Word> {
let key =
Word::from([Felt::ZERO, Felt::from(role), member.suffix(), member.prefix().as_felt()]);
account
.storage()
.get_map_item(RoleBasedAccessControl::role_membership_slot(), StorageMapKey::new(key))
.map_err(|e| anyhow::anyhow!("reading role_membership[{role}][{member}]: {e}"))
}
fn read_attester(account: &Account, commitment: Word) -> Result<Word> {
account
.storage()
.get_map_item(
XReserveFaucetExtension::xreserve_attesters_slot(),
StorageMapKey::new(commitment),
)
.map_err(|e| anyhow::anyhow!("reading xReserveAttesters[{commitment}]: {e}"))
}
fn role_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, 53, 59, 61]))
.build()
.map_err(|e| anyhow::anyhow!("building the standard role-action note: {e}"))?;
Ok(Note::from(note))
}
#[tokio::test]
async fn every_remaining_admin_note_still_lands() -> Result<()> {
let commitment = Word::from([7u32, 8, 9, 10]);
let new_max_supply = MAX_SUPPLY * 2;
let new_min_burn = 42u64;
let mut pf = admin_faucet(|faucet_id| {
vec![
XReserveSetAttesterNote::create(
admin_holder(),
faucet_id,
commitment,
1,
&mut note_rng(501),
)
.expect("the set_attester note builds"),
stock_set_max_supply_note(admin_holder(), faucet_id, new_max_supply, 502)
.expect("the set_max_supply note builds"),
stock_min_burn_note(admin_holder(), faucet_id, new_min_burn, 503)
.expect("the min-burn note builds"),
stock_pause_note(pauser_holder(), faucet_id, 504).expect("the pause note builds"),
stock_block_note(blocklist_holder(), faucet_id, stranger(), 505)
.expect("the block note builds"),
]
})?;
let notes: Vec<Note> = pf.seeded_notes.clone();
let after = consume_and_commit(&mut pf, ¬es[0], "set_attester").await?;
assert_eq!(
read_attester(&after, commitment)?,
set_word(),
"the attester setter must still write its allowlist entry"
);
let after = consume_and_commit(&mut pf, ¬es[1], "set_max_supply").await?;
assert_eq!(
read_token_config(&after)?[1],
Felt::try_from(new_max_supply).expect("the new supply cap is a valid felt"),
"the supply cap setter must still land"
);
let after = consume_and_commit(&mut pf, ¬es[2], "min_burn").await?;
assert_eq!(
read_min_burn_size(&after)?[0],
Felt::try_from(new_min_burn).expect("the new burn floor is a valid felt"),
"the burn-floor setter must still land"
);
let after = consume_and_commit(&mut pf, ¬es[3], "pause").await?;
assert_eq!(
read_paused(&after)?,
set_word(),
"the Domain pauser must still be able to pause"
);
let after = consume_and_commit(&mut pf, ¬es[4], "block_account").await?;
assert_eq!(
read_blocked(&after, stranger())?,
set_word(),
"the blocklist administrator must still be able to block"
);
Ok(())
}
fn successor() -> AccountId {
test_account_id(77)
}
#[tokio::test]
async fn the_administrator_role_hands_over_by_grant_then_revoke() -> Result<()> {
let admin = RoleBasedAccessControl::admin_role();
let successor_min_burn = 21u32;
let predecessor_min_burn = 31u32;
let mut pf = admin_faucet(|faucet_id| {
vec![
stock_min_burn_note(successor(), faucet_id, successor_min_burn.into(), 601)
.expect("the successor's min-burn note builds"),
role_note(
admin_holder(),
faucet_id,
RbacConfig::GrantRole {
role: RoleBasedAccessControl::admin_role(),
account: successor(),
},
602,
)
.expect("the ADMIN grant note builds"),
stock_min_burn_note(successor(), faucet_id, successor_min_burn.into(), 603)
.expect("the successor's second min-burn note builds"),
role_note(
successor(),
faucet_id,
RbacConfig::RevokeRole {
role: RoleBasedAccessControl::admin_role(),
account: admin_holder(),
},
604,
)
.expect("the ADMIN revoke note builds"),
stock_min_burn_note(admin_holder(), faucet_id, predecessor_min_burn.into(), 605)
.expect("the predecessor's min-burn note builds"),
]
})?;
let notes: Vec<Note> = pf.seeded_notes.clone();
let refused = consume(&pf, ¬es[0]).await;
assert_transaction_executor_error!(refused, err_sender_lacks_role());
let granted = consume_and_commit(&mut pf, ¬es[1], "grant ADMIN to the successor").await?;
assert_eq!(
read_role_membership(&granted, &admin, successor())?,
set_word(),
"the successor must now hold the administrator role"
);
assert_eq!(
read_role_member_count(&granted, &admin)?,
Felt::from(2u32),
"the role must carry both administrators during the handover window"
);
let after = consume_and_commit(&mut pf, ¬es[2], "successor min-burn update").await?;
assert_eq!(
read_min_burn_size(&after)?,
Word::from([successor_min_burn, 0, 0, 0]),
"the successor must gain the administrator-gated capability with the role"
);
let revoked =
consume_and_commit(&mut pf, ¬es[3], "revoke ADMIN from the predecessor").await?;
assert_eq!(
read_role_membership(&revoked, &admin, admin_holder())?,
Word::empty(),
"the predecessor must no longer hold the administrator role"
);
assert_eq!(
read_role_member_count(&revoked, &admin)?,
Felt::from(1u32),
"the successor must be the sole administrator once the handover completes"
);
let refused = consume(&pf, ¬es[4]).await;
assert_transaction_executor_error!(refused, err_sender_lacks_role());
let final_state = pf.mock_chain.committed_account(pf.faucet_id)?.clone();
assert_eq!(
read_min_burn_size(&final_state)?,
Word::from([successor_min_burn, 0, 0, 0]),
"the refused predecessor write must leave the burn floor untouched"
);
Ok(())
}
#[tokio::test]
async fn only_an_administrator_can_grant_the_administrator_role() -> Result<()> {
let pf = admin_faucet(|faucet_id| {
vec![
role_note(
unpauser_holder(),
faucet_id,
RbacConfig::GrantRole {
role: RoleBasedAccessControl::admin_role(),
account: successor(),
},
606,
)
.expect("the ADMIN grant note builds"),
]
})?;
let note = pf.seeded_notes[0].clone();
let result = consume(&pf, ¬e).await;
assert_transaction_executor_error!(result, err_sender_not_role_admin());
let account = pf.mock_chain.committed_account(pf.faucet_id)?.clone();
assert_eq!(
read_role_membership(&account, &RoleBasedAccessControl::admin_role(), successor())?,
Word::empty(),
"the refused grant must leave the administrator role's membership untouched"
);
Ok(())
}