extern crate alloc;
use alloc::vec::Vec;
use miden_processor::crypto::random::RandomCoin;
use miden_protocol::account::{Account, AccountBuilder, AccountId, AccountType, AssetCallbackFlag};
use miden_protocol::asset::AssetAmount;
use miden_protocol::note::Note;
use miden_protocol::testing::account_id::AccountIdBuilder;
use miden_protocol::{Felt, Word};
use miden_standards::account::access::{Authority, Ownable2Step};
use miden_standards::account::faucets::{FungibleFaucet, TokenName};
use miden_standards::account::policies::{
BurnPolicy,
MintPolicy,
TokenPolicyManager,
TransferPolicy,
};
use miden_standards::errors::standards::{
ERR_FAUCET_POLICY_CONFIG_TARGET_ACCOUNT_MISMATCH,
ERR_FAUCET_POLICY_CONFIG_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS,
ERR_FAUCET_POLICY_CONFIG_UNKNOWN_SELECTOR,
};
use miden_standards::note::{
FaucetPolicyConfig,
FaucetPolicyConfigNote,
NetworkAccountTarget,
NoteExecutionHint,
};
use miden_standards::testing::note::NoteBuilder;
use miden_testing::{
AccountState,
Auth,
MockChain,
MockChainBuilder,
assert_transaction_executor_error,
};
fn create_faucet_with_policies(
builder: &mut MockChainBuilder,
owner: AccountId,
) -> anyhow::Result<Account> {
let faucet = FungibleFaucet::builder()
.name(TokenName::new("SYM")?)
.symbol("SYM".try_into()?)
.decimals(8)
.max_supply(AssetAmount::new(1_000_000)?)
.build()?;
let token_policy_manager = TokenPolicyManager::builder()
.active_mint_policy(MintPolicy::allow_all())
.allowed_mint_policy(MintPolicy::owner_only())
.active_burn_policy(BurnPolicy::allow_all())
.allowed_burn_policy(BurnPolicy::owner_only())
.active_send_policy(TransferPolicy::allow_all())
.active_receive_policy(TransferPolicy::allow_all())
.build();
let account_builder = AccountBuilder::new([43; 32])
.account_type(AccountType::Public)
.with_component(faucet)
.with_component(Ownable2Step::new(owner))
.with_component(Authority::OwnerControlled)
.with_asset_callbacks(AssetCallbackFlag::from(token_policy_manager.has_transfer_policy()))
.with_components(token_policy_manager);
builder.add_account_from_builder(Auth::IncrNonce, account_builder, AccountState::Exists)
}
fn active_mint_policy_root(account: &Account) -> anyhow::Result<Word> {
Ok(account.storage().get_item(TokenPolicyManager::active_mint_policy_slot())?)
}
fn active_burn_policy_root(account: &Account) -> anyhow::Result<Word> {
Ok(account.storage().get_item(TokenPolicyManager::active_burn_policy_slot())?)
}
fn faucet_policy_config_note(
sender: AccountId,
account: AccountId,
config: FaucetPolicyConfig,
rng: &mut RandomCoin,
) -> anyhow::Result<Note> {
let note = FaucetPolicyConfigNote::builder()
.sender(sender)
.target(account)
.config(config)
.generate_serial_number(rng)
.build()?
.into();
Ok(note)
}
fn malformed_faucet_policy_config_note(
sender: AccountId,
target: AccountId,
storage: Vec<Felt>,
rng: &mut RandomCoin,
) -> anyhow::Result<Note> {
let note = NoteBuilder::new(sender, rng)
.script(FaucetPolicyConfigNote::script())
.note_storage(storage)?
.attachment(NetworkAccountTarget::new(target, NoteExecutionHint::Always)?)
.build()?;
Ok(note)
}
async fn execute_note_and_apply(
mock_chain: &MockChain,
account: &Account,
note: &Note,
) -> anyhow::Result<Account> {
let tx = mock_chain
.build_transaction(account.clone())
.unauthenticated_input_note(note.clone())
.build()?;
let executed = tx.execute().await?;
let mut updated = account.clone();
updated.apply_patch(executed.account_patch())?;
Ok(updated)
}
#[tokio::test]
async fn set_mint_policy_dispatch() -> anyhow::Result<()> {
let owner = AccountIdBuilder::new().build_with_seed([1; 32]);
let mut builder = MockChain::builder();
let faucet = create_faucet_with_policies(&mut builder, owner)?;
let mock_chain = builder.build()?;
let mut rng = RandomCoin::new([Felt::from(100u32); 4].into());
let owner_only_root = MintPolicy::owner_only().root();
assert_ne!(active_mint_policy_root(&faucet)?, owner_only_root.as_word());
let note = faucet_policy_config_note(
owner,
faucet.id(),
FaucetPolicyConfig::SetMintPolicy { policy_root: owner_only_root },
&mut rng,
)?;
let updated = execute_note_and_apply(&mock_chain, &faucet, ¬e).await?;
assert_eq!(active_mint_policy_root(&updated)?, owner_only_root.as_word());
Ok(())
}
#[tokio::test]
async fn set_burn_policy_dispatch() -> anyhow::Result<()> {
let owner = AccountIdBuilder::new().build_with_seed([1; 32]);
let mut builder = MockChain::builder();
let faucet = create_faucet_with_policies(&mut builder, owner)?;
let mock_chain = builder.build()?;
let mut rng = RandomCoin::new([Felt::from(100u32); 4].into());
let owner_only_root = BurnPolicy::owner_only().root();
assert_ne!(active_burn_policy_root(&faucet)?, owner_only_root.as_word());
let note = faucet_policy_config_note(
owner,
faucet.id(),
FaucetPolicyConfig::SetBurnPolicy { policy_root: owner_only_root },
&mut rng,
)?;
let updated = execute_note_and_apply(&mock_chain, &faucet, ¬e).await?;
assert_eq!(active_burn_policy_root(&updated)?, owner_only_root.as_word());
Ok(())
}
#[tokio::test]
async fn unknown_selector_fails() -> anyhow::Result<()> {
let owner = AccountIdBuilder::new().build_with_seed([1; 32]);
let mut builder = MockChain::builder();
let faucet = create_faucet_with_policies(&mut builder, owner)?;
let mock_chain = builder.build()?;
let mut rng = RandomCoin::new([Felt::from(100u32); 4].into());
let storage = vec![Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::from(99u32)];
let note = malformed_faucet_policy_config_note(owner, faucet.id(), storage, &mut rng)?;
let tx = mock_chain
.build_transaction(faucet.clone())
.unauthenticated_input_note(note)
.build()?;
let result = tx.execute().await;
assert_transaction_executor_error!(result, ERR_FAUCET_POLICY_CONFIG_UNKNOWN_SELECTOR);
Ok(())
}
#[tokio::test]
async fn wrong_storage_item_count_fails() -> anyhow::Result<()> {
let owner = AccountIdBuilder::new().build_with_seed([1; 32]);
let mut builder = MockChain::builder();
let faucet = create_faucet_with_policies(&mut builder, owner)?;
let mock_chain = builder.build()?;
let mut rng = RandomCoin::new([Felt::from(100u32); 4].into());
let note =
malformed_faucet_policy_config_note(owner, faucet.id(), vec![Felt::from(0u32)], &mut rng)?;
let tx = mock_chain
.build_transaction(faucet.clone())
.unauthenticated_input_note(note)
.build()?;
let result = tx.execute().await;
assert_transaction_executor_error!(
result,
ERR_FAUCET_POLICY_CONFIG_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS
);
Ok(())
}
#[tokio::test]
async fn decoy_faucet_cannot_consume_note_of_another_faucet() -> anyhow::Result<()> {
let owner = AccountIdBuilder::new().build_with_seed([1; 32]);
let mut builder = MockChain::builder();
let decoy = create_faucet_with_policies(&mut builder, owner)?;
let mock_chain = builder.build()?;
let mut rng = RandomCoin::new([Felt::from(100u32); 4].into());
let target = AccountId::builder().account_type(AccountType::Public).build_with_seed([9; 32]);
let note = faucet_policy_config_note(
owner,
target,
FaucetPolicyConfig::SetMintPolicy {
policy_root: MintPolicy::owner_only().root(),
},
&mut rng,
)?;
let result = mock_chain
.build_transaction(decoy.clone())
.unauthenticated_input_note(note)
.build()?
.execute()
.await;
assert_transaction_executor_error!(result, ERR_FAUCET_POLICY_CONFIG_TARGET_ACCOUNT_MISMATCH);
Ok(())
}