diff --git a/contracts/token/src/events.rs b/contracts/token/src/events.rs index b400f9b..d21d17a 100644 --- a/contracts/token/src/events.rs +++ b/contracts/token/src/events.rs @@ -1,11 +1,7 @@ -//! # bc-forge Token Events -//! -//! Structured event emission for all token contract operations. -//! Events are emitted to the ledger for indexing by off-chain services. +//! # bc-forge Token Events use soroban_sdk::{symbol_short, Address, BytesN, Env, String}; -/// Emitted when the token contract is initialized. pub fn emit_initialized(env: &Env, admin: &Address, decimals: u32, name: &String, symbol: &String) { env.events().publish( (symbol_short!("init"),), @@ -13,22 +9,20 @@ pub fn emit_initialized(env: &Env, admin: &Address, decimals: u32, name: &String ); } -/// Emitted when tokens are minted. -pub fn emit_mint( - env: &Env, - admin: &Address, - to: &Address, - amount: i128, - new_balance: i128, - new_supply: i128, -) { +pub fn emit_max_supply_set(env: &Env, admin: &Address, max_supply: i128) { + env.events().publish( + (symbol_short!("max_sup"),), + (admin.clone(), max_supply), + ); +} + +pub fn emit_mint(env: &Env, admin: &Address, to: &Address, amount: i128, new_balance: i128, new_supply: i128) { env.events().publish( (symbol_short!("mint"),), (admin.clone(), to.clone(), amount, new_balance, new_supply), ); } -/// Emitted when tokens are burned. pub fn emit_burn(env: &Env, from: &Address, amount: i128, new_balance: i128, new_supply: i128) { env.events().publish( (symbol_short!("burn"),), @@ -36,34 +30,17 @@ pub fn emit_burn(env: &Env, from: &Address, amount: i128, new_balance: i128, new ); } -/// Emitted on a standard transfer. pub fn emit_transfer(env: &Env, from: &Address, to: &Address, amount: i128) { - env.events() - .publish((symbol_short!("xfer"),), (from.clone(), to.clone(), amount)); -} - -/// Emitted on a delegated transfer (transfer_from). -pub fn emit_transfer_from( - env: &Env, - spender: &Address, - from: &Address, - to: &Address, - amount: i128, - remaining_allowance: i128, -) { + env.events().publish((symbol_short!("xfer"),), (from.clone(), to.clone(), amount)); +} + +pub fn emit_transfer_from(env: &Env, spender: &Address, from: &Address, to: &Address, amount: i128, remaining_allowance: i128) { env.events().publish( (symbol_short!("xfer_frm"),), - ( - spender.clone(), - from.clone(), - to.clone(), - amount, - remaining_allowance, - ), + (spender.clone(), from.clone(), to.clone(), amount, remaining_allowance), ); } -/// Emitted when an allowance is approved. pub fn emit_approve(env: &Env, from: &Address, spender: &Address, amount: i128) { env.events().publish( (symbol_short!("approve"),), @@ -71,7 +48,6 @@ pub fn emit_approve(env: &Env, from: &Address, spender: &Address, amount: i128) ); } -/// Emitted when contract ownership is transferred. pub fn emit_ownership_transferred(env: &Env, old_admin: &Address, new_admin: &Address) { env.events().publish( (symbol_short!("own_xfer"),), @@ -79,7 +55,6 @@ pub fn emit_ownership_transferred(env: &Env, old_admin: &Address, new_admin: &Ad ); } -/// Emitted when a new admin is proposed (two-step transfer). pub fn emit_ownership_proposed(env: &Env, old_admin: &Address, pending_admin: &Address) { env.events().publish( (symbol_short!("own_prop"),), @@ -87,7 +62,6 @@ pub fn emit_ownership_proposed(env: &Env, old_admin: &Address, pending_admin: &A ); } -/// Emitted when pending admin accepts ownership. pub fn emit_ownership_accepted(env: &Env, old_admin: &Address, new_admin: &Address) { env.events().publish( (symbol_short!("own_acc"),), @@ -95,7 +69,6 @@ pub fn emit_ownership_accepted(env: &Env, old_admin: &Address, new_admin: &Addre ); } -/// Emitted when ownership transfer is cancelled. pub fn emit_ownership_cancelled(env: &Env, admin: &Address, cancelled_admin: &Address) { env.events().publish( (symbol_short!("own_can"),), @@ -103,19 +76,14 @@ pub fn emit_ownership_cancelled(env: &Env, admin: &Address, cancelled_admin: &Ad ); } -/// Emitted when the contract is paused. pub fn emit_paused(env: &Env, admin: &Address) { - env.events() - .publish((symbol_short!("paused"),), (admin.clone(),)); + env.events().publish((symbol_short!("paused"),), (admin.clone(),)); } -/// Emitted when the contract is unpaused. pub fn emit_unpaused(env: &Env, admin: &Address) { - env.events() - .publish((symbol_short!("unpause"),), (admin.clone(),)); + env.events().publish((symbol_short!("unpause"),), (admin.clone(),)); } -/// Emitted when tokens are clawed back. pub fn emit_clawback(env: &Env, admin: &Address, from: &Address, to: &Address, amount: i128) { env.events().publish( (symbol_short!("clawback"),), @@ -123,7 +91,6 @@ pub fn emit_clawback(env: &Env, admin: &Address, from: &Address, to: &Address, a ); } -/// Emitted when tokens are locked. pub fn emit_locked(env: &Env, user: &Address, amount: i128, unlock_time: u64) { env.events().publish( (symbol_short!("lock"),), @@ -131,13 +98,10 @@ pub fn emit_locked(env: &Env, user: &Address, amount: i128, unlock_time: u64) { ); } -/// Emitted when locked tokens are withdrawn. pub fn emit_withdraw_locked(env: &Env, user: &Address, amount: i128) { - env.events() - .publish((symbol_short!("unlock"),), (user.clone(), amount)); + env.events().publish((symbol_short!("unlock"),), (user.clone(), amount)); } -/// Emitted when the contract is upgraded. pub fn emit_upgrade(env: &Env, admin: &Address, new_wasm_hash: &BytesN<32>) { env.events().publish( (symbol_short!("upgrade"),), @@ -145,7 +109,6 @@ pub fn emit_upgrade(env: &Env, admin: &Address, new_wasm_hash: &BytesN<32>) { ); } -/// Emitted when the token name is updated. pub fn emit_update_name(env: &Env, admin: &Address, old_name: &String, new_name: &String) { env.events().publish( (symbol_short!("upd_name"),), @@ -153,7 +116,6 @@ pub fn emit_update_name(env: &Env, admin: &Address, old_name: &String, new_name: ); } -/// Emitted when the token symbol is updated. pub fn emit_update_symbol(env: &Env, admin: &Address, old_symbol: &String, new_symbol: &String) { env.events().publish( (symbol_short!("upd_sym"),), diff --git a/contracts/token/src/lib.rs b/contracts/token/src/lib.rs index 5faad34..b57c981 100644 --- a/contracts/token/src/lib.rs +++ b/contracts/token/src/lib.rs @@ -1,4 +1,4 @@ -//! # bc-forge Token Contract +//! # bc-forge Token Contract //! //! A Soroban-based token contract implementing the standard SEP-41 TokenInterface //! with additional administrative controls, pausable lifecycle, ownership management, @@ -20,7 +20,6 @@ use soroban_sdk::{ #[derive(Clone)] #[contracttype] pub enum DataKey { - /// The contract admin address (singular). Admin, PendingAdmin, /// Spending allowance: (owner, spender) → amount and expiration. @@ -33,6 +32,7 @@ pub enum DataKey { Symbol, Decimals, Supply, + MaxSupply, ClawbackAdmin, Lockup(Address), ProposalAction(u64), @@ -79,6 +79,7 @@ pub enum TokenError { InsufficientBalance = 4, InsufficientAllowance = 5, ContractPaused = 6, + MaxSupplyExceeded = 7, } #[contract] @@ -157,7 +158,6 @@ impl BcForgeToken { return 0; } } - env.storage() .persistent() .get(&DataKey::Allowance(from.clone(), spender.clone())) @@ -193,11 +193,9 @@ impl BcForgeToken { if from_balance < amount { return Err(TokenError::InsufficientBalance); } - if from == to { return Ok((from_balance, from_balance)); } - let new_from = from_balance - amount; let new_to = Self::read_balance(env, to) + amount; Self::write_balance(env, from, new_from); @@ -213,6 +211,10 @@ impl BcForgeToken { env.storage().instance().set(&DataKey::Supply, &supply); } + fn read_max_supply(env: &Env) -> Option { + env.storage().instance().get(&DataKey::MaxSupply) + } + fn internal_mint( env: &Env, admin: &Address, @@ -222,14 +224,20 @@ impl BcForgeToken { if amount <= 0 { return Err(TokenError::InvalidAmount); } - + let current_supply = Self::read_supply(env); + if let Some(max) = Self::read_max_supply(env) { + let new_supply = current_supply + .checked_add(amount) + .ok_or(TokenError::MaxSupplyExceeded)?; + if new_supply > max { + return Err(TokenError::MaxSupplyExceeded); + } + } let balance = Self::read_balance(env, to) + amount; Self::write_balance(env, to, balance); - - let supply = Self::read_supply(env) + amount; + let supply = current_supply + amount; Self::write_supply(env, supply); events::emit_mint(env, admin, to, amount, balance, supply); - Ok(()) } @@ -246,18 +254,24 @@ impl BcForgeToken { decimal: u32, name: String, symbol: String, + max_supply: Option, ) -> Result<(), TokenError> { if env.storage().instance().has(&DataKey::Admin) { return Err(TokenError::AlreadyInitialized); } - + if let Some(max) = max_supply { + if max <= 0 { + return Err(TokenError::InvalidAmount); + } + env.storage().instance().set(&DataKey::MaxSupply, &max); + events::emit_max_supply_set(&env, &admin, max); + } Self::set_admin(&env, &admin); env.storage().instance().set(&DataKey::Decimals, &decimal); env.storage().instance().set(&DataKey::Name, &name); env.storage().instance().set(&DataKey::Symbol, &symbol); Self::write_supply(&env, 0); events::emit_initialized(&env, &admin, decimal, &name, &symbol); - Ok(()) } @@ -274,19 +288,16 @@ impl BcForgeToken { Self::ensure_not_paused(&env)?; let current_admin = Self::read_admin(&env)?; current_admin.require_auth(); - for i in 0..recipients.len() { let recipient = recipients.get(i).expect("recipient should exist"); if recipient.amount <= 0 { return Err(TokenError::InvalidAmount); } } - for i in 0..recipients.len() { let recipient = recipients.get(i).expect("recipient should exist"); Self::internal_mint(&env, ¤t_admin, &recipient.address, recipient.amount)?; } - Ok(()) } @@ -294,7 +305,6 @@ impl BcForgeToken { Self::panic_on_err(&env, Self::ensure_initialized(&env)); Self::panic_on_err(&env, Self::ensure_not_paused(&env)); from.require_auth(); - let mut total: i128 = 0; for i in 0..recipients.len() { let (_, amount) = recipients.get(i).expect("recipient should exist"); @@ -306,11 +316,9 @@ impl BcForgeToken { None => soroban_sdk::panic_with_error!(&env, TokenError::InvalidAmount), }; } - if Self::read_balance(&env, &from) < total { soroban_sdk::panic_with_error!(&env, TokenError::InsufficientBalance); } - for i in 0..recipients.len() { let (to, amount) = recipients.get(i).expect("recipient should exist"); let _ = Self::panic_on_err(&env, Self::move_balance(&env, &from, &to, amount)); @@ -323,22 +331,20 @@ impl BcForgeToken { Self::read_supply(&env) } + pub fn max_supply(env: Env) -> Option { + Self::panic_on_err(&env, Self::ensure_initialized(&env)); + Self::read_max_supply(&env) + } + pub fn set_admin_pool(env: Env, pool: Vec
, threshold: u32) { let current_admin = Self::read_admin(&env).expect("contract not initialized"); current_admin.require_auth(); admin::set_admin_pool(&env, pool, threshold); } - pub fn propose_action( - env: Env, - signer: Address, - action: TokenAction, - description: String, - ) -> u64 { + pub fn propose_action(env: Env, signer: Address, action: TokenAction, description: String) -> u64 { let id = admin::create_proposal(&env, signer, description); - env.storage() - .instance() - .set(&DataKey::ProposalAction(id), &action); + env.storage().instance().set(&DataKey::ProposalAction(id), &action); id } @@ -353,7 +359,6 @@ impl BcForgeToken { .instance() .get(&DataKey::ProposalAction(proposal_id)) .expect("proposal action not found"); - match action { TokenAction::Mint(to, amount) => { Self::panic_on_err(&env, Self::ensure_not_paused(&env)); @@ -371,17 +376,13 @@ impl BcForgeToken { events::emit_unpaused(&env, ¤t_admin); } } - env.storage() - .instance() - .remove(&DataKey::ProposalAction(proposal_id)); + env.storage().instance().remove(&DataKey::ProposalAction(proposal_id)); } pub fn set_clawback_admin(env: Env, clawback_admin: Address) { let current_admin = Self::read_admin(&env).expect("contract not initialized"); current_admin.require_auth(); - env.storage() - .instance() - .set(&DataKey::ClawbackAdmin, &clawback_admin); + env.storage().instance().set(&DataKey::ClawbackAdmin, &clawback_admin); } pub fn clawback(env: Env, from: Address, to: Address, amount: i128) -> Result<(), TokenError> { @@ -392,11 +393,9 @@ impl BcForgeToken { .get(&DataKey::ClawbackAdmin) .expect("clawback admin not set"); clawback_admin.require_auth(); - if amount <= 0 { return Err(TokenError::InvalidAmount); } - let _ = Self::move_balance(&env, &from, &to, amount)?; events::emit_clawback(&env, &clawback_admin, &from, &to, amount); Ok(()) @@ -414,40 +413,27 @@ impl BcForgeToken { admin::has_role(&env, role, &address) } - pub fn lock_tokens( - env: Env, - user: Address, - amount: i128, - unlock_time: u64, - ) -> Result<(), TokenError> { + pub fn lock_tokens(env: Env, user: Address, amount: i128, unlock_time: u64) -> Result<(), TokenError> { let current_admin = Self::read_admin(&env)?; current_admin.require_auth(); - if amount <= 0 { return Err(TokenError::InvalidAmount); } - let balance = Self::read_balance(&env, &user); if balance < amount { return Err(TokenError::InsufficientBalance); } - Self::write_balance(&env, &user, balance - amount); let mut lockup = env .storage() .persistent() .get::<_, LockupInfo>(&DataKey::Lockup(user.clone())) - .unwrap_or(LockupInfo { - amount: 0, - unlock_time: 0, - }); + .unwrap_or(LockupInfo { amount: 0, unlock_time: 0 }); lockup.amount += amount; if unlock_time > lockup.unlock_time { lockup.unlock_time = unlock_time; } - env.storage() - .persistent() - .set(&DataKey::Lockup(user.clone()), &lockup); + env.storage().persistent().set(&DataKey::Lockup(user.clone()), &lockup); events::emit_locked(&env, &user, amount, lockup.unlock_time); Ok(()) } @@ -459,16 +445,12 @@ impl BcForgeToken { .persistent() .get(&DataKey::Lockup(user.clone())) .expect("no lockup found"); - if env.ledger().timestamp() < lockup.unlock_time { panic!("tokens are still locked"); } - let balance = Self::read_balance(&env, &user); Self::write_balance(&env, &user, balance + lockup.amount); - env.storage() - .persistent() - .remove(&DataKey::Lockup(user.clone())); + env.storage().persistent().remove(&DataKey::Lockup(user.clone())); events::emit_withdraw_locked(&env, &user, lockup.amount); } @@ -483,9 +465,7 @@ impl BcForgeToken { pub fn propose_owner(env: Env, new_admin: Address) -> Result<(), TokenError> { let current_admin = Self::read_admin(&env)?; current_admin.require_auth(); - env.storage() - .instance() - .set(&DataKey::PendingAdmin, &new_admin); + env.storage().instance().set(&DataKey::PendingAdmin, &new_admin); events::emit_ownership_proposed(&env, ¤t_admin, &new_admin); Ok(()) } @@ -529,23 +509,19 @@ impl BcForgeToken { pub fn upgrade(env: Env, new_wasm_hash: BytesN<32>) -> Result<(), TokenError> { let current_admin = Self::read_admin(&env)?; current_admin.require_auth(); - env.deployer() - .update_current_contract_wasm(new_wasm_hash.clone()); + env.deployer().update_current_contract_wasm(new_wasm_hash.clone()); events::emit_upgrade(&env, ¤t_admin, &new_wasm_hash); Ok(()) } pub fn version(env: Env) -> String { - String::from_str(&env, "1.1.0") + String::from_str(&env, "1.2.0") } pub fn update_name(env: Env, new_name: String) -> Result<(), TokenError> { let current_admin = Self::read_admin(&env)?; current_admin.require_auth(); - let old_name = env - .storage() - .instance() - .get(&DataKey::Name) + let old_name = env.storage().instance().get(&DataKey::Name) .unwrap_or_else(|| String::from_str(&env, "bc-forge")); env.storage().instance().set(&DataKey::Name, &new_name); events::emit_update_name(&env, ¤t_admin, &old_name, &new_name); @@ -555,10 +531,7 @@ impl BcForgeToken { pub fn update_symbol(env: Env, new_symbol: String) -> Result<(), TokenError> { let current_admin = Self::read_admin(&env)?; current_admin.require_auth(); - let old_symbol = env - .storage() - .instance() - .get(&DataKey::Symbol) + let old_symbol = env.storage().instance().get(&DataKey::Symbol) .unwrap_or_else(|| String::from_str(&env, "SFG")); env.storage().instance().set(&DataKey::Symbol, &new_symbol); events::emit_update_symbol(&env, ¤t_admin, &old_symbol, &new_symbol); @@ -592,11 +565,9 @@ impl TokenInterface for BcForgeToken { Self::panic_on_err(&env, Self::ensure_initialized(&env)); Self::panic_on_err(&env, Self::ensure_not_paused(&env)); from.require_auth(); - if amount <= 0 { soroban_sdk::panic_with_error!(&env, TokenError::InvalidAmount); } - let _ = Self::panic_on_err(&env, Self::move_balance(&env, &from, &to, amount)); events::emit_transfer(&env, &from, &to, amount); } @@ -605,11 +576,9 @@ impl TokenInterface for BcForgeToken { Self::panic_on_err(&env, Self::ensure_initialized(&env)); Self::panic_on_err(&env, Self::ensure_not_paused(&env)); spender.require_auth(); - if amount <= 0 { soroban_sdk::panic_with_error!(&env, TokenError::InvalidAmount); } - let allowance = Self::read_allowance(&env, &from, &spender); if allowance < amount { soroban_sdk::panic_with_error!(&env, TokenError::InsufficientAllowance); @@ -628,16 +597,13 @@ impl TokenInterface for BcForgeToken { Self::panic_on_err(&env, Self::ensure_initialized(&env)); Self::panic_on_err(&env, Self::ensure_not_paused(&env)); from.require_auth(); - if amount <= 0 { soroban_sdk::panic_with_error!(&env, TokenError::InvalidAmount); } - let balance = Self::read_balance(&env, &from); if balance < amount { soroban_sdk::panic_with_error!(&env, TokenError::InsufficientBalance); } - let new_balance = balance - amount; Self::write_balance(&env, &from, new_balance); let supply = Self::read_supply(&env) - amount; @@ -649,16 +615,13 @@ impl TokenInterface for BcForgeToken { Self::panic_on_err(&env, Self::ensure_initialized(&env)); Self::panic_on_err(&env, Self::ensure_not_paused(&env)); spender.require_auth(); - if amount <= 0 { soroban_sdk::panic_with_error!(&env, TokenError::InvalidAmount); } - let allowance = Self::read_allowance(&env, &from, &spender); if allowance < amount { soroban_sdk::panic_with_error!(&env, TokenError::InsufficientAllowance); } - let balance = Self::read_balance(&env, &from); if balance < amount { soroban_sdk::panic_with_error!(&env, TokenError::InsufficientBalance); @@ -676,25 +639,18 @@ impl TokenInterface for BcForgeToken { fn decimals(env: Env) -> u32 { Self::panic_on_err(&env, Self::ensure_initialized(&env)); - env.storage() - .instance() - .get(&DataKey::Decimals) - .unwrap_or(7) + env.storage().instance().get(&DataKey::Decimals).unwrap_or(7) } fn name(env: Env) -> String { Self::panic_on_err(&env, Self::ensure_initialized(&env)); - env.storage() - .instance() - .get(&DataKey::Name) + env.storage().instance().get(&DataKey::Name) .unwrap_or_else(|| String::from_str(&env, "bc-forge")) } fn symbol(env: Env) -> String { Self::panic_on_err(&env, Self::ensure_initialized(&env)); - env.storage() - .instance() - .get(&DataKey::Symbol) + env.storage().instance().get(&DataKey::Symbol) .unwrap_or_else(|| String::from_str(&env, "SFG")) } } diff --git a/contracts/token/src/test.rs b/contracts/token/src/test.rs index 1de36a0..d6d4027 100644 --- a/contracts/token/src/test.rs +++ b/contracts/token/src/test.rs @@ -1,4 +1,4 @@ -#![cfg(test)] +#![cfg(test)] use soroban_sdk::testutils::Address as _; use soroban_sdk::{vec, Address, Env, String, Vec}; @@ -6,17 +6,20 @@ use soroban_sdk::{vec, Address, Env, String, Vec}; use crate::{BcForgeToken, BcForgeTokenClient, TokenError}; fn setup(env: &Env) -> (BcForgeTokenClient<'_>, Address) { + setup_with_max_supply(env, None) +} + +fn setup_with_max_supply(env: &Env, max_supply: Option) -> (BcForgeTokenClient<'_>, Address) { let contract_id = env.register(BcForgeToken, ()); let client = BcForgeTokenClient::new(env, &contract_id); let admin = Address::generate(env); - client.initialize( &admin, &7, &String::from_str(env, "bc-forge Token"), &String::from_str(env, "SFG"), + &max_supply, ); - (client, admin) } @@ -27,10 +30,8 @@ fn test_transfer() { let (client, _admin) = setup(&env); let from = Address::generate(&env); let to = Address::generate(&env); - client.mint(&from, &1000); client.transfer(&from, &to, &300); - assert_eq!(client.balance(&from), 700); assert_eq!(client.balance(&to), 300); assert_eq!(client.supply(), 1000); @@ -825,9 +826,7 @@ fn test_batch_transfer_multiple_recipients() { let recipient_a = Address::generate(&env); let recipient_b = Address::generate(&env); let recipient_c = Address::generate(&env); - client.mint(&from, &1000); - let recipients = vec![ &env, (recipient_a.clone(), 100_i128), @@ -835,7 +834,6 @@ fn test_batch_transfer_multiple_recipients() { (recipient_c.clone(), 50_i128), ]; client.batch_transfer(&from, &recipients); - assert_eq!(client.balance(&from), 600); assert_eq!(client.balance(&recipient_a), 100); assert_eq!(client.balance(&recipient_b), 250); @@ -850,9 +848,7 @@ fn test_batch_transfer_rejects_invalid_amount() { let (client, _admin) = setup(&env); let from = Address::generate(&env); let recipient = Address::generate(&env); - client.mint(&from, &1000); - let recipients = vec![&env, (recipient.clone(), 0_i128)]; assert_eq!( client.try_batch_transfer(&from, &recipients), @@ -872,9 +868,7 @@ fn test_batch_transfer_rejects_insufficient_balance_before_moving_tokens() { let from = Address::generate(&env); let recipient_a = Address::generate(&env); let recipient_b = Address::generate(&env); - client.mint(&from, &100); - let recipients = vec![ &env, (recipient_a.clone(), 80_i128), @@ -898,10 +892,8 @@ fn test_batch_transfer_while_paused_returns_error() { let (client, _admin) = setup(&env); let from = Address::generate(&env); let recipient = Address::generate(&env); - client.mint(&from, &100); client.pause(); - let recipients: Vec<(Address, i128)> = vec![&env, (recipient, 10_i128)]; assert_eq!( client.try_batch_transfer(&from, &recipients), @@ -910,3 +902,139 @@ fn test_batch_transfer_while_paused_returns_error() { ))) ); } + +#[test] +fn test_no_max_supply_by_default() { + let env = Env::default(); + env.mock_all_auths(); + let (client, _admin) = setup(&env); + assert_eq!(client.max_supply(), None); +} + +#[test] +fn test_initialize_with_max_supply() { + let env = Env::default(); + env.mock_all_auths(); + let (client, _admin) = setup_with_max_supply(&env, Some(1_000_000)); + assert_eq!(client.max_supply(), Some(1_000_000)); +} + +#[test] +fn test_mint_within_max_supply_succeeds() { + let env = Env::default(); + env.mock_all_auths(); + let (client, _admin) = setup_with_max_supply(&env, Some(1_000_000)); + let user = Address::generate(&env); + client.mint(&user, &500_000); + assert_eq!(client.supply(), 500_000); + client.mint(&user, &500_000); + assert_eq!(client.supply(), 1_000_000); +} + +#[test] +fn test_mint_exactly_at_max_supply_succeeds() { + let env = Env::default(); + env.mock_all_auths(); + let (client, _admin) = setup_with_max_supply(&env, Some(1_000)); + let user = Address::generate(&env); + client.mint(&user, &1_000); + assert_eq!(client.supply(), 1_000); + assert_eq!(client.balance(&user), 1_000); +} + +#[test] +fn test_mint_exceeding_max_supply_fails() { + let env = Env::default(); + env.mock_all_auths(); + let (client, _admin) = setup_with_max_supply(&env, Some(1_000)); + let user = Address::generate(&env); + client.mint(&user, &1_000); + assert_eq!( + client.try_mint(&user, &1), + Err(Ok(TokenError::MaxSupplyExceeded)) + ); + assert_eq!(client.supply(), 1_000); +} + +#[test] +fn test_mint_partially_exceeding_max_supply_fails() { + let env = Env::default(); + env.mock_all_auths(); + let (client, _admin) = setup_with_max_supply(&env, Some(1_000)); + let user = Address::generate(&env); + client.mint(&user, &900); + assert_eq!( + client.try_mint(&user, &200), + Err(Ok(TokenError::MaxSupplyExceeded)) + ); + assert_eq!(client.supply(), 900); +} + +#[test] +fn test_uncapped_supply_allows_unlimited_mint() { + let env = Env::default(); + env.mock_all_auths(); + let (client, _admin) = setup(&env); + let user = Address::generate(&env); + client.mint(&user, &1_000_000_000); + client.mint(&user, &1_000_000_000); + assert_eq!(client.supply(), 2_000_000_000); +} + +#[test] +fn test_batch_mint_respects_max_supply() { + let env = Env::default(); + env.mock_all_auths(); + let (client, _admin) = setup_with_max_supply(&env, Some(500)); + let user_a = Address::generate(&env); + let user_b = Address::generate(&env); + use crate::Recipient; + let recipients = vec![ + &env, + Recipient { address: user_a.clone(), amount: 300 }, + Recipient { address: user_b.clone(), amount: 300 }, + ]; + assert_eq!( + client.try_batch_mint(&recipients), + Err(Ok(TokenError::MaxSupplyExceeded)) + ); + assert_eq!(client.supply(), 0); +} + +#[test] +fn test_initialize_with_zero_max_supply_fails() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register(BcForgeToken, ()); + let client = BcForgeTokenClient::new(&env, &contract_id); + let admin = Address::generate(&env); + assert_eq!( + client.try_initialize( + &admin, + &7, + &String::from_str(&env, "bc-forge Token"), + &String::from_str(&env, "SFG"), + &Some(0_i128), + ), + Err(Ok(TokenError::InvalidAmount)) + ); +} + +#[test] +fn test_initialize_with_negative_max_supply_fails() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register(BcForgeToken, ()); + let client = BcForgeTokenClient::new(&env, &contract_id); + let admin = Address::generate(&env); + assert_eq!( + client.try_initialize( + &admin, + &7, + &String::from_str(&env, "bc-forge Token"), + &String::from_str(&env, "SFG"), + &Some(-1_i128), + ), + Err(Ok(TokenError::InvalidAmount)) + ); +}