use super::*;
use crate::generic_proxy::*;
use fuels::{
core::Configurables,
programs::responses::CallResponse,
test_helpers::{
ChainConfig,
WalletsConfig,
launch_custom_provider_and_get_wallets,
},
tx::{
ContractIdExt,
StorageSlot,
},
types::{
Bits256,
Identity,
},
};
use sha2::{
Digest,
Sha256,
};
macro_rules! read {
($call:expr) => {
$call
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
};
}
struct Fixture {
admin: Wallet,
operator: Wallet,
holder: Wallet,
deployed: EvalMinterDeployment<Wallet>,
}
impl Fixture {
async fn new() -> Self {
let mut wallets = launch_custom_provider_and_get_wallets(
WalletsConfig::new(Some(3), Some(4), Some(100_000_000_000)),
None,
Some(ChainConfig::local_testnet()),
)
.await
.unwrap();
let holder = wallets.pop().unwrap();
let operator = wallets.pop().unwrap();
let admin = wallets.pop().unwrap();
let deployed = EvalMinterDeployment::deploy(&admin, &Default::default())
.await
.unwrap();
Self {
admin,
operator,
holder,
deployed,
}
}
fn minter(&self) -> &EvalMinterContract<Wallet> {
&self.deployed.minter
}
fn as_wallet(&self, wallet: &Wallet) -> EvalMinterContract<Wallet> {
self.minter().clone().with_account(wallet.clone())
}
fn assert_error(&self, error: anyhow::Error, expected: EvalMinterError) {
use fuels::types::errors::{
Error,
transaction::Reason,
};
let Some(Error::Transaction(Reason::Failure { receipts, .. })) =
error.downcast_ref::<Error>()
else {
panic!("expected {expected:?}, got {error:#}");
};
let errors = self
.minter()
.log_decoder()
.decode_logs_with_type::<EvalMinterError>(receipts)
.unwrap();
assert_eq!(errors, vec![expected], "{error:#}");
}
async fn register(&self, symbol: &str) -> RegisteredAsset {
self.minter()
.methods()
.register_asset(format!("Evaluation {symbol}"), symbol.into(), 9)
.call()
.await
.unwrap()
.value
}
async fn grant(&self, role: u64, wallet: &Wallet) {
self.minter()
.methods()
.grant_role(role, Identity::Address(wallet.address()))
.call()
.await
.unwrap();
}
async fn mint(
&self,
wallet: &Wallet,
symbol: &str,
amount: u64,
) -> anyhow::Result<CallResponse<()>> {
Ok(self
.as_wallet(wallet)
.methods()
.mint(
Identity::Address(self.holder.address()),
symbol.into(),
amount,
)
.with_variable_output_policy(VariableOutputPolicy::EstimateMinimum)
.call()
.await?)
}
async fn burn(
&self,
asset_id: AssetId,
amount: u64,
) -> anyhow::Result<CallResponse<()>> {
Ok(self
.as_wallet(&self.holder)
.methods()
.burn()
.call_params(
CallParameters::default()
.with_asset_id(asset_id)
.with_amount(amount),
)?
.call()
.await?)
}
async fn supply(&self, asset: AssetId) -> Option<u64> {
read!(self.minter().methods().total_supply(asset))
}
}
fn sub_id(symbol: &str) -> Bits256 {
Bits256(Sha256::digest(symbol.as_bytes()).into())
}
#[tokio::test]
async fn registrations_metadata_and_all_identity_mappings() {
let f = Fixture::new().await;
let m = f.minter();
let receipt = m
.methods()
.register_asset("Evaluation".into(), "EVAL".into(), 9)
.call()
.await
.unwrap();
let a = receipt.value.clone();
assert_eq!(a.sub_id, sub_id("EVAL"));
let mut hash = Sha256::new();
hash.update(f.deployed.contract_id.as_ref());
hash.update(a.sub_id.0);
assert_eq!(a.asset_id, AssetId::new(hash.finalize().into()));
let events = receipt
.decode_logs_with_type::<EvalAssetRegistered>()
.unwrap();
assert_eq!(events.len(), 1);
assert_eq!(events[0].asset_id, a.asset_id);
assert_eq!(events[0].sub_id, a.sub_id);
assert_eq!(events[0].caller, Identity::Address(f.admin.address()));
assert_eq!(
receipt
.decode_logs_with_type::<SetNameEvent>()
.unwrap()
.len(),
1
);
assert_eq!(
receipt
.decode_logs_with_type::<SetSymbolEvent>()
.unwrap()
.len(),
1
);
assert_eq!(
receipt
.decode_logs_with_type::<SetDecimalsEvent>()
.unwrap()
.len(),
1
);
assert_eq!(
receipt.decode_logs_with_type::<TotalSupplyEvent>().unwrap()[0].supply,
0
);
assert_eq!(f.supply(a.asset_id).await, Some(0));
assert_eq!(read!(m.methods().asset_id(a.sub_id)), Some(a.asset_id));
assert_eq!(read!(m.methods().sub_id(a.asset_id)), Some(a.sub_id));
assert_eq!(
read!(m.methods().sub_id_by_symbol("EVAL".into())),
Some(a.sub_id)
);
assert_eq!(
read!(m.methods().asset_id_by_symbol("EVAL".into())),
Some(a.asset_id)
);
assert_eq!(
read!(m.methods().symbol_by_sub_id(a.sub_id)),
Some("EVAL".into())
);
assert_eq!(read!(m.methods().symbol(a.asset_id)), Some("EVAL".into()));
let error = m
.methods()
.register_asset("Duplicate".into(), "EVAL".into(), 9)
.call()
.await
.unwrap_err();
f.assert_error(error.into(), EvalMinterError::SymbolAlreadyRegistered);
assert!(
m.methods()
.register_asset("".into(), "EMPTY".into(), 9)
.call()
.await
.is_err()
);
assert!(
m.methods()
.register_asset("Empty".into(), "".into(), 9)
.call()
.await
.is_err()
);
let b = f.register("OTHER").await;
assert_eq!(b.sub_id, sub_id("OTHER"));
assert_eq!(
read!(m.methods().sub_id_by_symbol("EVAL".into())),
Some(a.sub_id)
);
f.grant(MINTER_ROLE, &f.operator).await;
f.mint(&f.operator, "EVAL", 100).await.unwrap();
m.methods()
.set_name(a.sub_id, "Updated name".into())
.call()
.await
.unwrap();
m.methods().set_decimals(a.sub_id, 18).call().await.unwrap();
assert_eq!(
read!(m.methods().name(a.asset_id)),
Some("Updated name".into())
);
assert_eq!(read!(m.methods().decimals(a.asset_id)), Some(18));
assert_eq!(
read!(m.methods().asset_id_by_symbol("EVAL".into())),
Some(a.asset_id)
);
assert_eq!(read!(m.methods().sub_id(a.asset_id)), Some(a.sub_id));
assert_eq!(read!(m.methods().symbol(a.asset_id)), Some("EVAL".into()));
assert_eq!(f.register("eval").await.sub_id, sub_id("eval"));
assert_eq!(read!(m.methods().total_assets()), 3);
assert_eq!(f.supply(a.asset_id).await, Some(100));
assert_eq!(f.holder.get_asset_balance(&a.asset_id).await.unwrap(), 100);
let unknown = AssetId::new([0x99; 32]);
assert_eq!(f.supply(unknown).await, None);
assert_eq!(read!(m.methods().name(unknown)), None);
assert_eq!(read!(m.methods().symbol(unknown)), None);
assert_eq!(read!(m.methods().decimals(unknown)), None);
assert_eq!(read!(m.methods().sub_id(unknown)), None);
assert_eq!(read!(m.methods().asset_id(sub_id("missing"))), None);
assert_eq!(read!(m.methods().symbol_by_sub_id(sub_id("missing"))), None);
assert_eq!(read!(m.methods().sub_id_by_symbol("missing".into())), None);
assert_eq!(
read!(m.methods().asset_id_by_symbol("missing".into())),
None
);
assert!(
m.methods()
.set_name(sub_id("missing"), "Invalid".into())
.call()
.await
.is_err()
);
}
#[tokio::test]
async fn mint_resolves_exact_case_sensitive_symbols() {
let f = Fixture::new().await;
let a = f.register("EVAL").await;
f.grant(MINTER_ROLE, &f.operator).await;
f.mint(&f.operator, "EVAL", 11).await.unwrap();
for symbol in ["", "MISSING", "eval"] {
let error = f.mint(&f.operator, symbol, 1).await.unwrap_err();
f.assert_error(error, EvalMinterError::UnknownAsset);
assert_eq!(f.supply(a.asset_id).await, Some(11));
assert_eq!(f.holder.get_asset_balance(&a.asset_id).await.unwrap(), 11);
}
let lowercase = f.register("eval").await;
let unicode = f.register("éVAL").await;
assert_eq!(lowercase.sub_id, sub_id("eval"));
assert_eq!(unicode.sub_id, sub_id("éVAL"));
assert_ne!(a.sub_id, lowercase.sub_id);
f.mint(&f.operator, "EVAL", 7).await.unwrap();
f.mint(&f.operator, "eval", 17).await.unwrap();
f.mint(&f.operator, "éVAL", 13).await.unwrap();
for (asset, amount) in [(a, 18), (lowercase, 17), (unicode, 13)] {
assert_eq!(f.supply(asset.asset_id).await, Some(amount));
assert_eq!(
f.holder.get_asset_balance(&asset.asset_id).await.unwrap(),
u128::from(amount)
);
assert_eq!(
read!(f.minter().methods().asset_id(asset.sub_id)),
Some(asset.asset_id)
);
}
}
#[tokio::test]
async fn asset_ids_depend_on_exact_symbol_and_proxy_not_registration_order() {
let f = Fixture::new().await;
let second = EvalMinterDeployment::deploy(
&f.admin,
&EvalMinterDeployConfig {
salt: Salt::new([42; 32]),
..Default::default()
},
)
.await
.unwrap();
let symbols = ["wBTC", "éVAL", "EVAL", "eval"];
let mut original = Vec::new();
for symbol in symbols {
original.push((symbol, f.register(symbol).await));
}
for (symbol, first) in original.into_iter().rev() {
let other = second
.minter
.methods()
.register_asset(symbol.into(), symbol.into(), 9)
.call()
.await
.unwrap()
.value;
let expected = sub_id(symbol);
assert_eq!(first.sub_id, expected);
assert_eq!(other.sub_id, expected);
assert_ne!(first.asset_id, other.asset_id);
for (asset, proxy) in [
(&first, f.deployed.contract_id),
(&other, second.contract_id),
] {
let mut hash = Sha256::new();
hash.update(proxy.as_ref());
hash.update(expected.0);
assert_eq!(asset.asset_id, AssetId::new(hash.finalize().into()));
}
assert_eq!(
read!(second.minter.methods().sub_id(other.asset_id)),
Some(expected)
);
assert_eq!(
read!(second.minter.methods().symbol_by_sub_id(expected)),
Some(symbol.into())
);
}
}
#[tokio::test]
async fn burn_derives_the_asset_and_amount_and_preserves_existing_balances() {
let f = Fixture::new().await;
let a = f.register("EVAL").await;
let b = f.register("OTHER").await;
f.grant(MINTER_ROLE, &f.operator).await;
f.mint(&f.operator, "EVAL", 100).await.unwrap();
f.mint(&f.operator, "OTHER", 40).await.unwrap();
f.holder
.force_transfer_to_contract(
f.deployed.contract_id,
30,
a.asset_id,
TxPolicies::default(),
)
.await
.unwrap();
assert!(!read!(
f.minter()
.methods()
.has_role(MINTER_ROLE, Identity::Address(f.holder.address()))
));
let error = f.burn(a.asset_id, 0).await.unwrap_err();
f.assert_error(error, EvalMinterError::InvalidAmount);
assert_eq!(f.supply(a.asset_id).await, Some(100));
for (asset, amount, supply, balance) in [
(&a, 9, 91, 61),
(&b, 11, 29, 29),
(&a, 61, 30, 0),
(&b, 29, 0, 0),
] {
let response = f.burn(asset.asset_id, amount).await.unwrap();
let events = response
.decode_logs_with_type::<TotalSupplyEvent>()
.unwrap();
assert_eq!(events.len(), 1);
assert_eq!(events[0].asset, asset.asset_id);
assert_eq!(events[0].supply, supply);
assert_eq!(events[0].sender, Identity::Address(f.holder.address()));
assert_eq!(f.supply(asset.asset_id).await, Some(supply));
assert_eq!(
f.holder.get_asset_balance(&asset.asset_id).await.unwrap(),
balance
);
assert_eq!(
f.admin
.provider()
.get_contract_asset_balance(&f.deployed.contract_id, &a.asset_id)
.await
.unwrap(),
30
);
assert_eq!(
f.admin
.provider()
.get_contract_asset_balance(&f.deployed.contract_id, &b.asset_id)
.await
.unwrap(),
0
);
}
assert_eq!(read!(f.minter().methods().total_assets()), 2);
let (foreign_minter, foreign_id) =
crate::src20_mock::setup_src20_mock(&f.admin).await;
foreign_minter
.methods()
.mint_to_address(f.holder.address(), 10)
.with_variable_output_policy(VariableOutputPolicy::EstimateMinimum)
.call()
.await
.unwrap();
let foreign_asset = foreign_id.default_asset();
let error = f.burn(foreign_asset, 10).await.unwrap_err();
f.assert_error(error, EvalMinterError::UnknownAsset);
assert_eq!(
f.holder.get_asset_balance(&foreign_asset).await.unwrap(),
10
);
assert_eq!(f.supply(foreign_asset).await, None);
assert_eq!(f.supply(a.asset_id).await, Some(30));
assert_eq!(f.supply(b.asset_id).await, Some(0));
assert_eq!(
f.admin
.provider()
.get_contract_asset_balance(&f.deployed.contract_id, &foreign_asset)
.await
.unwrap(),
0
);
}
#[tokio::test]
async fn role_separation_pausing_and_holder_burns_are_atomic() {
let f = Fixture::new().await;
let a = f.register("EVAL").await;
let m = f.minter();
let operator = f.as_wallet(&f.operator);
assert!(f.mint(&f.admin, "EVAL", 100).await.is_err());
assert!(f.mint(&f.operator, "EVAL", 100).await.is_err());
assert!(operator.methods().pause().call().await.is_err());
f.grant(MINTER_ROLE, &f.operator).await;
assert!(
operator
.methods()
.register_asset("Bad".into(), "BAD".into(), 9)
.call()
.await
.is_err()
);
assert!(
operator
.methods()
.set_name(a.sub_id, "Bad".into())
.call()
.await
.is_err()
);
assert!(
operator
.methods()
.set_decimals(a.sub_id, 0)
.call()
.await
.is_err()
);
assert!(
operator
.methods()
.grant_role(DEFAULT_ADMIN_ROLE, Identity::Address(f.operator.address()))
.call()
.await
.is_err()
);
let minted = f.mint(&f.operator, "EVAL", 100).await.unwrap();
let events = minted.decode_logs_with_type::<TotalSupplyEvent>().unwrap();
assert_eq!(events.len(), 1);
assert_eq!(events[0].asset, a.asset_id);
assert_eq!(events[0].supply, 100);
assert_eq!(events[0].sender, Identity::Address(f.operator.address()));
assert!(f.mint(&f.operator, "EVAL", 0).await.is_err());
assert!(
operator
.methods()
.mint(Identity::Address(Address::zeroed()), "EVAL".into(), 1)
.call()
.await
.is_err()
);
assert!(
operator
.methods()
.mint(Identity::Address(f.holder.address()), "MISSING".into(), 1)
.call()
.await
.is_err()
);
f.grant(PAUSER_ROLE, &f.holder).await;
let holder = f.as_wallet(&f.holder);
holder.methods().pause().call().await.unwrap();
assert!(holder.methods().unpause().call().await.is_err());
assert!(f.mint(&f.operator, "EVAL", 1).await.is_err());
assert!(f.mint(&f.holder, "EVAL", 1).await.is_err());
assert!(f.burn(a.asset_id, 10).await.is_err());
m.methods().set_decimals(a.sub_id, 0).call().await.unwrap();
m.methods()
.revoke_role(PAUSER_ROLE, Identity::Address(f.holder.address()))
.call()
.await
.unwrap();
assert!(read!(m.methods().is_paused()));
assert_eq!(f.holder.get_asset_balance(&a.asset_id).await.unwrap(), 100);
assert_eq!(f.supply(a.asset_id).await, Some(100));
m.methods().unpause().call().await.unwrap();
assert!(holder.methods().pause().call().await.is_err());
m.methods().pause().call().await.unwrap();
m.methods().unpause().call().await.unwrap();
for (asset_id, amount, reason) in [
(AssetId::zeroed(), 10, EvalMinterError::UnknownAsset),
(a.asset_id, 0, EvalMinterError::InvalidAmount),
] {
let error = f.burn(asset_id, amount).await.unwrap_err();
f.assert_error(error, reason);
assert_eq!(f.supply(a.asset_id).await, Some(100));
assert_eq!(f.holder.get_asset_balance(&a.asset_id).await.unwrap(), 100);
}
let burned = f.burn(a.asset_id, 100).await.unwrap();
assert_eq!(
burned.decode_logs_with_type::<TotalSupplyEvent>().unwrap()[0].supply,
0
);
assert_eq!(f.holder.get_asset_balance(&a.asset_id).await.unwrap(), 0);
assert_eq!(f.supply(a.asset_id).await, Some(0));
assert_eq!(read!(m.methods().total_assets()), 1);
assert_eq!(read!(m.methods().asset_id(a.sub_id)), Some(a.asset_id));
assert_eq!(read!(m.methods().sub_id(a.asset_id)), Some(a.sub_id));
assert_eq!(
read!(m.methods().asset_id_by_symbol("EVAL".into())),
Some(a.asset_id)
);
let duplicate = m
.methods()
.register_asset("Duplicate".into(), "EVAL".into(), 9)
.call()
.await
.unwrap_err();
f.assert_error(duplicate.into(), EvalMinterError::SymbolAlreadyRegistered);
f.mint(&f.operator, "EVAL", 1).await.unwrap();
assert_eq!(f.supply(a.asset_id).await, Some(1));
m.methods()
.revoke_role(MINTER_ROLE, Identity::Address(f.operator.address()))
.call()
.await
.unwrap();
assert!(f.mint(&f.operator, "EVAL", 1).await.is_err());
}
#[tokio::test]
async fn mint_to_contract_and_supply_overflow() {
let f = Fixture::new().await;
let a = f.register("EVAL").await;
f.grant(MINTER_ROLE, &f.operator).await;
let (_, receiver) = crate::src20_mock::setup_src20_mock(&f.admin).await;
f.as_wallet(&f.operator)
.methods()
.mint(Identity::ContractId(receiver), "EVAL".into(), 5)
.with_contract_ids(&[receiver])
.call()
.await
.unwrap();
assert_eq!(
f.admin
.provider()
.get_contract_asset_balance(&receiver, &a.asset_id)
.await
.unwrap(),
5
);
f.mint(&f.operator, "EVAL", u64::MAX - 5).await.unwrap();
assert_eq!(f.supply(a.asset_id).await, Some(u64::MAX));
assert!(f.mint(&f.operator, "EVAL", 1).await.is_err());
assert_eq!(f.supply(a.asset_id).await, Some(u64::MAX));
assert_eq!(
f.holder.get_asset_balance(&a.asset_id).await.unwrap(),
u128::from(u64::MAX - 5)
);
}
#[tokio::test]
async fn initialization_is_proxy_owner_only_and_ownership_does_not_transfer_roles() {
let f = Fixture::new().await;
let mut slots: Vec<StorageSlot> =
serde_json::from_slice(GENERIC_PROXY_STORAGE).unwrap();
slots
.extend(serde_json::from_slice::<Vec<StorageSlot>>(EVAL_MINTER_STORAGE).unwrap());
let contract =
Contract::regular(GENERIC_PROXY_BYTECODE.to_vec(), Salt::new([9; 32]), slots)
.with_configurables(
GenericProxyContractConfigurables::default()
.with_INITIAL_OWNER(State::Initialized(Identity::Address(
f.operator.address(),
)))
.unwrap()
.with_INITIAL_TARGET(f.deployed.blob_id)
.unwrap(),
);
let id = contract.contract_id();
contract
.deploy(&f.admin, TxPolicies::default())
.await
.unwrap();
let proxy = GenericProxyContract::new(id, f.admin.clone());
let minter = EvalMinterContract::new(id, f.admin.clone());
assert!(minter.methods().initialize().call().await.is_err());
proxy.methods().initialize_proxy().call().await.unwrap();
assert!(proxy.methods().initialize_proxy().call().await.is_err());
assert!(minter.methods().initialize().call().await.is_err());
let owner_minter = minter.clone().with_account(f.operator.clone());
owner_minter.methods().initialize().call().await.unwrap();
assert!(owner_minter.methods().initialize().call().await.is_err());
assert!(read!(owner_minter.methods().has_role(
DEFAULT_ADMIN_ROLE,
Identity::Address(f.operator.address())
)));
assert!(!read!(
owner_minter
.methods()
.has_role(MINTER_ROLE, Identity::Address(f.operator.address()))
));
proxy
.with_account(f.operator.clone())
.methods()
.set_owner(Identity::Address(f.holder.address()))
.call()
.await
.unwrap();
assert!(!read!(owner_minter.methods().has_role(
DEFAULT_ADMIN_ROLE,
Identity::Address(f.holder.address())
)));
assert!(read!(owner_minter.methods().has_role(
DEFAULT_ADMIN_ROLE,
Identity::Address(f.operator.address())
)));
}
#[tokio::test]
async fn upgrades_preserve_asset_identity_supply_roles_and_pause_state() {
let f = Fixture::new().await;
let a = f.register("EVAL").await;
f.grant(MINTER_ROLE, &f.operator).await;
f.grant(PAUSER_ROLE, &f.holder).await;
f.mint(&f.operator, "EVAL", 100).await.unwrap();
f.as_wallet(&f.holder)
.methods()
.pause()
.call()
.await
.unwrap();
let config = EvalMinterDeployConfig {
contract_id: Some(f.deployed.contract_id),
max_words_per_blob: 500,
..Default::default()
};
let reused = EvalMinterDeployment::deploy(&f.admin, &config)
.await
.unwrap();
assert_eq!(reused.blob_id, f.deployed.blob_id);
let (data, loader) = crate::blob_loader::build_loader_blobs(
EVAL_MINTER_BYTECODE.to_vec(),
config.salt,
serde_json::from_slice(EVAL_MINTER_STORAGE).unwrap(),
Configurables::default(),
500,
)
.unwrap();
let target: ContractId =
crate::blob_loader::upload_loader_blobs(&f.admin, data, loader)
.await
.unwrap()
.into();
assert_ne!(target, f.deployed.blob_id);
assert!(
f.deployed
.proxy
.clone()
.with_account(f.operator.clone())
.methods()
.set_proxy_target(target)
.call()
.await
.is_err()
);
assert!(
f.deployed
.proxy
.methods()
.set_proxy_target(ContractId::new([0x55; 32]))
.call()
.await
.is_err()
);
let upgraded = EvalMinterDeployment::deploy(
&f.admin,
&EvalMinterDeployConfig {
upgrade_bytecode: true,
..config.clone()
},
)
.await
.unwrap();
assert_eq!(upgraded.contract_id, f.deployed.contract_id);
assert_eq!(upgraded.blob_id, target);
let m = &upgraded.minter;
assert_eq!(read!(m.methods().asset_id(a.sub_id)), Some(a.asset_id));
assert_eq!(read!(m.methods().symbol(a.asset_id)), Some("EVAL".into()));
assert_eq!(read!(m.methods().total_supply(a.asset_id)), Some(100));
assert_eq!(read!(m.methods().total_assets()), 1);
assert_eq!(read!(m.methods().sub_id(a.asset_id)), Some(sub_id("EVAL")));
assert!(read!(
m.methods()
.has_role(MINTER_ROLE, Identity::Address(f.operator.address()))
));
assert!(read!(
m.methods()
.has_role(PAUSER_ROLE, Identity::Address(f.holder.address()))
));
assert!(read!(m.methods().is_paused()));
assert_eq!(f.holder.get_asset_balance(&a.asset_id).await.unwrap(), 100);
m.methods().unpause().call().await.unwrap();
f.burn(a.asset_id, 10).await.unwrap();
f.mint(&f.operator, "EVAL", 5).await.unwrap();
assert_eq!(f.supply(a.asset_id).await, Some(95));
let height = f.admin.provider().latest_block_height().await.unwrap();
EvalMinterDeployment::deploy(
&f.admin,
&EvalMinterDeployConfig {
upgrade_bytecode: true,
..config
},
)
.await
.unwrap();
assert_eq!(
f.admin.provider().latest_block_height().await.unwrap(),
height
);
}