mod support;
use std::collections::BTreeMap;
use anyhow::{Context, Result};
use miden_processor::crypto::random::RandomCoin;
use miden_protocol::account::{
Account,
AccountId,
RoleSymbol,
StorageMapKey,
StorageSlotName,
StorageSlotPatch,
};
use miden_protocol::crypto::rand::FeltRng;
use miden_protocol::errors::MasmError;
use miden_protocol::note::Note;
use miden_protocol::transaction::ExecutedTransaction;
use miden_protocol::{Felt, Word};
use miden_standards::account::access::PausableStorage;
use miden_standards::account::faucets::FungibleFaucet;
use miden_standards::note::config::{RbacConfig, RbacConfigNote};
use miden_testing::{MockChain, assert_transaction_executor_error};
use miden_usdcx::account::xreserve::{BLK_MANAGER_ROLE, DOM_PAUSER_ROLE, XReserveFaucetExtension};
use miden_usdcx::note::xreserve_admin::XReserveSetAttesterNote;
use support::*;
fn err_not_role_admin() -> MasmError {
MasmError::from_static_str("note sender does not hold the role's admin role")
}
fn pauser_sym() -> RoleSymbol {
RoleSymbol::new(DOM_PAUSER_ROLE).expect("DOM_PAUSER is a fixed valid role symbol")
}
fn blk_manager_sym() -> RoleSymbol {
RoleSymbol::new(BLK_MANAGER_ROLE).expect("BLK_MANAGER is a fixed valid role symbol")
}
fn err_account_not_in_role() -> MasmError {
MasmError::from_static_str("account does not hold the role")
}
fn member_marker() -> Word {
Word::from([1u32, 0, 0, 0])
}
fn note_rng(seed: u64) -> RandomCoin {
RandomCoin::new(Word::from([
Felt::from(seed as u32),
Felt::from((seed >> 32) as u32),
Felt::from(7u32),
Felt::from(11u32),
]))
}
fn stock_role_note<R: FeltRng>(
sender: AccountId,
faucet_id: AccountId,
action: RbacConfig,
rng: &mut R,
) -> Result<Note> {
let note = RbacConfigNote::builder()
.sender(sender)
.target(faucet_id)
.config(action)
.serial_number(rng.draw_word())
.build()
.map_err(|e| anyhow::anyhow!("building the standard role-action note: {e}"))?;
Ok(Note::from(note))
}
fn stock_grant_role_note<R: FeltRng>(
sender: AccountId,
faucet_id: AccountId,
role: RoleSymbol,
member: AccountId,
rng: &mut R,
) -> Result<Note> {
stock_role_note(sender, faucet_id, RbacConfig::GrantRole { role, account: member }, rng)
}
fn stock_revoke_role_note<R: FeltRng>(
sender: AccountId,
faucet_id: AccountId,
role: RoleSymbol,
member: AccountId,
rng: &mut R,
) -> Result<Note> {
stock_role_note(sender, faucet_id, RbacConfig::RevokeRole { role, account: member }, rng)
}
#[tokio::test]
async fn set_attester_admin_note_admin_writes_and_nonadmin_traps() -> Result<()> {
let pf = setup_production_faucet(0, |_, _faucet_id| Vec::new())
.context("building the production network-auth faucet")?;
let chain = pf.mock_chain;
let faucet_id = pf.faucet_id;
let owner = test_account_id(1);
let commitment = gen_attester(1, b"attester").commitment;
let note = XReserveSetAttesterNote::create(owner, faucet_id, commitment, 1, &mut note_rng(1))
.context("building the administrator set_attester note")?;
let tx = chain
.build_transaction(faucet_id)
.unauthenticated_input_note(note.clone())
.build()
.context("owner set_attester tx build")?
.execute()
.await
.map_err(|e| {
anyhow::anyhow!("owner-sent set_attester must succeed under network auth: {e}")
})?;
let StorageSlotPatch::Map(delta) = tx
.account_patch()
.storage()
.get(XReserveFaucetExtension::xreserve_attesters_slot())
.context("xReserveAttesters slot delta")?
else {
panic!("xReserveAttesters must be a Map slot delta");
};
let written = delta
.entries()
.expect("map patch carries entries")
.as_map()
.get(&StorageMapKey::new(commitment))
.copied()
.context("the commitment key must appear in the xReserveAttesters delta")?;
assert_eq!(
written,
Word::from([1u32, 0, 0, 0]),
"owner set_attester must write the enabled marker at the creator-committed commitment key \
(storage-param marshaling correct)",
);
let bad = XReserveSetAttesterNote::create(
test_account_id(9),
faucet_id,
commitment,
0,
&mut note_rng(2),
)
.context("building the non-administrator set_attester note")?;
let result = chain
.build_transaction(faucet_id)
.unauthenticated_input_note(bad.clone())
.build()
.context("non-administrator set_attester tx build")?
.execute()
.await;
assert_transaction_executor_error!(result, err_sender_lacks_role());
Ok(())
}
fn value_delta(tx: &ExecutedTransaction, name: &StorageSlotName) -> Word {
match tx.account_patch().storage().get(name) {
Some(StorageSlotPatch::Value(w)) => w.value().expect("value patch carries a value"),
other => panic!("value slot {name} expected a value delta, got {other:?}"),
}
}
fn scalar_word(f: Felt) -> Word {
Word::from([f, Felt::from(0u32), Felt::from(0u32), Felt::from(0u32)])
}
const NEW_MIN_BURN: u64 = 5_000;
fn expected_min_burn() -> Word {
scalar_word(Felt::try_from(NEW_MIN_BURN).expect("min burn within the field"))
}
#[tokio::test]
async fn min_burn_administrator_writes_slot() -> Result<()> {
let pf = setup_production_faucet(0, |_, _faucet_id| Vec::new())
.context("building the production network-auth faucet")?;
let chain = pf.mock_chain;
let faucet_id = pf.faucet_id;
let owner = test_account_id(1);
let note = stock_min_burn_note(owner, faucet_id, NEW_MIN_BURN, 40)
.context("building the administrator min-burn note")?;
let tx = chain
.build_transaction(faucet_id)
.unauthenticated_input_note(note.clone())
.build()
.context("owner min-burn tx build")?
.execute()
.await
.map_err(|e| {
anyhow::anyhow!("owner-sent min-burn note must succeed under network auth: {e}")
})?;
let mut evolved = chain.committed_account(faucet_id).context("committed faucet")?.clone();
evolved.apply_patch(tx.account_patch())?;
assert_eq!(
read_min_burn_size(&evolved)?,
expected_min_burn(),
"the owner min-burn note must write [new_min,0,0,0] into the STOCK MinBurnAmount slot",
);
Ok(())
}
async fn assert_min_burn_nonadmin_traps(sender: AccountId, seed: u64) -> Result<()> {
let pf = setup_production_faucet(0, |_, _faucet_id| Vec::new())
.context("building the production network-auth faucet")?;
let chain = pf.mock_chain;
let faucet_id = pf.faucet_id;
let note = stock_min_burn_note(sender, faucet_id, NEW_MIN_BURN, seed)
.context("building the non-administrator min-burn note")?;
let result = chain
.build_transaction(faucet_id)
.unauthenticated_input_note(note.clone())
.build()
.context("non-administrator min-burn tx build")?
.execute()
.await;
assert_transaction_executor_error!(result, err_sender_lacks_role());
Ok(())
}
#[tokio::test]
async fn min_burn_dom_pauser_traps() -> Result<()> {
assert_min_burn_nonadmin_traps(test_account_id(2), 41).await
}
#[tokio::test]
async fn min_burn_dom_unpauser_traps() -> Result<()> {
assert_min_burn_nonadmin_traps(test_account_id(3), 42).await
}
#[tokio::test]
async fn min_burn_third_party_traps() -> Result<()> {
assert_min_burn_nonadmin_traps(test_account_id(99), 43).await
}
#[tokio::test]
async fn min_burn_note_args_are_inert() -> Result<()> {
let pf = setup_production_faucet(0, |_, _faucet_id| Vec::new())
.context("building the production network-auth faucet")?;
let chain = pf.mock_chain;
let faucet_id = pf.faucet_id;
let owner = test_account_id(1);
let note = stock_min_burn_note(owner, faucet_id, NEW_MIN_BURN, 44)
.context("building the administrator min-burn note")?;
let bogus_args = Word::from([999u32, 1, 2, 3]);
let tx = chain
.build_transaction(faucet_id)
.unauthenticated_input_note(note.clone())
.extend_note_args(BTreeMap::from([(note.id(), bogus_args)]))
.build()
.context("min-burn note-args tx build")?
.execute()
.await
.map_err(|e| {
anyhow::anyhow!("the min-burn note with bogus NOTE_ARGS must still succeed: {e}")
})?;
let mut evolved = chain.committed_account(faucet_id).context("committed faucet")?.clone();
evolved.apply_patch(tx.account_patch())?;
assert_eq!(
read_min_burn_size(&evolved)?,
expected_min_burn(),
"the min-burn note must write the storage-committed param regardless of executor NOTE_ARGS",
);
Ok(())
}
#[tokio::test]
async fn pause_dom_pauser_sets_is_paused() -> Result<()> {
let pf = setup_production_faucet(0, |_, _faucet_id| Vec::new())
.context("building the production network-auth faucet")?;
let chain = pf.mock_chain;
let faucet_id = pf.faucet_id;
let note = stock_pause_note(test_account_id(2), faucet_id, 50)
.context("building the DOM_PAUSER pause note")?;
let tx = chain
.build_transaction(faucet_id)
.unauthenticated_input_note(note.clone())
.build()
.context("DOM_PAUSER pause tx build")?
.execute()
.await
.map_err(|e| anyhow::anyhow!("DOM_PAUSER pause must succeed under network auth: {e}"))?;
assert_eq!(
value_delta(&tx, PausableStorage::is_paused_slot()),
scalar_word(Felt::from(1u32)),
"DOM_PAUSER pause must set is_paused = 1",
);
Ok(())
}
async fn assert_pause_nonpauser_traps(sender: AccountId, seed: u64) -> Result<()> {
let pf = setup_production_faucet(0, |_, _faucet_id| Vec::new())
.context("building the production network-auth faucet")?;
let chain = pf.mock_chain;
let faucet_id = pf.faucet_id;
let note =
stock_pause_note(sender, faucet_id, seed).context("building the non-pauser pause note")?;
let result = chain
.build_transaction(faucet_id)
.unauthenticated_input_note(note.clone())
.build()
.context("non-pauser pause tx build")?
.execute()
.await;
assert_transaction_executor_error!(result, err_sender_lacks_role());
Ok(())
}
#[tokio::test]
async fn pause_administrator_traps() -> Result<()> {
assert_pause_nonpauser_traps(test_account_id(1), 51).await
}
#[tokio::test]
async fn pause_third_party_traps() -> Result<()> {
assert_pause_nonpauser_traps(test_account_id(99), 52).await
}
#[tokio::test]
async fn pause_note_args_are_inert() -> Result<()> {
let pf = setup_production_faucet(0, |_, _faucet_id| Vec::new())
.context("building the production network-auth faucet")?;
let chain = pf.mock_chain;
let faucet_id = pf.faucet_id;
let note = stock_pause_note(test_account_id(2), faucet_id, 53)
.context("building the DOM_PAUSER pause note")?;
let bogus_args = Word::from([5u32, 5, 5, 5]);
let tx = chain
.build_transaction(faucet_id)
.unauthenticated_input_note(note.clone())
.extend_note_args(BTreeMap::from([(note.id(), bogus_args)]))
.build()
.context("pause note-args tx build")?
.execute()
.await
.map_err(|e| anyhow::anyhow!("pause with bogus NOTE_ARGS must still succeed: {e}"))?;
assert_eq!(
value_delta(&tx, PausableStorage::is_paused_slot()),
scalar_word(Felt::from(1u32)),
"pause must set is_paused=1 regardless of executor NOTE_ARGS",
);
Ok(())
}
async fn paused_faucet() -> Result<(MockChain, AccountId)> {
let pf = setup_production_faucet(0, |_, faucet_id| {
vec![
stock_pause_note(test_account_id(2), faucet_id, 60)
.expect("building the seeded pause note"),
]
})
.context("building the production faucet with a seeded pause")?;
let mut chain = pf.mock_chain;
let faucet_id = pf.faucet_id;
for note in pf.seeded_notes.clone() {
let tx = chain
.build_transaction(faucet_id)
.authenticated_input_note(note.id())
.build()
.context("pause bring-up tx build")?
.execute()
.await
.map_err(|e| anyhow::anyhow!("pause bring-up must succeed: {e}"))?;
chain.add_pending_executed_transaction(&tx)?;
chain.prove_next_block()?;
}
Ok((chain, faucet_id))
}
#[tokio::test]
async fn unpause_dom_unpauser_clears_is_paused() -> Result<()> {
let (chain, faucet_id) = paused_faucet().await?;
let note = stock_unpause_note(test_account_id(3), faucet_id, 61)
.context("building the DOM_UNPAUSER unpause note")?;
let tx = chain
.build_transaction(faucet_id)
.unauthenticated_input_note(note.clone())
.build()
.context("DOM_UNPAUSER unpause tx build")?
.execute()
.await
.map_err(|e| {
anyhow::anyhow!("DOM_UNPAUSER unpause must succeed under network auth: {e}")
})?;
assert_eq!(
value_delta(&tx, PausableStorage::is_paused_slot()),
scalar_word(Felt::from(0u32)),
"DOM_UNPAUSER unpause must clear is_paused to 0",
);
Ok(())
}
async fn assert_unpause_nonpauser_traps(sender: AccountId, seed: u64) -> Result<()> {
let pf = setup_production_faucet(0, |_, _faucet_id| Vec::new())
.context("building the production network-auth faucet")?;
let chain = pf.mock_chain;
let faucet_id = pf.faucet_id;
let note = stock_unpause_note(sender, faucet_id, seed)
.context("building the unpause note without DOM_UNPAUSER")?;
let result = chain
.build_transaction(faucet_id)
.unauthenticated_input_note(note.clone())
.build()
.context("unpause tx without DOM_UNPAUSER build")?
.execute()
.await;
assert_transaction_executor_error!(result, err_sender_lacks_role());
Ok(())
}
#[tokio::test]
async fn unpause_administrator_traps() -> Result<()> {
assert_unpause_nonpauser_traps(test_account_id(1), 62).await
}
#[tokio::test]
async fn unpause_third_party_traps() -> Result<()> {
assert_unpause_nonpauser_traps(test_account_id(99), 63).await
}
#[tokio::test]
async fn unpause_dom_pauser_traps() -> Result<()> {
assert_unpause_nonpauser_traps(test_account_id(2), 65).await
}
#[tokio::test]
async fn unpause_note_args_are_inert() -> Result<()> {
let (chain, faucet_id) = paused_faucet().await?;
let note = stock_unpause_note(test_account_id(3), faucet_id, 64)
.context("building the DOM_UNPAUSER unpause note")?;
let bogus_args = Word::from([8u32, 8, 8, 8]);
let tx = chain
.build_transaction(faucet_id)
.unauthenticated_input_note(note.clone())
.extend_note_args(BTreeMap::from([(note.id(), bogus_args)]))
.build()
.context("unpause note-args tx build")?
.execute()
.await
.map_err(|e| anyhow::anyhow!("unpause with bogus NOTE_ARGS must still succeed: {e}"))?;
assert_eq!(
value_delta(&tx, PausableStorage::is_paused_slot()),
scalar_word(Felt::from(0u32)),
"unpause must clear is_paused=0 regardless of executor NOTE_ARGS",
);
Ok(())
}
async fn assert_grant_role_authorized(
sender: AccountId,
role: RoleSymbol,
seed: u64,
) -> Result<()> {
let pf = setup_production_faucet(0, |_, _faucet_id| Vec::new())
.context("building the production network-auth faucet")?;
let chain = pf.mock_chain;
let faucet_id = pf.faucet_id;
let grantee = test_account_id(4);
let note = stock_grant_role_note(sender, faucet_id, role.clone(), grantee, &mut note_rng(seed))
.context("building the grant_role note")?;
let tx = chain
.build_transaction(faucet_id)
.unauthenticated_input_note(note.clone())
.build()
.context("grant_role tx build")?
.execute()
.await
.map_err(|e| {
anyhow::anyhow!("authorized grant_role must succeed under network auth: {e}")
})?;
let mut evolved = chain.committed_account(faucet_id).context("committed faucet")?.clone();
evolved.apply_patch(tx.account_patch())?;
assert_eq!(
read_role_membership(&evolved, &role, grantee)?,
member_marker(),
"authorized grant_role must make id(4) a role member",
);
Ok(())
}
#[tokio::test]
async fn grant_role_administrator_authorized() -> Result<()> {
assert_grant_role_authorized(test_account_id(1), pauser_sym(), 70).await
}
#[tokio::test]
async fn grant_role_third_party_traps() -> Result<()> {
let pf = setup_production_faucet(0, |_, _faucet_id| Vec::new())
.context("building the production network-auth faucet")?;
let chain = pf.mock_chain;
let faucet_id = pf.faucet_id;
let note = stock_grant_role_note(
test_account_id(99),
faucet_id,
pauser_sym(),
test_account_id(4),
&mut note_rng(72),
)
.context("building the third-party grant_role note")?;
let result = chain
.build_transaction(faucet_id)
.unauthenticated_input_note(note.clone())
.build()
.context("third-party grant_role tx build")?
.execute()
.await;
assert_transaction_executor_error!(result, err_not_role_admin());
Ok(())
}
#[tokio::test]
async fn grant_role_note_args_are_inert() -> Result<()> {
let pf = setup_production_faucet(0, |_, _faucet_id| Vec::new())
.context("building the production network-auth faucet")?;
let chain = pf.mock_chain;
let faucet_id = pf.faucet_id;
let grantee = test_account_id(5);
let note = stock_grant_role_note(
test_account_id(1),
faucet_id,
pauser_sym(),
grantee,
&mut note_rng(73),
)
.context("building the administrator grant_role note")?;
let bogus_args = Word::from([7u32, 7, 7, 7]);
let tx = chain
.build_transaction(faucet_id)
.unauthenticated_input_note(note.clone())
.extend_note_args(BTreeMap::from([(note.id(), bogus_args)]))
.build()
.context("grant_role note-args tx build")?
.execute()
.await
.map_err(|e| anyhow::anyhow!("grant_role with bogus NOTE_ARGS must still succeed: {e}"))?;
let mut evolved = chain.committed_account(faucet_id).context("committed faucet")?.clone();
evolved.apply_patch(tx.account_patch())?;
assert_eq!(
read_role_membership(&evolved, &pauser_sym(), grantee)?,
member_marker(),
"grant_role must write the storage-committed member regardless of executor NOTE_ARGS",
);
Ok(())
}
const NEW_MAX_SUPPLY: u64 = 2_000_000;
#[tokio::test]
async fn set_max_supply_administrator_writes_cap() -> Result<()> {
let pf = setup_production_faucet(0, |_, _faucet_id| Vec::new())
.context("building the production network-auth faucet")?;
let chain = pf.mock_chain;
let faucet_id = pf.faucet_id;
let note = stock_set_max_supply_note(test_account_id(1), faucet_id, NEW_MAX_SUPPLY, 90)
.context("building the administrator set_max_supply note")?;
let tx = chain
.build_transaction(faucet_id)
.unauthenticated_input_note(note.clone())
.build()
.context("owner set_max_supply tx build")?
.execute()
.await
.map_err(|e| {
anyhow::anyhow!("owner-sent set_max_supply must succeed under network auth: {e}")
})?;
assert_eq!(
value_delta(&tx, FungibleFaucet::token_config_slot())[1],
Felt::try_from(NEW_MAX_SUPPLY).expect("cap within the field"),
"owner set_max_supply must write word[1] = the new cap",
);
Ok(())
}
async fn assert_set_max_supply_nonadmin_traps(sender: AccountId, seed: u64) -> Result<()> {
let pf = setup_production_faucet(0, |_, _faucet_id| Vec::new())
.context("building the production network-auth faucet")?;
let chain = pf.mock_chain;
let faucet_id = pf.faucet_id;
let note = stock_set_max_supply_note(sender, faucet_id, NEW_MAX_SUPPLY, seed)
.context("building the non-administrator set_max_supply note")?;
let result = chain
.build_transaction(faucet_id)
.unauthenticated_input_note(note.clone())
.build()
.context("non-administrator set_max_supply tx build")?
.execute()
.await;
assert_transaction_executor_error!(result, err_sender_lacks_role());
Ok(())
}
#[tokio::test]
async fn set_max_supply_dom_pauser_traps() -> Result<()> {
assert_set_max_supply_nonadmin_traps(test_account_id(2), 91).await
}
#[tokio::test]
async fn set_max_supply_dom_unpauser_traps() -> Result<()> {
assert_set_max_supply_nonadmin_traps(test_account_id(3), 92).await
}
#[tokio::test]
async fn set_max_supply_third_party_traps() -> Result<()> {
assert_set_max_supply_nonadmin_traps(test_account_id(99), 93).await
}
#[tokio::test]
async fn set_max_supply_note_args_are_inert() -> Result<()> {
let pf = setup_production_faucet(0, |_, _faucet_id| Vec::new())
.context("building the production network-auth faucet")?;
let chain = pf.mock_chain;
let faucet_id = pf.faucet_id;
let note = stock_set_max_supply_note(test_account_id(1), faucet_id, NEW_MAX_SUPPLY, 94)
.context("building the administrator set_max_supply note")?;
let bogus_args = Word::from([3u32, 3, 3, 3]);
let tx = chain
.build_transaction(faucet_id)
.unauthenticated_input_note(note.clone())
.extend_note_args(BTreeMap::from([(note.id(), bogus_args)]))
.build()
.context("set_max_supply note-args tx build")?
.execute()
.await
.map_err(|e| {
anyhow::anyhow!("set_max_supply with bogus NOTE_ARGS must still succeed: {e}")
})?;
assert_eq!(
value_delta(&tx, FungibleFaucet::token_config_slot())[1],
Felt::try_from(NEW_MAX_SUPPLY).expect("cap within the field"),
"set_max_supply must write the storage-committed cap regardless of executor NOTE_ARGS",
);
Ok(())
}
async fn faucet_with_granted_role(
role: RoleSymbol,
grantor: AccountId,
grant_seed: u64,
) -> Result<(MockChain, AccountId, Account, AccountId)> {
let pf = setup_production_faucet(0, |_, _faucet_id| Vec::new())
.context("building the production network-auth faucet")?;
let chain = pf.mock_chain;
let faucet_id = pf.faucet_id;
let grantee = test_account_id(4);
let grant =
stock_grant_role_note(grantor, faucet_id, role.clone(), grantee, &mut note_rng(grant_seed))
.context("building the seeding grant note")?;
let tx = chain
.build_transaction(faucet_id)
.unauthenticated_input_note(grant.clone())
.build()
.context("grant seed tx build")?
.execute()
.await
.map_err(|e| anyhow::anyhow!("seeding the administrator grant must succeed: {e}"))?;
let mut evolved = chain.committed_account(faucet_id).context("committed faucet")?.clone();
evolved.apply_patch(tx.account_patch())?;
Ok((chain, faucet_id, evolved, grantee))
}
async fn assert_revoke_authorized(
sender: AccountId,
role: RoleSymbol,
grantor: AccountId,
grant_seed: u64,
revoke_seed: u64,
) -> Result<()> {
let (chain, faucet_id, evolved, grantee) =
faucet_with_granted_role(role.clone(), grantor, grant_seed).await?;
let note = stock_revoke_role_note(
sender,
faucet_id,
role.clone(),
grantee,
&mut note_rng(revoke_seed),
)
.context("building the revoke note")?;
let tx = chain
.build_transaction(evolved.clone())
.unauthenticated_input_note(note.clone())
.build()
.context("authorized revoke tx build")?
.execute()
.await
.map_err(|e| anyhow::anyhow!("authorized revoke must succeed under network auth: {e}"))?;
let mut evolved2 = evolved.clone();
evolved2.apply_patch(tx.account_patch())?;
assert_eq!(
read_role_membership(&evolved2, &role, grantee)?,
Word::from([0u32, 0, 0, 0]),
"authorized revoke must clear id(4)'s role membership",
);
Ok(())
}
#[tokio::test]
async fn revoke_role_administrator_authorized() -> Result<()> {
assert_revoke_authorized(test_account_id(1), pauser_sym(), test_account_id(1), 110, 100).await
}
#[tokio::test]
async fn revoke_role_third_party_traps() -> Result<()> {
let (chain, faucet_id, evolved, grantee) =
faucet_with_granted_role(pauser_sym(), test_account_id(1), 112).await?;
let note = stock_revoke_role_note(
test_account_id(99),
faucet_id,
pauser_sym(),
grantee,
&mut note_rng(102),
)
.context("building the third-party revoke note")?;
let result = chain
.build_transaction(evolved)
.unauthenticated_input_note(note.clone())
.build()
.context("third-party revoke tx build")?
.execute()
.await;
assert_transaction_executor_error!(result, err_not_role_admin());
Ok(())
}
#[tokio::test]
async fn revoke_role_note_args_are_inert() -> Result<()> {
let (chain, faucet_id, evolved, grantee) =
faucet_with_granted_role(pauser_sym(), test_account_id(1), 113).await?;
let note = stock_revoke_role_note(
test_account_id(1),
faucet_id,
pauser_sym(),
grantee,
&mut note_rng(103),
)
.context("building the administrator revoke note")?;
let bogus_args = Word::from([6u32, 6, 6, 6]);
let tx = chain
.build_transaction(evolved.clone())
.unauthenticated_input_note(note.clone())
.extend_note_args(BTreeMap::from([(note.id(), bogus_args)]))
.build()
.context("revoke note-args tx build")?
.execute()
.await
.map_err(|e| anyhow::anyhow!("revoke with bogus NOTE_ARGS must still succeed: {e}"))?;
let mut evolved2 = evolved.clone();
evolved2.apply_patch(tx.account_patch())?;
assert_eq!(
read_role_membership(&evolved2, &pauser_sym(), grantee)?,
Word::from([0u32, 0, 0, 0]),
"revoke must clear membership regardless of executor NOTE_ARGS",
);
Ok(())
}
#[tokio::test]
async fn set_role_admin_administrator_authorized() -> Result<()> {
let pf = setup_production_faucet(0, |_, _faucet_id| Vec::new())
.context("building the production network-auth faucet")?;
let chain = pf.mock_chain;
let faucet_id = pf.faucet_id;
let before = read_role_config(
&chain
.committed_account(faucet_id)
.context("reading the committed faucet")?
.clone(),
&pauser_sym(),
)?;
assert_eq!(
before[1],
Felt::ZERO,
"the build seed must administer DOM_PAUSER directly through ADMIN"
);
let note = stock_role_note(
test_account_id(1),
faucet_id,
RbacConfig::SetRoleAdmin {
role: pauser_sym(),
admin_role: Some(blk_manager_sym()),
},
&mut note_rng(150),
)
.context("building the administrator set_role_admin note")?;
let account = chain
.committed_account(faucet_id)
.context("reading the committed faucet")?
.clone();
let tx = chain
.build_transaction(faucet_id)
.unauthenticated_input_note(note.clone())
.build()
.context("set_role_admin tx build")?
.execute()
.await
.map_err(|e| anyhow::anyhow!("the administrator's set_role_admin must succeed: {e}"))?;
let mut evolved = account;
evolved.apply_patch(tx.account_patch())?;
let after = read_role_config(&evolved, &pauser_sym())?;
assert_eq!(
after[1],
Felt::from(&blk_manager_sym()),
"ADMIN must be able to re-point the role it administers",
);
assert_eq!(after[0], before[0], "re-pointing must preserve the role's member count",);
Ok(())
}
async fn assert_set_role_admin_rejected(sender: AccountId, seed: u64) -> Result<()> {
let pf = setup_production_faucet(0, |_, _faucet_id| Vec::new())
.context("building the production network-auth faucet")?;
let chain = pf.mock_chain;
let faucet_id = pf.faucet_id;
let account = chain
.committed_account(faucet_id)
.context("reading the committed faucet")?
.clone();
let before = read_role_config(&account, &pauser_sym())?;
let note = stock_role_note(
sender,
faucet_id,
RbacConfig::SetRoleAdmin { role: pauser_sym(), admin_role: None },
&mut note_rng(seed),
)
.context("building the unauthorized set_role_admin note")?;
let result = chain
.build_transaction(faucet_id)
.unauthenticated_input_note(note.clone())
.build()
.context("unauthorized set_role_admin tx build")?
.execute()
.await;
assert_transaction_executor_error!(result, err_not_role_admin());
assert_eq!(
read_role_config(&account, &pauser_sym())?,
before,
"a rejected set_role_admin leaves the role config untouched",
);
Ok(())
}
#[tokio::test]
async fn set_role_admin_role_holder_rejects() -> Result<()> {
assert_set_role_admin_rejected(test_account_id(3), 151).await
}
#[tokio::test]
async fn set_role_admin_third_party_rejects() -> Result<()> {
assert_set_role_admin_rejected(test_account_id(99), 152).await
}
#[tokio::test]
async fn renounce_role_holder_clears_own_membership() -> Result<()> {
let pf = setup_production_faucet(0, |_, _faucet_id| Vec::new())
.context("building the production network-auth faucet")?;
let chain = pf.mock_chain;
let faucet_id = pf.faucet_id;
let account = chain
.committed_account(faucet_id)
.context("reading the committed faucet")?
.clone();
let note = stock_role_note(
test_account_id(2),
faucet_id,
RbacConfig::RenounceRole { role: pauser_sym() },
&mut note_rng(153),
)
.context("building the DOM_PAUSER renounce note")?;
let tx = chain
.build_transaction(faucet_id)
.unauthenticated_input_note(note.clone())
.build()
.context("renounce tx build")?
.execute()
.await
.map_err(|e| anyhow::anyhow!("a holder's renounce of its own role must succeed: {e}"))?;
let mut evolved = account;
evolved.apply_patch(tx.account_patch())?;
assert_eq!(
read_role_membership(&evolved, &pauser_sym(), test_account_id(2))?,
Word::from([0u32, 0, 0, 0]),
"the holder must have dropped its own DOM_PAUSER membership",
);
assert_eq!(
read_role_config(&evolved, &pauser_sym())?[0],
Felt::ZERO,
"the role must be left with no members",
);
Ok(())
}
#[tokio::test]
async fn renounce_role_non_holder_rejects() -> Result<()> {
let pf = setup_production_faucet(0, |_, _faucet_id| Vec::new())
.context("building the production network-auth faucet")?;
let chain = pf.mock_chain;
let faucet_id = pf.faucet_id;
let note = stock_role_note(
test_account_id(99),
faucet_id,
RbacConfig::RenounceRole { role: pauser_sym() },
&mut note_rng(154),
)
.context("building the non-holder renounce note")?;
let result = chain
.build_transaction(faucet_id)
.unauthenticated_input_note(note.clone())
.build()
.context("non-holder renounce tx build")?
.execute()
.await;
assert_transaction_executor_error!(result, err_account_not_in_role());
Ok(())
}