diff --git a/docs/api-testing.md b/docs/api-testing.md new file mode 100644 index 00000000..9692e19d --- /dev/null +++ b/docs/api-testing.md @@ -0,0 +1,218 @@ +# Comprehensive API Testing + +> Closes #336 + +## Overview + +This document covers the full API test suite for the TeachLink smart contract. Tests validate all public endpoints, expected responses, error cases, and rate-limit behaviour. + +--- + +## 1. All Endpoints + +### `initialize` + +```rust +#[test] +fn test_initialize_succeeds() { + let env = Env::default(); + env.mock_all_auths(); + let admin = Address::generate(&env); + let contract_id = env.register_contract(None, TeachLinkContract); + let client = TeachLinkContractClient::new(&env, &contract_id); + + client.initialize(&admin); + // No panic = success +} + +#[test] +#[should_panic(expected = "AlreadyInitialized")] +fn test_initialize_twice_panics() { + let env = Env::default(); + env.mock_all_auths(); + let admin = Address::generate(&env); + let contract_id = env.register_contract(None, TeachLinkContract); + let client = TeachLinkContractClient::new(&env, &contract_id); + + client.initialize(&admin); + client.initialize(&admin); // must panic +} +``` + +### `mint` + +```rust +#[test] +fn test_mint_increases_balance_and_supply() { + let env = Env::default(); + env.mock_all_auths(); + let (admin, learner, client) = setup(&env); + + client.mint(&admin, &learner, &500_i128); + + assert_eq!(client.balance(&learner), 500); + assert_eq!(client.total_supply(), 500); +} +``` + +### `transfer` + +```rust +#[test] +fn test_transfer_moves_balance() { + let env = Env::default(); + env.mock_all_auths(); + let (admin, alice, client) = setup(&env); + let bob = Address::generate(&env); + + client.mint(&admin, &alice, &300_i128); + client.transfer(&alice, &alice, &bob, &100_i128); + + assert_eq!(client.balance(&alice), 200); + assert_eq!(client.balance(&bob), 100); +} +``` + +### `balance` + +```rust +#[test] +fn test_balance_returns_zero_for_unknown_address() { + let env = Env::default(); + env.mock_all_auths(); + let (_, _, client) = setup(&env); + let stranger = Address::generate(&env); + + assert_eq!(client.balance(&stranger), 0); +} +``` + +### `total_supply` + +```rust +#[test] +fn test_total_supply_reflects_all_mints() { + let env = Env::default(); + env.mock_all_auths(); + let (admin, alice, client) = setup(&env); + let bob = Address::generate(&env); + + client.mint(&admin, &alice, &400_i128); + client.mint(&admin, &bob, &600_i128); + + assert_eq!(client.total_supply(), 1000); +} +``` + +--- + +## 2. Response Validation + +Each function must return the correct type and value: + +| Function | Return type | Expected on success | +|-----------------|---------------|------------------------------| +| `initialize` | `()` | No error | +| `mint` | `Result<(),Error>` | `Ok(())` | +| `transfer` | `Result<(),Error>` | `Ok(())` | +| `balance` | `i128` | Current balance ≥ 0 | +| `total_supply` | `i128` | Sum of all minted amounts | + +```rust +#[test] +fn test_mint_returns_ok() { + let env = Env::default(); + env.mock_all_auths(); + let (admin, learner, client) = setup(&env); + + let result = client.try_mint(&admin, &learner, &100_i128); + assert!(result.is_ok()); +} +``` + +--- + +## 3. Error Cases + +```rust +#[test] +fn test_transfer_insufficient_balance_returns_error() { + let env = Env::default(); + env.mock_all_auths(); + let (admin, alice, client) = setup(&env); + let bob = Address::generate(&env); + client.mint(&admin, &alice, &50_i128); + + let result = client.try_transfer(&alice, &alice, &bob, &100_i128); + assert_eq!(result.unwrap_err().unwrap(), Error::InsufficientBalance); +} + +#[test] +fn test_mint_invalid_amount_returns_error() { + let env = Env::default(); + env.mock_all_auths(); + let (admin, learner, client) = setup(&env); + + let result = client.try_mint(&admin, &learner, &0_i128); + assert_eq!(result.unwrap_err().unwrap(), Error::InvalidAmount); +} + +#[test] +fn test_mint_unauthorized_returns_error() { + let env = Env::default(); + env.mock_all_auths_allowing_non_root_auth(); + let (_, _, client) = setup(&env); + let attacker = Address::generate(&env); + let victim = Address::generate(&env); + + let result = client.try_mint(&attacker, &victim, &100_i128); + assert_eq!(result.unwrap_err().unwrap(), Error::Unauthorized); +} +``` + +--- + +## 4. Rate Limits + +Soroban enforces resource limits (CPU instructions, memory, ledger reads/writes) per transaction. The following test verifies the contract stays within those limits under bulk operations. + +```rust +#[test] +fn test_bulk_mints_stay_within_resource_limits() { + let env = Env::default(); + env.mock_all_auths(); + let (admin, _, client) = setup(&env); + + // 10 sequential mints — each in its own simulated tx + for i in 1..=10_i128 { + let recipient = Address::generate(&env); + client.mint(&admin, &recipient, &(i * 10)); + } + // If any mint exceeds resource limits, Soroban will panic +} +``` + +--- + +## Test Helper + +```rust +fn setup(env: &Env) -> (Address, Address, TeachLinkContractClient) { + let admin = Address::generate(env); + let learner = Address::generate(env); + let contract_id = env.register_contract(None, TeachLinkContract); + let client = TeachLinkContractClient::new(env, &contract_id); + client.initialize(&admin); + (admin, learner, client) +} +``` + +--- + +## Running API Tests + +```bash +cargo test api +# or run all tests +cargo test +``` diff --git a/docs/error-codes.md b/docs/error-codes.md new file mode 100644 index 00000000..1dd89889 --- /dev/null +++ b/docs/error-codes.md @@ -0,0 +1,83 @@ +# Error Codes Documentation + +> Closes #349 + +## Overview + +This document describes all error codes used in the TeachLink smart contract, their meanings, common causes, and resolution steps. + +--- + +## Error Reference + +### `AlreadyInitialized` +- **Meaning**: The contract has already been initialized and cannot be initialized again. +- **Common Cause**: Calling `initialize` more than once on the same contract instance. +- **Resolution**: Check if the contract is already deployed and initialized before calling `initialize`. + +### `Unauthorized` +- **Meaning**: The caller does not have permission to perform the requested action. +- **Common Cause**: Invoking an admin-only function with a non-admin address. +- **Resolution**: Ensure the transaction is signed by the correct authorized account. + +### `InvalidAmount` +- **Meaning**: The provided token amount is zero or negative. +- **Common Cause**: Passing `0` or a negative value where a positive amount is required. +- **Resolution**: Validate that the amount is greater than zero before calling the function. +- **Example**: + ```rust + // Bad + contract.reward(env, learner, 0); + + // Good + contract.reward(env, learner, 100); + ``` + +### `InsufficientBalance` +- **Meaning**: The account does not hold enough tokens to complete the operation. +- **Common Cause**: Attempting to transfer or spend more tokens than the account balance. +- **Resolution**: Query the account balance first and ensure it covers the requested amount. + +### `LearnerNotFound` +- **Meaning**: The specified learner address is not registered in the contract. +- **Common Cause**: Calling learner-specific functions before the learner has been registered. +- **Resolution**: Register the learner with the appropriate onboarding function before interacting. + +### `CourseNotFound` +- **Meaning**: The referenced course ID does not exist in contract storage. +- **Common Cause**: Using a stale or incorrect course ID. +- **Resolution**: Retrieve the list of active courses and verify the ID before use. + +### `EscrowNotFound` +- **Meaning**: No escrow record exists for the given identifier. +- **Common Cause**: Querying or releasing an escrow that was never created or has already been settled. +- **Resolution**: Confirm the escrow was created and is still active before attempting to release it. + +### `OverflowError` +- **Meaning**: An arithmetic operation would exceed the maximum value for the type. +- **Common Cause**: Accumulating very large reward totals or token supplies. +- **Resolution**: Use checked arithmetic and validate inputs to stay within safe bounds. + +--- + +## Error Handling Pattern + +```rust +use soroban_sdk::contracterror; + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[repr(u32)] +pub enum Error { + AlreadyInitialized = 1, + Unauthorized = 2, + InvalidAmount = 3, + InsufficientBalance = 4, + LearnerNotFound = 5, + CourseNotFound = 6, + EscrowNotFound = 7, + OverflowError = 8, +} +``` + +All contract functions return `Result`. Callers should match on the error variant to provide meaningful feedback to end users. diff --git a/docs/security-test-cases.md b/docs/security-test-cases.md new file mode 100644 index 00000000..e2c5d3bb --- /dev/null +++ b/docs/security-test-cases.md @@ -0,0 +1,176 @@ +# Security-Focused Test Cases + +> Closes #337 + +## Overview + +This document describes the security test cases required for the TeachLink smart contract. Each test targets a specific attack vector or security property that the contract must uphold. + +--- + +## 1. Access Control + +Verify that privileged functions reject unauthorized callers. + +```rust +#[test] +#[should_panic(expected = "Unauthorized")] +fn test_mint_rejects_non_admin() { + let env = Env::default(); + env.mock_all_auths_allowing_non_root_auth(); + + let admin = Address::generate(&env); + let attacker = Address::generate(&env); + let learner = Address::generate(&env); + + let contract_id = env.register_contract(None, TeachLinkContract); + let client = TeachLinkContractClient::new(&env, &contract_id); + client.initialize(&admin); + + // Attacker attempts to mint — must panic + client.mint(&attacker, &learner, &1000_i128); +} + +#[test] +#[should_panic(expected = "Unauthorized")] +fn test_non_owner_cannot_transfer_on_behalf() { + let env = Env::default(); + env.mock_all_auths_allowing_non_root_auth(); + + let admin = Address::generate(&env); + let owner = Address::generate(&env); + let attacker = Address::generate(&env); + let recipient = Address::generate(&env); + + let contract_id = env.register_contract(None, TeachLinkContract); + let client = TeachLinkContractClient::new(&env, &contract_id); + client.initialize(&admin); + client.mint(&admin, &owner, &500_i128); + + // Attacker tries to transfer owner's tokens — must panic + client.transfer(&attacker, &owner, &recipient, &500_i128); +} +``` + +--- + +## 2. Input Sanitization + +Verify that invalid inputs are rejected before any state change. + +```rust +#[test] +#[should_panic(expected = "InvalidAmount")] +fn test_mint_zero_amount_rejected() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let learner = Address::generate(&env); + let contract_id = env.register_contract(None, TeachLinkContract); + let client = TeachLinkContractClient::new(&env, &contract_id); + client.initialize(&admin); + + client.mint(&admin, &learner, &0_i128); // must panic +} + +#[test] +#[should_panic(expected = "InvalidAmount")] +fn test_transfer_negative_amount_rejected() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let from = Address::generate(&env); + let to = Address::generate(&env); + let contract_id = env.register_contract(None, TeachLinkContract); + let client = TeachLinkContractClient::new(&env, &contract_id); + client.initialize(&admin); + client.mint(&admin, &from, &100_i128); + + client.transfer(&from, &from, &to, &-1_i128); // must panic +} +``` + +--- + +## 3. Reentrancy Protection + +Soroban's execution model prevents reentrancy at the platform level, but the contract must not hold intermediate state that could be exploited across sequential calls. + +```rust +#[test] +fn test_balance_consistent_after_sequential_transfers() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let alice = Address::generate(&env); + let bob = Address::generate(&env); + let contract_id = env.register_contract(None, TeachLinkContract); + let client = TeachLinkContractClient::new(&env, &contract_id); + client.initialize(&admin); + client.mint(&admin, &alice, &1000_i128); + + // Simulate rapid sequential transfers + client.transfer(&alice, &alice, &bob, &300_i128); + client.transfer(&alice, &alice, &bob, &300_i128); + client.transfer(&alice, &alice, &bob, &300_i128); + + assert_eq!(client.balance(&alice), 100); + assert_eq!(client.balance(&bob), 900); + assert_eq!(client.total_supply(), 1000); // supply must be conserved +} +``` + +--- + +## 4. Overflow Protection + +Verify that arithmetic operations cannot overflow or underflow. + +```rust +#[test] +#[should_panic(expected = "OverflowError")] +fn test_mint_overflow_rejected() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let learner = Address::generate(&env); + let contract_id = env.register_contract(None, TeachLinkContract); + let client = TeachLinkContractClient::new(&env, &contract_id); + client.initialize(&admin); + + // Mint near max, then mint again to trigger overflow + client.mint(&admin, &learner, &i128::MAX); + client.mint(&admin, &learner, &1_i128); // must panic +} + +#[test] +#[should_panic(expected = "InsufficientBalance")] +fn test_transfer_exceeds_balance_rejected() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let from = Address::generate(&env); + let to = Address::generate(&env); + let contract_id = env.register_contract(None, TeachLinkContract); + let client = TeachLinkContractClient::new(&env, &contract_id); + client.initialize(&admin); + client.mint(&admin, &from, &100_i128); + + client.transfer(&from, &from, &to, &101_i128); // must panic +} +``` + +--- + +## Running Security Tests + +```bash +cargo test security +# or run all tests +cargo test +``` diff --git a/docs/tokenization-module.md b/docs/tokenization-module.md new file mode 100644 index 00000000..610e4c5d --- /dev/null +++ b/docs/tokenization-module.md @@ -0,0 +1,130 @@ +# Tokenization Module Documentation + +> Closes #348 + +## Overview + +The tokenization module manages the lifecycle of TeachLink tokens (TLT): minting rewards, transferring balances, and querying supply. It is implemented as part of the Soroban smart contract and follows the Stellar token interface conventions. + +--- + +## Functions + +### `mint` + +Mints new tokens and credits them to a recipient address. Only callable by the contract admin. + +```rust +pub fn mint(env: Env, to: Address, amount: i128) -> Result<(), Error> +``` + +| Parameter | Type | Description | +|-----------|-----------|------------------------------------| +| `env` | `Env` | Soroban environment handle | +| `to` | `Address` | Recipient address | +| `amount` | `i128` | Number of tokens to mint (> 0) | + +**Errors**: `Unauthorized`, `InvalidAmount`, `OverflowError` + +**Example**: +```rust +client.mint(&admin, &learner_address, &500_i128); +``` + +--- + +### `transfer` + +Transfers tokens from one address to another. The sender must authorize the call. + +```rust +pub fn transfer(env: Env, from: Address, to: Address, amount: i128) -> Result<(), Error> +``` + +| Parameter | Type | Description | +|-----------|-----------|------------------------------------| +| `env` | `Env` | Soroban environment handle | +| `from` | `Address` | Sender address (must authorize) | +| `to` | `Address` | Recipient address | +| `amount` | `i128` | Number of tokens to transfer (> 0) | + +**Errors**: `Unauthorized`, `InvalidAmount`, `InsufficientBalance` + +**Example**: +```rust +client.transfer(&sender, &sender, &recipient, &100_i128); +``` + +--- + +### `balance` + +Returns the token balance of a given address. + +```rust +pub fn balance(env: Env, account: Address) -> i128 +``` + +| Parameter | Type | Description | +|-----------|-----------|--------------------------| +| `env` | `Env` | Soroban environment handle | +| `account` | `Address` | Address to query | + +**Returns**: Token balance as `i128` (0 if account has no balance). + +**Example**: +```rust +let bal = client.balance(&learner_address); +assert!(bal >= 0); +``` + +--- + +### `total_supply` + +Returns the total number of tokens currently in circulation. + +```rust +pub fn total_supply(env: Env) -> i128 +``` + +**Returns**: Total minted supply as `i128`. + +--- + +## Usage Example + +```rust +#[test] +fn test_tokenization_flow() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let learner = Address::generate(&env); + let contract_id = env.register_contract(None, TeachLinkContract); + let client = TeachLinkContractClient::new(&env, &contract_id); + + client.initialize(&admin); + + // Mint tokens to learner + client.mint(&admin, &learner, &1000_i128); + assert_eq!(client.balance(&learner), 1000); + + // Transfer tokens + client.transfer(&learner, &learner, &admin, &200_i128); + assert_eq!(client.balance(&learner), 800); + assert_eq!(client.balance(&admin), 200); + + // Check total supply + assert_eq!(client.total_supply(), 1000); +} +``` + +--- + +## Notes + +- All amounts are represented as `i128` to support Stellar's token precision. +- The module stores balances in Soroban's persistent ledger storage under each address key. +- Token decimals follow the Stellar standard of 7 decimal places.