diff --git a/README.md b/README.md index 6376935..6cadb66 100644 --- a/README.md +++ b/README.md @@ -477,8 +477,8 @@ that matters (e.g. each voter) before its balance changes, (3) read with `balanc | Function | Auth | Parameters | Returns | Description | |---|---|---|---|---| -| `create_vesting_schedule` | Admin | `beneficiary: Address, total_amount: i128, cliff_timestamp: u64, duration_seconds: u64` | — | Create a vesting schedule. | -| `claim_vested_tokens` | Beneficiary | `beneficiary: Address` | — | Claim vested tokens. | +| `create_vesting` | Admin | `beneficiary: Address, total_amount: i128, cliff_timestamp: u64, duration_seconds: u64` | — | Create a vesting schedule. | +| `claim_vested` | Beneficiary | `beneficiary: Address` | — | Claim vested tokens. | | `get_vesting_schedule` | — | `beneficiary: Address` | `Option` | Vesting schedule details. | #### Governance @@ -487,7 +487,7 @@ that matters (e.g. each voter) before its balance changes, (3) read with `balanc |---|---|---|---|---| | `create_proposal` | Admin | `description: String, choices: u32, start_time: u64, end_time: u64, snapshot_ledger: u32` | `u64` | Create a proposal using a past ledger for voting power. | | `vote` | Token Holder | `proposal_id: u64, choice: u32` | — | Cast a vote using the voter's recorded balance at the proposal snapshot. | -| `execute_proposal` | — | `proposal_id: u64` | — | Execute a passed proposal. | +| `execute_proposal` | Admin | `proposal_id: u64` | `u32` | Execute a passed proposal. Returns the winning choice index. | | `get_proposal` | — | `proposal_id: u64` | `Option` | Proposal details. | ### credential-nft diff --git a/contracts/learn-token/src/lib.rs b/contracts/learn-token/src/lib.rs index ed1865a..ec4e487 100644 --- a/contracts/learn-token/src/lib.rs +++ b/contracts/learn-token/src/lib.rs @@ -5,8 +5,8 @@ mod storage; use chainlearn_shared::{BASE_REWARD_PER_POINT, MAX_QUIZ_SCORE}; use soroban_sdk::{ - contract, contracterror, contractimpl, contracttype, Address, BytesN, Env, IntoVal, - String as SorobanString, Symbol, Vec, + contract, contracterror, contractimpl, contracttype, panic_with_error, Address, BytesN, Env, + IntoVal, String as SorobanString, Symbol, Vec, }; // Re-export governance/vesting/admin types so tests can use them. @@ -473,17 +473,17 @@ impl LearnToken { from.require_auth(); if from == to { - return; + panic_with_error!(&env, ContractError::TransferToSelf); } // Prevent transfers to the contract itself, which would lock tokens // irretrievably (#76). if to == env.current_contract_address() { - panic!("cannot transfer to contract"); + panic_with_error!(&env, ContractError::TransferToContract); } if amount < 0 { - panic!("negative amount"); + panic_with_error!(&env, ContractError::NegativeAmount); } if amount == 0 { @@ -492,7 +492,7 @@ impl LearnToken { let from_balance = storage::get_balance(&env, &from); if from_balance < amount { - panic!("insufficient balance"); + panic_with_error!(&env, ContractError::InsufficientBalance); } // Check transfer restrictions (#191) @@ -520,17 +520,17 @@ impl LearnToken { spender.require_auth(); if from == to { - return; + panic_with_error!(&env, ContractError::TransferToSelf); } // Prevent transfers to the contract itself, which would lock tokens // irretrievably (#76). if to == env.current_contract_address() { - panic!("cannot transfer to contract"); + panic_with_error!(&env, ContractError::TransferToContract); } if amount < 0 { - panic!("negative amount"); + panic_with_error!(&env, ContractError::NegativeAmount); } if amount == 0 { @@ -545,12 +545,12 @@ impl LearnToken { let allowance = storage::get_allowance(&env, &from, &spender); if allowance < amount { - panic!("insufficient allowance"); + panic_with_error!(&env, ContractError::InsufficientAllowance); } let from_balance = storage::get_balance(&env, &from); if from_balance < amount { - panic!("insufficient balance"); + panic_with_error!(&env, ContractError::InsufficientBalance); } // Check transfer restrictions (#191) @@ -584,11 +584,11 @@ impl LearnToken { owner.require_auth(); if amount < 0 { - panic!("negative amount"); + panic_with_error!(&env, ContractError::NegativeAmount); } if expiration_ledger <= env.ledger().sequence() { - panic!("expiration_ledger must be in the future"); + panic_with_error!(&env, ContractError::ExpirationInPast); } storage::set_allowance(&env, &owner, &spender, amount, expiration_ledger); @@ -624,12 +624,12 @@ impl LearnToken { from.require_auth(); if amount < 0 { - panic!("negative amount"); + panic_with_error!(&env, ContractError::NegativeAmount); } let from_balance = storage::get_balance(&env, &from); if from_balance < amount { - panic!("insufficient balance"); + panic_with_error!(&env, ContractError::InsufficientBalance); } storage::set_balance(&env, &from, from_balance - amount); @@ -661,7 +661,7 @@ impl LearnToken { spender.require_auth(); if amount < 0 { - panic!("negative amount"); + panic_with_error!(&env, ContractError::NegativeAmount); } // Surface an expired allowance the same way transfer_from does, so @@ -674,12 +674,12 @@ impl LearnToken { let allowance = storage::get_allowance(&env, &from, &spender); if allowance < amount { - panic!("insufficient allowance"); + panic_with_error!(&env, ContractError::InsufficientAllowance); } let from_balance = storage::get_balance(&env, &from); if from_balance < amount { - panic!("insufficient balance"); + panic_with_error!(&env, ContractError::InsufficientBalance); } storage::reduce_allowance(&env, &from, &spender, amount); @@ -702,7 +702,7 @@ impl LearnToken { Self::require_not_paused(&env); caller.require_auth(); if !storage::has_role(&env, &caller, &storage::AdminRole::Minter) { - panic!("not authorized"); + panic_with_error!(&env, ContractError::NotAuthorized); } let zero_address = chainlearn_shared::zero_address(&env); @@ -711,7 +711,7 @@ impl LearnToken { } if amount < 0 { - panic!("negative amount"); + panic_with_error!(&env, ContractError::NegativeAmount); } if amount == 0 { @@ -755,7 +755,7 @@ impl LearnToken { learner.require_auth(); if storage::is_reward_claimed(&env, &learner, &course_id, &quiz_id) { - panic!("reward already claimed"); + panic_with_error!(&env, ContractError::RewardAlreadyClaimed); } // Verify score by querying the progress-tracker contract via a single @@ -775,7 +775,7 @@ impl LearnToken { } if score > MAX_QUIZ_SCORE { - panic!("score exceeds maximum"); + panic_with_error!(&env, ContractError::InvalidScore); } let reward_amount = (score as i128) * BASE_REWARD_PER_POINT; @@ -993,7 +993,7 @@ impl LearnToken { pub fn grant_role(env: Env, caller: Address, address: Address, role: storage::AdminRole) { caller.require_auth(); if !storage::has_role(&env, &caller, &storage::AdminRole::Admin) { - panic!("not authorized"); + panic_with_error!(&env, ContractError::NotAuthorized); } storage::add_admin(&env, &address, &role); events::role_granted(&env, &address, &role); @@ -1003,7 +1003,7 @@ impl LearnToken { pub fn revoke_role(env: Env, caller: Address, address: Address, role: storage::AdminRole) { caller.require_auth(); if !storage::has_role(&env, &caller, &storage::AdminRole::Admin) { - panic!("not authorized"); + panic_with_error!(&env, ContractError::NotAuthorized); } storage::remove_admin(&env, &address, &role); events::role_revoked(&env, &address, &role); @@ -1015,7 +1015,7 @@ impl LearnToken { pub fn add_admin(env: Env, caller: Address, admin_info: AdminInfo) { caller.require_auth(); if !storage::has_role(&env, &caller, &storage::AdminRole::Admin) { - panic!("not authorized"); + panic_with_error!(&env, ContractError::NotAuthorized); } storage::add_admin(&env, &admin_info.address, &admin_info.role); @@ -1028,7 +1028,7 @@ impl LearnToken { pub fn remove_admin(env: Env, caller: Address, admin_info: AdminInfo) { caller.require_auth(); if !storage::has_role(&env, &caller, &storage::AdminRole::Admin) { - panic!("not authorized"); + panic_with_error!(&env, ContractError::NotAuthorized); } storage::remove_admin(&env, &admin_info.address, &admin_info.role); @@ -1052,7 +1052,7 @@ impl LearnToken { if !storage::has_role(&env, &caller, &storage::AdminRole::Admin) || !storage::has_role(&env, &co_signer, &storage::AdminRole::Admin) { - panic!("not authorized"); + panic_with_error!(&env, ContractError::NotAuthorized); } env.events().publish( @@ -1078,7 +1078,7 @@ impl LearnToken { if !storage::has_role(&env, &caller, &storage::AdminRole::Admin) || !storage::has_role(&env, &co_signer, &storage::AdminRole::Admin) { - panic!("not authorized"); + panic_with_error!(&env, ContractError::NotAuthorized); } env.deployer() @@ -1116,7 +1116,7 @@ impl LearnToken { pub fn pause(env: Env, caller: Address) { caller.require_auth(); if !storage::has_role(&env, &caller, &storage::AdminRole::Pauser) { - panic!("not authorized"); + panic_with_error!(&env, ContractError::NotAuthorized); } if storage::is_paused(&env) { @@ -1131,7 +1131,7 @@ impl LearnToken { pub fn unpause(env: Env, caller: Address) { caller.require_auth(); if !storage::has_role(&env, &caller, &storage::AdminRole::Pauser) { - panic!("not authorized"); + panic_with_error!(&env, ContractError::NotAuthorized); } if !storage::is_paused(&env) { @@ -1413,11 +1413,11 @@ impl LearnToken { owner.require_auth(); if additional_amount < 0 { - panic!("negative amount"); + panic_with_error!(&env, ContractError::NegativeAmount); } if expiration_ledger <= env.ledger().sequence() { - panic!("expiration_ledger must be in the future"); + panic_with_error!(&env, ContractError::ExpirationInPast); } let current = storage::get_allowance(&env, &owner, &spender); @@ -1521,7 +1521,7 @@ impl LearnToken { owner.require_auth(); if decrease_amount < 0 { - panic!("negative amount"); + panic_with_error!(&env, ContractError::NegativeAmount); } let current = storage::get_allowance(&env, &owner, &spender); @@ -1572,11 +1572,11 @@ impl LearnToken { owner.require_auth(); if amount < 0 { - panic!("negative amount"); + panic_with_error!(&env, ContractError::NegativeAmount); } if expiration_ledger <= env.ledger().sequence() { - panic!("expiration_ledger must be in the future"); + panic_with_error!(&env, ContractError::ExpirationInPast); } let current_nonce = storage::get_permit_nonce(&env, &owner); @@ -2595,7 +2595,7 @@ mod tests { } #[test] - #[should_panic(expected = "reward already claimed")] + #[should_panic(expected = "Error(Contract, #12)")] fn test_claim_reward_prevents_double_claim() { let env = Env::default(); let (_, lt_contract_id, pt_contract_id) = setup(&env); @@ -2708,7 +2708,7 @@ mod tests { // ── #76: transfer to contract address ─────────────────────────────────── #[test] - #[should_panic(expected = "cannot transfer to contract")] + #[should_panic(expected = "Error(Contract, #9)")] fn test_transfer_to_contract_address_panics() { let env = Env::default(); let (admin, lt_contract_id, _) = setup(&env); @@ -2723,7 +2723,7 @@ mod tests { } #[test] - #[should_panic(expected = "cannot transfer to contract")] + #[should_panic(expected = "Error(Contract, #9)")] fn test_transfer_from_to_contract_address_panics() { let env = Env::default(); let (admin, lt_contract_id, _) = setup(&env); @@ -2740,6 +2740,20 @@ mod tests { client.transfer_from(&spender, &owner, <_contract_id, &200); } + #[test] + #[should_panic(expected = "Error(Contract, #8)")] + fn test_transfer_to_self_is_rejected() { + let env = Env::default(); + let (admin, lt_contract_id, _) = setup(&env); + let client = LearnTokenClient::new(&env, <_contract_id); + + let alice = Address::generate(&env); + env.mock_all_auths(); + + client.mint(&admin, &alice, &500); + client.transfer(&alice, &alice, &100); + } + // ── #77: increase_allowance / decrease_allowance ───────────────────────── #[test] @@ -2858,7 +2872,7 @@ mod tests { } #[test] - #[should_panic(expected = "insufficient balance")] + #[should_panic(expected = "Error(Contract, #5)")] fn test_burn_more_than_balance_panics() { let env = Env::default(); let (admin, lt_contract_id, _) = setup(&env); @@ -2872,7 +2886,7 @@ mod tests { } #[test] - #[should_panic(expected = "negative amount")] + #[should_panic(expected = "Error(Contract, #4)")] fn test_burn_negative_amount_panics() { let env = Env::default(); let (admin, lt_contract_id, _) = setup(&env); @@ -2922,7 +2936,7 @@ mod tests { } #[test] - #[should_panic(expected = "insufficient allowance")] + #[should_panic(expected = "Error(Contract, #6)")] fn test_burn_from_beyond_allowance_panics() { let env = Env::default(); let (admin, lt_contract_id, _) = setup(&env); @@ -2939,7 +2953,7 @@ mod tests { } #[test] - #[should_panic(expected = "insufficient balance")] + #[should_panic(expected = "Error(Contract, #5)")] fn test_burn_from_beyond_balance_panics() { let env = Env::default(); let (admin, lt_contract_id, _) = setup(&env); @@ -2957,7 +2971,7 @@ mod tests { } #[test] - #[should_panic(expected = "insufficient allowance")] + #[should_panic(expected = "Error(Contract, #6)")] fn test_burn_from_without_allowance_panics() { let env = Env::default(); let (admin, lt_contract_id, _) = setup(&env); diff --git a/docs/security.md b/docs/security.md index ce69175..ab35d89 100644 --- a/docs/security.md +++ b/docs/security.md @@ -165,7 +165,7 @@ if new_max_supply > max_allowed { **Threat**: Attacker gains control of admin key. **Mitigation**: -- **Delayed admin transfer**: 24-hour delay before new admin can accept +- **Delayed admin transfer**: 48-hour delay (172,800 seconds) before new admin can accept - **Cancel window**: Current admin can cancel pending transfer - **Multi-sig for critical ops**: Upgrades require two admin signatures - **Emergency pause**: Can freeze all operations diff --git a/examples/cli_usage_examples.sh b/examples/cli_usage_examples.sh index 2759eb8..b4d3a92 100755 --- a/examples/cli_usage_examples.sh +++ b/examples/cli_usage_examples.sh @@ -1,7 +1,9 @@ #!/bin/bash # Example Soroban CLI commands for ChainLearn +# +# Set STELLAR_SECRET_KEY in the environment before running this script. +# This file must not export a placeholder; that would overwrite a real key. -export STELLAR_SECRET_KEY="S..." PROGRESS_ID="C..." TOKEN_ID="C..." CREDENTIAL_ID="C..." diff --git a/examples/javascript_client.js b/examples/javascript_client.js index 8d30d17..12c0879 100644 --- a/examples/javascript_client.js +++ b/examples/javascript_client.js @@ -11,7 +11,8 @@ * * Environment variables: * STELLAR_RPC_URL - Soroban RPC endpoint (default: testnet) - * STELLAR_SECRET_KEY - Admin/learner secret key + * STELLAR_SECRET_KEY - Admin secret key + * LEARNER_SECRET_KEY - Learner secret key (must be different from the admin key) * PROGRESS_TRACKER_ID - Deployed progress-tracker contract ID * LEARN_TOKEN_ID - Deployed learn-token contract ID * CREDENTIAL_NFT_ID - Deployed credential-nft contract ID @@ -222,12 +223,18 @@ async function mintCredential(adminKeypair, learnerKeypair) { // ── Main Flow ───────────────────────────────────────────────────────── async function main() { - const adminKeypair = StellarSdk.Keypair.fromSecret( - process.env.STELLAR_SECRET_KEY - ); - const learnerKeypair = StellarSdk.Keypair.fromSecret( - process.env.STELLAR_SECRET_KEY // In production, use a different key - ); + const adminSecret = process.env.STELLAR_SECRET_KEY; + const learnerSecret = process.env.LEARNER_SECRET_KEY; + if (!adminSecret || !learnerSecret) { + throw new Error( + "Set STELLAR_SECRET_KEY and LEARNER_SECRET_KEY before running this example" + ); + } + if (adminSecret === learnerSecret) { + throw new Error("Admin and learner must use separate secret keys"); + } + const adminKeypair = StellarSdk.Keypair.fromSecret(adminSecret); + const learnerKeypair = StellarSdk.Keypair.fromSecret(learnerSecret); console.log("=== ChainLearn Full Flow ===\n"); diff --git a/examples/javascript_client_example.js b/examples/javascript_client_example.js index afe418e..f5af615 100644 --- a/examples/javascript_client_example.js +++ b/examples/javascript_client_example.js @@ -1,14 +1,14 @@ /** * Example JavaScript client to interact with ChainLearn contracts using Stellar SDK. */ -import { Keypair, SorobanRpc, Contract, Networks, TransactionBuilder, BASE_FEE, xdr } from 'stellar-sdk'; +import { Keypair, rpc, Contract, Networks, TransactionBuilder, BASE_FEE, xdr } from '@stellar/stellar-sdk'; const RPC_URL = 'https://soroban-testnet.stellar.org:443'; const NETWORK_PASSPHRASE = Networks.TESTNET; const PROGRESS_TRACKER_ID = 'C...'; // Replace with actual contract ID export async function enrollLearner(secretKey, courseId) { - const server = new SorobanRpc.Server(RPC_URL); + const server = new rpc.Server(RPC_URL); const keypair = Keypair.fromSecret(secretKey); const contract = new Contract(PROGRESS_TRACKER_ID); diff --git a/examples/rust_client.rs b/examples/rust_client.rs index 21ae803..5bc6f8c 100644 --- a/examples/rust_client.rs +++ b/examples/rust_client.rs @@ -14,19 +14,35 @@ //! Run with: //! cargo run --example rust_client -use soroban_sdk::{testutils::Address as _, Address, Env, Symbol}; +use soroban_sdk::{Address, Env, String as SorobanString, Symbol}; fn main() { let env = Env::default(); - let _admin = Address::generate(&env); - let _learner = Address::generate(&env); - - let _progress_tracker = Address::generate(&env); - let _learn_token = Address::generate(&env); - let _credential_nft = Address::generate(&env); + // Address::from_string is the soroban_sdk constructor. Address::from_str does not exist. + let _admin = Address::from_string(&SorobanString::from_str( + &env, + "GABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABVCX", + )); + let _learner = Address::from_string(&SorobanString::from_str( + &env, + "GAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHV4", + )); + + let _progress_tracker = Address::from_string(&SorobanString::from_str( + &env, + "CAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABDQF", + )); + let _learn_token = Address::from_string(&SorobanString::from_str( + &env, + "CABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARHO", + )); + let _credential_nft = Address::from_string(&SorobanString::from_str( + &env, + "CABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVX", + )); // ── Admin: Create a course ───────────────────────────────────────── - let course_id = Symbol::new(&env, "rust_101"); + let _course_id = Symbol::new(&env, "rust_101"); let mut module_ids = soroban_sdk::Vec::new(&env); module_ids.push_back(Symbol::new(&env, "mod_basics")); module_ids.push_back(Symbol::new(&env, "mod_ownership")); diff --git a/examples/rust_client_example.rs b/examples/rust_client_example.rs index 1dc3b63..cd4b5c2 100644 --- a/examples/rust_client_example.rs +++ b/examples/rust_client_example.rs @@ -1,6 +1,5 @@ //! Example Rust client using the soroban-sdk to interact with the ChainLearn progress-tracker contract. -#![no_std] -use soroban_sdk::{contract, contractimpl, Address, Env, Symbol, IntoVal}; +use soroban_sdk::{contract, contractimpl, Address, Env, IntoVal, Symbol}; #[contract] pub struct ChainLearnClient; @@ -11,7 +10,7 @@ impl ChainLearnClient { pub fn enroll_learner(env: Env, tracker_id: Address, learner: Address, course_id: Symbol) { // Authorize the learner learner.require_auth(); - + // Invoke the enroll function on the progress-tracker contract let args = soroban_sdk::vec![&env, learner.into_val(&env), course_id.into_val(&env)]; env.invoke_contract::<()>(&tracker_id, &Symbol::new(&env, "enroll"), args); @@ -19,4 +18,3 @@ impl ChainLearnClient { } fn main() {} - diff --git a/tests/integration/security_double_spending_tests.rs b/tests/integration/security_double_spending_tests.rs index be94080..b2d62de 100644 --- a/tests/integration/security_double_spending_tests.rs +++ b/tests/integration/security_double_spending_tests.rs @@ -61,7 +61,7 @@ fn create_course_and_complete( // ── Double-claim reward ───────────────────────────────────────────────────── #[test] -#[should_panic(expected = "reward already claimed")] +#[should_panic(expected = "Error(Contract, #12)")] fn test_double_claim_reward_is_prevented() { let env = Env::default(); let (_admin, token_client, _credential_client, progress_client) = setup_env(&env); diff --git a/tests/integration/token_flow.rs b/tests/integration/token_flow.rs index 21ec8a3..58a59c8 100644 --- a/tests/integration/token_flow.rs +++ b/tests/integration/token_flow.rs @@ -60,7 +60,7 @@ fn test_end_to_end_reward_flow() { } #[test] -#[should_panic(expected = "reward already claimed")] +#[should_panic(expected = "Error(Contract, #12)")] fn test_double_claim_prevented() { let setup = fixtures::setup_chainlearn_env(); let env = &setup.env; diff --git a/tests/unit/error_message_tests.rs b/tests/unit/error_message_tests.rs index 2e77bc7..c07fb6c 100644 --- a/tests/unit/error_message_tests.rs +++ b/tests/unit/error_message_tests.rs @@ -280,7 +280,7 @@ mod error_message_tests { // ── Learn Token Error Message Tests ─────────────────────────────────── #[test] - #[should_panic(expected = "insufficient balance")] + #[should_panic(expected = "Error(Contract, #5)")] fn test_learn_token_insufficient_balance_error_message() { let env = Env::default(); let (_admin, contract_id, _) = setup_token(&env); @@ -294,7 +294,7 @@ mod error_message_tests { } #[test] - #[should_panic(expected = "negative amount")] + #[should_panic(expected = "Error(Contract, #4)")] fn test_learn_token_negative_amount_error_message() { let env = Env::default(); let (_admin, contract_id, _) = setup_token(&env); @@ -308,7 +308,7 @@ mod error_message_tests { } #[test] - #[should_panic(expected = "cannot transfer to contract")] + #[should_panic(expected = "Error(Contract, #9)")] fn test_learn_token_transfer_to_contract_error_message() { let env = Env::default(); let (admin, contract_id, _) = setup_token(&env); @@ -318,11 +318,11 @@ mod error_message_tests { env.mock_all_auths(); client.mint(&admin, &alice, &1000); - client.transfer(&alice, &env.current_contract_address(), &100); + client.transfer(&alice, &contract_id, &100); } #[test] - #[should_panic(expected = "insufficient allowance")] + #[should_panic(expected = "Error(Contract, #6)")] fn test_learn_token_insufficient_allowance_error_message() { let env = Env::default(); let (_admin, contract_id, _) = setup_token(&env); @@ -338,7 +338,7 @@ mod error_message_tests { } #[test] - #[should_panic(expected = "reward already claimed")] + #[should_panic(expected = "Error(Contract, #12)")] fn test_learn_token_reward_already_claimed_error_message() { let env = Env::default(); let (_admin, contract_id, pt_contract_id) = setup_token(&env); @@ -399,7 +399,7 @@ mod error_message_tests { } #[test] - #[should_panic(expected = "not authorized")] + #[should_panic(expected = "Error(Contract, #13)")] fn test_learn_token_not_authorized_error_message() { let env = Env::default(); let (_admin, contract_id, _) = setup_token(&env); @@ -407,8 +407,9 @@ mod error_message_tests { let non_admin = Address::generate(&env); let recipient = Address::generate(&env); - - // Don't mock auths, so mint should fail + env.mock_all_auths(); + + // Authorization succeeds, but this address does not hold the minter role. client.mint(&non_admin, &recipient, &1000); } @@ -455,7 +456,7 @@ mod error_message_tests { } #[test] - #[should_panic(expected = "expiration_ledger must be in the future")] + #[should_panic(expected = "Error(Contract, #10)")] fn test_learn_token_past_expiration_error_message() { let env = Env::default(); let (_admin, contract_id, _) = setup_token(&env); @@ -464,6 +465,7 @@ mod error_message_tests { let owner = Address::generate(&env); let spender = Address::generate(&env); env.mock_all_auths(); + env.ledger().with_mut(|li| li.sequence_number = 10); let past_ledger = env.ledger().sequence() - 1; client.approve(&owner, &spender, &500, &past_ledger); diff --git a/tests/unit/security_arithmetic_tests.rs b/tests/unit/security_arithmetic_tests.rs index 22c79c3..1906e78 100644 --- a/tests/unit/security_arithmetic_tests.rs +++ b/tests/unit/security_arithmetic_tests.rs @@ -5,7 +5,7 @@ use progress_tracker::ProgressTracker; use soroban_sdk::{testutils::Address as _, Address, Env, String as SorobanString}; #[test] -#[should_panic(expected = "insufficient balance")] +#[should_panic(expected = "Error(Contract, #5)")] fn test_underflow_balance_subtraction() { let env = Env::default(); let admin = Address::generate(&env); @@ -29,7 +29,7 @@ fn test_underflow_balance_subtraction() { } #[test] -#[should_panic(expected = "insufficient balance")] +#[should_panic(expected = "Error(Contract, #5)")] fn test_underflow_balance_burn() { let env = Env::default(); let admin = Address::generate(&env); @@ -79,7 +79,7 @@ fn test_overflow_supply() { } #[test] -#[should_panic(expected = "insufficient allowance")] +#[should_panic(expected = "Error(Contract, #6)")] fn test_underflow_allowance_spend() { let env = Env::default(); let admin = Address::generate(&env); diff --git a/tests/unit/token_tests.rs b/tests/unit/token_tests.rs index 67433ba..dda9cd5 100644 --- a/tests/unit/token_tests.rs +++ b/tests/unit/token_tests.rs @@ -96,7 +96,7 @@ mod token_unit_tests { } #[test] - #[should_panic(expected = "insufficient balance")] + #[should_panic(expected = "Error(Contract, #5)")] fn test_transfer_insufficient_balance() { let env = Env::default(); let (admin, contract_id, _) = setup_token(&env); @@ -132,7 +132,7 @@ mod token_unit_tests { } #[test] - #[should_panic(expected = "reward already claimed")] + #[should_panic(expected = "Error(Contract, #12)")] fn test_claim_reward_double_claim() { let env = Env::default(); let (_admin, contract_id, pt_contract_id) = setup_token(&env); @@ -187,7 +187,7 @@ mod token_unit_tests { } #[test] - #[should_panic(expected = "insufficient allowance")] + #[should_panic(expected = "Error(Contract, #6)")] fn test_transfer_from_insufficient_allowance() { let env = Env::default(); let (admin, contract_id, _) = setup_token(&env); @@ -204,7 +204,7 @@ mod token_unit_tests { } #[test] - #[should_panic(expected = "insufficient allowance")] + #[should_panic(expected = "Error(Contract, #6)")] fn test_transfer_from_expired_allowance() { let env = Env::default(); let (admin, contract_id, _) = setup_token(&env); @@ -724,7 +724,7 @@ mod token_unit_tests { } #[test] - #[should_panic(expected = "insufficient balance")] + #[should_panic(expected = "Error(Contract, #5)")] fn test_security_transfer_balance_underflow_reverts() { let env = Env::default(); let (_admin, contract_id, _) = setup_token(&env); @@ -767,7 +767,7 @@ mod token_unit_tests { } #[test] - #[should_panic(expected = "insufficient balance")] + #[should_panic(expected = "Error(Contract, #5)")] fn test_security_burn_balance_underflow_reverts() { let env = Env::default(); let (_admin, contract_id, _) = setup_token(&env); @@ -891,8 +891,6 @@ mod token_unit_tests { #[test] #[should_panic(expected = "no snapshot available at specified ledger")] fn test_vote_rejects_missing_snapshot_instead_of_using_current_balance() { - #[should_panic(expected = "contract is paused")] - fn test_create_proposal_fails_while_paused() { let env = Env::default(); let (admin, contract_id, _) = setup_token(&env); let client = LearnTokenClient::new(&env, &contract_id); @@ -928,6 +926,16 @@ mod token_unit_tests { &0, &1_000, &100, + ); + } + + #[test] + #[should_panic(expected = "contract is paused")] + fn test_create_proposal_fails_while_paused() { + let env = Env::default(); + let (admin, contract_id, _) = setup_token(&env); + let client = LearnTokenClient::new(&env, &contract_id); + env.mock_all_auths(); client.pause(&admin); client.create_proposal( &SorobanString::from_str(&env, "Paused proposal"), @@ -939,7 +947,6 @@ mod token_unit_tests { } #[test] - fn test_vesting_schedule_cliff_linear_vesting_and_claiming() { #[should_panic(expected = "contract is paused")] fn test_vote_fails_while_paused() { let env = Env::default();