diff --git a/contracts/token-factory/Cargo.toml b/contracts/token-factory/Cargo.toml index 12381a80..12305da4 100644 --- a/contracts/token-factory/Cargo.toml +++ b/contracts/token-factory/Cargo.toml @@ -1,19 +1,18 @@ -[package] -name = "token-factory" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -soroban-sdk = "21.0.0" -soroban-token-sdk = { version = "21.0.0" } -proptest = "1.4" - -[dev-dependencies] -soroban-sdk = { version = "21.0.0", features = ["testutils"] } -soroban-token-sdk = { version = "21.0.0", features = ["testutils"] } - -[features] -testutils = ["soroban-sdk/testutils", "soroban-token-sdk/testutils"] \ No newline at end of file +[package] +name = "token-factory" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +soroban-sdk = "21.0.0" +soroban-token-sdk = { version = "21.0.0" } +proptest = "1.4" + +[dev-dependencies] +soroban-sdk = { version = "21.0.0", features = ["testutils"] } + +[features] +testutils = ["soroban-sdk/testutils"] \ No newline at end of file diff --git a/contracts/token-factory/src/lib.rs b/contracts/token-factory/src/lib.rs index 2303f5d4..ad68ec47 100644 --- a/contracts/token-factory/src/lib.rs +++ b/contracts/token-factory/src/lib.rs @@ -1,322 +1,322 @@ -#![no_std] - -use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, String, Vec, vec, symbol_short, token}; -use soroban_token_sdk::TokenClient; - -#[contracttype] -#[derive(Clone)] -pub struct TokenInfo { - pub name: String, - pub symbol: String, - pub decimals: u32, - pub creator: Address, - pub created_at: u64, -} - -#[contracttype] -#[derive(Clone)] -pub struct FactoryState { - pub admin: Address, - pub paused: bool, - pub treasury: Address, - pub base_fee: i128, - pub metadata_fee: i128, - pub token_count: u32, -} - -#[contract] -pub struct TokenFactory; - -#[contractimpl] -impl TokenFactory { - pub fn initialize( - env: Env, - admin: Address, - treasury: Address, - base_fee: i128, - metadata_fee: i128, - ) -> Result<(), Error> { - if env.storage().instance().has(&symbol_short!("init")) { - return Err(Error::AlreadyInitialized); - } - - // FIX 1: Added `paused: false` to the FactoryState initializer - let state = FactoryState { - admin: admin.clone(), - paused: false, - treasury, - base_fee, - metadata_fee, - token_count: 0, - }; - - env.storage().instance().set(&symbol_short!("state"), &state); - env.storage().instance().set(&symbol_short!("init"), &true); - - env.events().publish((symbol_short!("init"),), (admin,)); - - Ok(()) - } - - // FIX 2: Replaced DataKey::State with symbol_short!("state") to match the rest of the codebase - fn require_not_paused(env: &Env) -> Result<(), Error> { - let state: FactoryState = env.storage().instance().get(&symbol_short!("state")).unwrap(); - if state.paused { - return Err(Error::ContractPaused); - } - Ok(()) - } - - pub fn create_token( - env: Env, - creator: Address, - name: String, - symbol: String, - decimals: u32, - initial_supply: i128, - fee_payment: i128, - ) -> Result { - // FIX 3: Changed require_not_paused(&env) to Self::require_not_paused(&env) - Self::require_not_paused(&env)?; - creator.require_auth(); - - let state: FactoryState = env.storage().instance().get(&symbol_short!("state")).unwrap(); - - if fee_payment < state.base_fee { - return Err(Error::InsufficientFee); - } - - // Transfer fee to treasury - token::StellarAssetClient::new(&env, &env.current_contract_address()).transfer( - &creator, - &state.treasury, - &fee_payment, - ); - - // Deploy token using soroban-token-sdk - let token_address = env.deployer().deploy_token( - &name, - &symbol, - &decimals, - &creator, - &initial_supply, - ); - - // Store token info - let token_info = TokenInfo { - name, - symbol, - decimals, - creator: creator.clone(), - created_at: env.ledger().timestamp(), - }; - - let mut token_count = state.token_count; - token_count += 1; - - env.storage().instance().set(&token_count, &token_info); - env.storage().instance().set(&symbol_short!("state"), &FactoryState { - token_count, - ..state - }); - - // Append token index to creator's list - let creator_key = (symbol_short!("cr_tokens"), creator.clone()); - let mut creator_tokens: Vec = env - .storage() - .instance() - .get(&creator_key) - .unwrap_or_else(|| vec![&env]); - creator_tokens.push_back(token_count); - env.storage().instance().set(&creator_key, &creator_tokens); - - env.events().publish((symbol_short!("token_created"),), (token_address.clone(), creator)); - - Ok(token_address) - } - - pub fn set_metadata( - env: Env, - token_address: Address, - admin: Address, - metadata_uri: String, - fee_payment: i128, - ) -> Result<(), Error> { - // FIX 3: Changed require_not_paused(&env) to Self::require_not_paused(&env) - Self::require_not_paused(&env)?; - admin.require_auth(); - - let state: FactoryState = env.storage().instance().get(&symbol_short!("state")).unwrap(); - - if fee_payment < state.metadata_fee { - return Err(Error::InsufficientFee); - } - - // Transfer fee - token::StellarAssetClient::new(&env, &env.current_contract_address()).transfer( - &admin, - &state.treasury, - &fee_payment, - ); - - env.storage().instance().set(&(&token_address, symbol_short!("metadata")), &metadata_uri); - - env.events().publish((symbol_short!("metadata_set"),), (token_address, metadata_uri)); - - Ok(()) - } - - pub fn mint_tokens( - env: Env, - token_address: Address, - admin: Address, - to: Address, - amount: i128, - fee_payment: i128, - ) -> Result<(), Error> { - // FIX 3: Changed require_not_paused(&env) to Self::require_not_paused(&env) - Self::require_not_paused(&env)?; - admin.require_auth(); - - let state: FactoryState = env.storage().instance().get(&symbol_short!("state")).unwrap(); - - if fee_payment < state.base_fee { - return Err(Error::InsufficientFee); - } - - // Transfer fee - token::StellarAssetClient::new(&env, &env.current_contract_address()).transfer( - &admin, - &state.treasury, - &fee_payment, - ); - - // Mint tokens - TokenClient::new(&env, &token_address).mint(&admin, &to, &amount); - - env.events().publish((symbol_short!("tokens_minted"),), (token_address, to, amount)); - - Ok(()) - } - - pub fn burn( - env: Env, - token_address: Address, - from: Address, - amount: i128, - ) -> Result<(), Error> { - // NOTE: burn intentionally has NO require_not_paused check - from.require_auth(); - - if amount <= 0 { - return Err(Error::InvalidBurnAmount); - } - - TokenClient::new(&env, &token_address).burn(&from, &amount); - - env.events().publish((symbol_short!("tokens_burned"),), (token_address, from, amount)); - - Ok(()) - } - - // FIX 2: Replaced DataKey::State with symbol_short!("state") throughout pause/unpause - pub fn pause(env: Env, admin: Address) -> Result<(), Error> { - admin.require_auth(); - let mut state: FactoryState = env.storage().instance().get(&symbol_short!("state")).unwrap(); - - if state.admin != admin { - return Err(Error::Unauthorized); - } - - state.paused = true; - env.storage().instance().set(&symbol_short!("state"), &state); - Ok(()) - } - - pub fn unpause(env: Env, admin: Address) -> Result<(), Error> { - admin.require_auth(); - let mut state: FactoryState = env.storage().instance().get(&symbol_short!("state")).unwrap(); - - if state.admin != admin { - return Err(Error::Unauthorized); - } - - state.paused = false; - env.storage().instance().set(&symbol_short!("state"), &state); - Ok(()) - } - - pub fn update_fees( - env: Env, - admin: Address, - base_fee: Option, - metadata_fee: Option, - ) -> Result<(), Error> { - admin.require_auth(); - - let mut state: FactoryState = env.storage().instance().get(&symbol_short!("state")).unwrap(); - - if admin != state.admin { - return Err(Error::Unauthorized); - } - - if let Some(fee) = base_fee { - state.base_fee = fee; - } - if let Some(fee) = metadata_fee { - state.metadata_fee = fee; - } - - env.storage().instance().set(&symbol_short!("state"), &state); - - env.events().publish((symbol_short!("fees_updated"),), (base_fee, metadata_fee)); - - Ok(()) - } - - pub fn get_state(env: Env) -> FactoryState { - env.storage().instance().get(&symbol_short!("state")).unwrap() - } - - pub fn get_base_fee(env: Env) -> i128 { - Self::get_state(env).base_fee - } - - pub fn get_metadata_fee(env: Env) -> i128 { - Self::get_state(env).metadata_fee - } - - pub fn get_token_info(env: Env, index: u32) -> Result { - match env.storage().instance().get(&index) { - Some(info) => Ok(info), - None => Err(Error::TokenNotFound), - } - } - - pub fn get_tokens_by_creator(env: Env, creator: Address) -> Vec { - let creator_key = (symbol_short!("cr_tokens"), creator); - env.storage() - .instance() - .get(&creator_key) - .unwrap_or_else(|| vec![&env]) - } -} - -#[contracttype] -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum Error { - InsufficientFee = 1, - Unauthorized = 2, - InvalidParameters = 3, - TokenNotFound = 4, - MetadataAlreadySet = 5, - AlreadyInitialized = 6, - BurnAmountExceedsBalance = 7, - BurnNotEnabled = 8, - InvalidBurnAmount = 9, - // FIX 4: Replaced X with 10 - ContractPaused = 10, -} - -#[cfg(test)] -mod test; +#![no_std] + +use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, String, Vec, vec, symbol_short, token}; +use soroban_token_sdk::TokenClient; + +#[contracttype] +#[derive(Clone)] +pub struct TokenInfo { + pub name: String, + pub symbol: String, + pub decimals: u32, + pub creator: Address, + pub created_at: u64, +} + +#[contracttype] +#[derive(Clone)] +pub struct FactoryState { + pub admin: Address, + pub paused: bool, + pub treasury: Address, + pub base_fee: i128, + pub metadata_fee: i128, + pub token_count: u32, +} + +#[contract] +pub struct TokenFactory; + +#[contractimpl] +impl TokenFactory { + pub fn initialize( + env: Env, + admin: Address, + treasury: Address, + base_fee: i128, + metadata_fee: i128, + ) -> Result<(), Error> { + if env.storage().instance().has(&symbol_short!("init")) { + return Err(Error::AlreadyInitialized); + } + + // FIX 1: Added `paused: false` to the FactoryState initializer + let state = FactoryState { + admin: admin.clone(), + paused: false, + treasury, + base_fee, + metadata_fee, + token_count: 0, + }; + + env.storage().instance().set(&symbol_short!("state"), &state); + env.storage().instance().set(&symbol_short!("init"), &true); + + env.events().publish((symbol_short!("init"),), (admin,)); + + Ok(()) + } + + // FIX 2: Replaced DataKey::State with symbol_short!("state") to match the rest of the codebase + fn require_not_paused(env: &Env) -> Result<(), Error> { + let state: FactoryState = env.storage().instance().get(&symbol_short!("state")).unwrap(); + if state.paused { + return Err(Error::ContractPaused); + } + Ok(()) + } + + pub fn create_token( + env: Env, + creator: Address, + name: String, + symbol: String, + decimals: u32, + initial_supply: i128, + fee_payment: i128, + ) -> Result { + // FIX 3: Changed require_not_paused(&env) to Self::require_not_paused(&env) + Self::require_not_paused(&env)?; + creator.require_auth(); + + let state: FactoryState = env.storage().instance().get(&symbol_short!("state")).unwrap(); + + if fee_payment < state.base_fee { + return Err(Error::InsufficientFee); + } + + // Transfer fee to treasury + token::StellarAssetClient::new(&env, &env.current_contract_address()).transfer( + &creator, + &state.treasury, + &fee_payment, + ); + + // Deploy token using soroban-token-sdk + let token_address = env.deployer().deploy_token( + &name, + &symbol, + &decimals, + &creator, + &initial_supply, + ); + + // Store token info + let token_info = TokenInfo { + name, + symbol, + decimals, + creator: creator.clone(), + created_at: env.ledger().timestamp(), + }; + + let mut token_count = state.token_count; + token_count += 1; + + env.storage().instance().set(&token_count, &token_info); + env.storage().instance().set(&symbol_short!("state"), &FactoryState { + token_count, + ..state + }); + + // Append token index to creator's list + let creator_key = (symbol_short!("cr_tokens"), creator.clone()); + let mut creator_tokens: Vec = env + .storage() + .instance() + .get(&creator_key) + .unwrap_or_else(|| vec![&env]); + creator_tokens.push_back(token_count); + env.storage().instance().set(&creator_key, &creator_tokens); + + env.events().publish((symbol_short!("token_created"),), (token_address.clone(), creator)); + + Ok(token_address) + } + + pub fn set_metadata( + env: Env, + token_address: Address, + admin: Address, + metadata_uri: String, + fee_payment: i128, + ) -> Result<(), Error> { + // FIX 3: Changed require_not_paused(&env) to Self::require_not_paused(&env) + Self::require_not_paused(&env)?; + admin.require_auth(); + + let state: FactoryState = env.storage().instance().get(&symbol_short!("state")).unwrap(); + + if fee_payment < state.metadata_fee { + return Err(Error::InsufficientFee); + } + + // Transfer fee + token::StellarAssetClient::new(&env, &env.current_contract_address()).transfer( + &admin, + &state.treasury, + &fee_payment, + ); + + env.storage().instance().set(&(&token_address, symbol_short!("metadata")), &metadata_uri); + + env.events().publish((symbol_short!("metadata_set"),), (token_address, metadata_uri)); + + Ok(()) + } + + pub fn mint_tokens( + env: Env, + token_address: Address, + admin: Address, + to: Address, + amount: i128, + fee_payment: i128, + ) -> Result<(), Error> { + // FIX 3: Changed require_not_paused(&env) to Self::require_not_paused(&env) + Self::require_not_paused(&env)?; + admin.require_auth(); + + let state: FactoryState = env.storage().instance().get(&symbol_short!("state")).unwrap(); + + if fee_payment < state.base_fee { + return Err(Error::InsufficientFee); + } + + // Transfer fee + token::StellarAssetClient::new(&env, &env.current_contract_address()).transfer( + &admin, + &state.treasury, + &fee_payment, + ); + + // Mint tokens + TokenClient::new(&env, &token_address).mint(&admin, &to, &amount); + + env.events().publish((symbol_short!("tokens_minted"),), (token_address, to, amount)); + + Ok(()) + } + + pub fn burn( + env: Env, + token_address: Address, + from: Address, + amount: i128, + ) -> Result<(), Error> { + // NOTE: burn intentionally has NO require_not_paused check + from.require_auth(); + + if amount <= 0 { + return Err(Error::InvalidBurnAmount); + } + + TokenClient::new(&env, &token_address).burn(&from, &amount); + + env.events().publish((symbol_short!("tokens_burned"),), (token_address, from, amount)); + + Ok(()) + } + + // FIX 2: Replaced DataKey::State with symbol_short!("state") throughout pause/unpause + pub fn pause(env: Env, admin: Address) -> Result<(), Error> { + admin.require_auth(); + let mut state: FactoryState = env.storage().instance().get(&symbol_short!("state")).unwrap(); + + if state.admin != admin { + return Err(Error::Unauthorized); + } + + state.paused = true; + env.storage().instance().set(&symbol_short!("state"), &state); + Ok(()) + } + + pub fn unpause(env: Env, admin: Address) -> Result<(), Error> { + admin.require_auth(); + let mut state: FactoryState = env.storage().instance().get(&symbol_short!("state")).unwrap(); + + if state.admin != admin { + return Err(Error::Unauthorized); + } + + state.paused = false; + env.storage().instance().set(&symbol_short!("state"), &state); + Ok(()) + } + + pub fn update_fees( + env: Env, + admin: Address, + base_fee: Option, + metadata_fee: Option, + ) -> Result<(), Error> { + admin.require_auth(); + + let mut state: FactoryState = env.storage().instance().get(&symbol_short!("state")).unwrap(); + + if admin != state.admin { + return Err(Error::Unauthorized); + } + + if let Some(fee) = base_fee { + state.base_fee = fee; + } + if let Some(fee) = metadata_fee { + state.metadata_fee = fee; + } + + env.storage().instance().set(&symbol_short!("state"), &state); + + env.events().publish((symbol_short!("fees_updated"),), (base_fee, metadata_fee)); + + Ok(()) + } + + pub fn get_state(env: Env) -> FactoryState { + env.storage().instance().get(&symbol_short!("state")).unwrap() + } + + pub fn get_base_fee(env: Env) -> i128 { + Self::get_state(env).base_fee + } + + pub fn get_metadata_fee(env: Env) -> i128 { + Self::get_state(env).metadata_fee + } + + pub fn get_token_info(env: Env, index: u32) -> Result { + match env.storage().instance().get(&index) { + Some(info) => Ok(info), + None => Err(Error::TokenNotFound), + } + } + + pub fn get_tokens_by_creator(env: Env, creator: Address) -> Vec { + let creator_key = (symbol_short!("cr_tokens"), creator); + env.storage() + .instance() + .get(&creator_key) + .unwrap_or_else(|| vec![&env]) + } +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Error { + InsufficientFee = 1, + Unauthorized = 2, + InvalidParameters = 3, + TokenNotFound = 4, + MetadataAlreadySet = 5, + AlreadyInitialized = 6, + BurnAmountExceedsBalance = 7, + BurnNotEnabled = 8, + InvalidBurnAmount = 9, + // FIX 4: Replaced X with 10 + ContractPaused = 10, +} + +#[cfg(test)] +mod test; diff --git a/contracts/token-factory/src/test.rs b/contracts/token-factory/src/test.rs index 018ddee2..19c23fdc 100644 --- a/contracts/token-factory/src/test.rs +++ b/contracts/token-factory/src/test.rs @@ -1,256 +1,256 @@ -#![cfg(test)] - -use super::*; -use soroban_sdk::{ - testutils::{Address as _, AuthorizedFunction, AuthorizedInvocation}, - Address, Env, String, -}; - -// ── helpers ────────────────────────────────────────────────────────────────── - -fn setup_env() -> (Env, TokenFactoryClient<'static>, Address, Address) { - let env = Env::default(); - env.mock_all_auths(); - - let contract_id = env.register_contract(None, TokenFactory); - let client = TokenFactoryClient::new(&env, &contract_id); - - let admin = Address::generate(&env); - let treasury = Address::generate(&env); - - client.initialize(&admin, &treasury, &1000, &500); - - (env, client, admin, treasury) -} - -// ── pause / unpause ─────────────────────────────────────────────────────────── - -#[test] -fn test_initial_state_is_not_paused() { - let (_env, client, _admin, _treasury) = setup_env(); - let state = client.get_state(); - assert!(!state.paused); -} - -#[test] -fn test_admin_can_pause() { - let (_env, client, admin, _treasury) = setup_env(); - client.pause(&admin); - let state = client.get_state(); - assert!(state.paused); -} - -#[test] -fn test_admin_can_unpause() { - let (_env, client, admin, _treasury) = setup_env(); - client.pause(&admin); - client.unpause(&admin); - let state = client.get_state(); - assert!(!state.paused); -} - -#[test] -fn test_non_admin_cannot_pause() { - let (env, client, _admin, _treasury) = setup_env(); - let stranger = Address::generate(&env); - - let result = client.try_pause(&stranger); - assert_eq!(result, Err(Ok(Error::Unauthorized))); -} - -#[test] -fn test_non_admin_cannot_unpause() { - let (env, client, admin, _treasury) = setup_env(); - let stranger = Address::generate(&env); - - client.pause(&admin); - let result = client.try_unpause(&stranger); - assert_eq!(result, Err(Ok(Error::Unauthorized))); -} - -// ── paused blocks create_token, mint_tokens, set_metadata ──────────────────── - -#[test] -fn test_create_token_blocked_when_paused() { - let (env, client, admin, _treasury) = setup_env(); - client.pause(&admin); - - let creator = Address::generate(&env); - let result = client.try_create_token( - &creator, - &String::from_str(&env, "MyToken"), - &String::from_str(&env, "MTK"), - &7, - &1_000_000, - &1000, - ); - - assert_eq!(result, Err(Ok(Error::ContractPaused))); -} - -#[test] -fn test_mint_tokens_blocked_when_paused() { - let (env, client, admin, _treasury) = setup_env(); - client.pause(&admin); - - let token_address = Address::generate(&env); - let recipient = Address::generate(&env); - - let result = client.try_mint_tokens( - &token_address, - &admin, - &recipient, - &500, - &1000, - ); - - assert_eq!(result, Err(Ok(Error::ContractPaused))); -} - -#[test] -fn test_set_metadata_blocked_when_paused() { - let (env, client, admin, _treasury) = setup_env(); - client.pause(&admin); - - let token_address = Address::generate(&env); - - let result = client.try_set_metadata( - &token_address, - &admin, - &String::from_str(&env, "https://example.com/meta.json"), - &500, - ); - - assert_eq!(result, Err(Ok(Error::ContractPaused))); -} - -// ── unpause restores functionality ─────────────────────────────────────────── - -#[test] -fn test_create_token_works_after_unpause() { - // This test just verifies unpause lifts the block. - // create_token will still fail due to fee transfer in test env, - // but the error should NOT be ContractPaused. - let (env, client, admin, _treasury) = setup_env(); - - client.pause(&admin); - client.unpause(&admin); - - let creator = Address::generate(&env); - let result = client.try_create_token( - &creator, - &String::from_str(&env, "MyToken"), - &String::from_str(&env, "MTK"), - &7, - &1_000_000, - &1000, - ); - - // Should NOT be ContractPaused — any other error is fine here - assert_ne!(result, Err(Ok(Error::ContractPaused))); -} - -// ── burn is NOT blocked by pause ───────────────────────────────────────────── - -#[test] -fn test_burn_not_blocked_when_paused() { - let (env, client, admin, _treasury) = setup_env(); - client.pause(&admin); - - let token_address = Address::generate(&env); - let burner = Address::generate(&env); - - // burn will fail because the token isn't real in this unit test, - // but the error must NOT be ContractPaused - let result = client.try_burn(&token_address, &burner, &100); - assert_ne!(result, Err(Ok(Error::ContractPaused))); -} - -// ── transfer_admin ──────────────────────────────────────────────────────────── - -#[test] -fn test_admin_can_transfer_ownership() { - let (env, client, admin, _treasury) = setup_env(); - let new_admin = Address::generate(&env); - - client.transfer_admin(&admin, &new_admin); - - let state = client.get_state(); - assert_eq!(state.admin, new_admin); -} - -#[test] -fn test_old_admin_loses_privileges_after_transfer() { - let (env, client, admin, _treasury) = setup_env(); - let new_admin = Address::generate(&env); - - client.transfer_admin(&admin, &new_admin); - - // old admin can no longer pause - let result = client.try_pause(&admin); - assert_eq!(result, Err(Ok(Error::Unauthorized))); -} - -#[test] -fn test_non_admin_cannot_transfer_admin() { - let (env, client, _admin, _treasury) = setup_env(); - let stranger = Address::generate(&env); - let new_admin = Address::generate(&env); - - let result = client.try_transfer_admin(&stranger, &new_admin); - assert_eq!(result, Err(Ok(Error::Unauthorized))); -} - -#[test] -fn test_transfer_admin_to_same_address_fails() { - let (_env, client, admin, _treasury) = setup_env(); - - let result = client.try_transfer_admin(&admin, &admin); - assert_eq!(result, Err(Ok(Error::InvalidParameters))); -} - -// ── get_tokens_by_creator ───────────────────────────────────────────────────── - -#[test] -fn test_get_tokens_by_creator_returns_empty_for_unknown_address() { - let (env, client, _admin, _treasury) = setup_env(); - let stranger = Address::generate(&env); - - let indices = client.get_tokens_by_creator(&stranger); - assert_eq!(indices.len(), 0); -} - -#[test] -fn test_get_tokens_by_creator_returns_correct_indices() { - let (env, client, _admin, _treasury) = setup_env(); - let creator = Address::generate(&env); - - // create_token will fail at the fee-transfer step in the test env, - // so we call it twice and verify both indices are tracked. - // We use try_create_token and only care that the creator list is updated - // when the call succeeds. Since fee transfer fails in unit tests we - // verify the storage key logic by checking the empty-vec baseline and - // that a second creator gets an independent empty list. - let creator2 = Address::generate(&env); - - let indices1 = client.get_tokens_by_creator(&creator); - let indices2 = client.get_tokens_by_creator(&creator2); - - // Both unknown creators return empty vecs - assert_eq!(indices1.len(), 0); - assert_eq!(indices2.len(), 0); - - // Confirm they are independent (not the same object) - assert_eq!(indices1, indices2); -} - -#[test] -fn test_get_tokens_by_creator_different_creators_are_independent() { - let (env, client, _admin, _treasury) = setup_env(); - let creator_a = Address::generate(&env); - let creator_b = Address::generate(&env); - - // Neither has tokens — both return empty - assert_eq!(client.get_tokens_by_creator(&creator_a).len(), 0); - assert_eq!(client.get_tokens_by_creator(&creator_b).len(), 0); -} \ No newline at end of file +#![cfg(test)] + +use super::*; +use soroban_sdk::{ + testutils::{Address as _, AuthorizedFunction, AuthorizedInvocation}, + Address, Env, String, +}; + +// ── helpers ────────────────────────────────────────────────────────────────── + +fn setup_env() -> (Env, TokenFactoryClient<'static>, Address, Address) { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register_contract(None, TokenFactory); + let client = TokenFactoryClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let treasury = Address::generate(&env); + + client.initialize(&admin, &treasury, &1000, &500); + + (env, client, admin, treasury) +} + +// ── pause / unpause ─────────────────────────────────────────────────────────── + +#[test] +fn test_initial_state_is_not_paused() { + let (_env, client, _admin, _treasury) = setup_env(); + let state = client.get_state(); + assert!(!state.paused); +} + +#[test] +fn test_admin_can_pause() { + let (_env, client, admin, _treasury) = setup_env(); + client.pause(&admin); + let state = client.get_state(); + assert!(state.paused); +} + +#[test] +fn test_admin_can_unpause() { + let (_env, client, admin, _treasury) = setup_env(); + client.pause(&admin); + client.unpause(&admin); + let state = client.get_state(); + assert!(!state.paused); +} + +#[test] +fn test_non_admin_cannot_pause() { + let (env, client, _admin, _treasury) = setup_env(); + let stranger = Address::generate(&env); + + let result = client.try_pause(&stranger); + assert_eq!(result, Err(Ok(Error::Unauthorized))); +} + +#[test] +fn test_non_admin_cannot_unpause() { + let (env, client, admin, _treasury) = setup_env(); + let stranger = Address::generate(&env); + + client.pause(&admin); + let result = client.try_unpause(&stranger); + assert_eq!(result, Err(Ok(Error::Unauthorized))); +} + +// ── paused blocks create_token, mint_tokens, set_metadata ──────────────────── + +#[test] +fn test_create_token_blocked_when_paused() { + let (env, client, admin, _treasury) = setup_env(); + client.pause(&admin); + + let creator = Address::generate(&env); + let result = client.try_create_token( + &creator, + &String::from_str(&env, "MyToken"), + &String::from_str(&env, "MTK"), + &7, + &1_000_000, + &1000, + ); + + assert_eq!(result, Err(Ok(Error::ContractPaused))); +} + +#[test] +fn test_mint_tokens_blocked_when_paused() { + let (env, client, admin, _treasury) = setup_env(); + client.pause(&admin); + + let token_address = Address::generate(&env); + let recipient = Address::generate(&env); + + let result = client.try_mint_tokens( + &token_address, + &admin, + &recipient, + &500, + &1000, + ); + + assert_eq!(result, Err(Ok(Error::ContractPaused))); +} + +#[test] +fn test_set_metadata_blocked_when_paused() { + let (env, client, admin, _treasury) = setup_env(); + client.pause(&admin); + + let token_address = Address::generate(&env); + + let result = client.try_set_metadata( + &token_address, + &admin, + &String::from_str(&env, "https://example.com/meta.json"), + &500, + ); + + assert_eq!(result, Err(Ok(Error::ContractPaused))); +} + +// ── unpause restores functionality ─────────────────────────────────────────── + +#[test] +fn test_create_token_works_after_unpause() { + // This test just verifies unpause lifts the block. + // create_token will still fail due to fee transfer in test env, + // but the error should NOT be ContractPaused. + let (env, client, admin, _treasury) = setup_env(); + + client.pause(&admin); + client.unpause(&admin); + + let creator = Address::generate(&env); + let result = client.try_create_token( + &creator, + &String::from_str(&env, "MyToken"), + &String::from_str(&env, "MTK"), + &7, + &1_000_000, + &1000, + ); + + // Should NOT be ContractPaused — any other error is fine here + assert_ne!(result, Err(Ok(Error::ContractPaused))); +} + +// ── burn is NOT blocked by pause ───────────────────────────────────────────── + +#[test] +fn test_burn_not_blocked_when_paused() { + let (env, client, admin, _treasury) = setup_env(); + client.pause(&admin); + + let token_address = Address::generate(&env); + let burner = Address::generate(&env); + + // burn will fail because the token isn't real in this unit test, + // but the error must NOT be ContractPaused + let result = client.try_burn(&token_address, &burner, &100); + assert_ne!(result, Err(Ok(Error::ContractPaused))); +} + +// ── transfer_admin ──────────────────────────────────────────────────────────── + +#[test] +fn test_admin_can_transfer_ownership() { + let (env, client, admin, _treasury) = setup_env(); + let new_admin = Address::generate(&env); + + client.transfer_admin(&admin, &new_admin); + + let state = client.get_state(); + assert_eq!(state.admin, new_admin); +} + +#[test] +fn test_old_admin_loses_privileges_after_transfer() { + let (env, client, admin, _treasury) = setup_env(); + let new_admin = Address::generate(&env); + + client.transfer_admin(&admin, &new_admin); + + // old admin can no longer pause + let result = client.try_pause(&admin); + assert_eq!(result, Err(Ok(Error::Unauthorized))); +} + +#[test] +fn test_non_admin_cannot_transfer_admin() { + let (env, client, _admin, _treasury) = setup_env(); + let stranger = Address::generate(&env); + let new_admin = Address::generate(&env); + + let result = client.try_transfer_admin(&stranger, &new_admin); + assert_eq!(result, Err(Ok(Error::Unauthorized))); +} + +#[test] +fn test_transfer_admin_to_same_address_fails() { + let (_env, client, admin, _treasury) = setup_env(); + + let result = client.try_transfer_admin(&admin, &admin); + assert_eq!(result, Err(Ok(Error::InvalidParameters))); +} + +// ── get_tokens_by_creator ───────────────────────────────────────────────────── + +#[test] +fn test_get_tokens_by_creator_returns_empty_for_unknown_address() { + let (env, client, _admin, _treasury) = setup_env(); + let stranger = Address::generate(&env); + + let indices = client.get_tokens_by_creator(&stranger); + assert_eq!(indices.len(), 0); +} + +#[test] +fn test_get_tokens_by_creator_returns_correct_indices() { + let (env, client, _admin, _treasury) = setup_env(); + let creator = Address::generate(&env); + + // create_token will fail at the fee-transfer step in the test env, + // so we call it twice and verify both indices are tracked. + // We use try_create_token and only care that the creator list is updated + // when the call succeeds. Since fee transfer fails in unit tests we + // verify the storage key logic by checking the empty-vec baseline and + // that a second creator gets an independent empty list. + let creator2 = Address::generate(&env); + + let indices1 = client.get_tokens_by_creator(&creator); + let indices2 = client.get_tokens_by_creator(&creator2); + + // Both unknown creators return empty vecs + assert_eq!(indices1.len(), 0); + assert_eq!(indices2.len(), 0); + + // Confirm they are independent (not the same object) + assert_eq!(indices1, indices2); +} + +#[test] +fn test_get_tokens_by_creator_different_creators_are_independent() { + let (env, client, _admin, _treasury) = setup_env(); + let creator_a = Address::generate(&env); + let creator_b = Address::generate(&env); + + // Neither has tokens — both return empty + assert_eq!(client.get_tokens_by_creator(&creator_a).len(), 0); + assert_eq!(client.get_tokens_by_creator(&creator_b).len(), 0); +}