From 13c77ee79da808572de5816971e27446a79fbbfb Mon Sep 17 00:00:00 2001 From: Timi Date: Sat, 25 Apr 2026 19:39:27 +0100 Subject: [PATCH 1/7] refactor: standardize test environment setup and utilities across test files --- TESTING_PLATFORM.md | 25 +++++++++ contracts/teachlink/tests/common/mod.rs | 39 +++++++++++++ contracts/teachlink/tests/test_assessment.rs | 48 ++++++---------- .../tests/test_interface_versioning.rs | 23 ++++---- contracts/teachlink/tests/test_rewards.rs | 23 ++++---- contracts/teachlink/tests/test_security.rs | 55 ++++++++----------- 6 files changed, 125 insertions(+), 88 deletions(-) create mode 100644 contracts/teachlink/tests/common/mod.rs diff --git a/TESTING_PLATFORM.md b/TESTING_PLATFORM.md index 9f6adab0..f1c9085a 100644 --- a/TESTING_PLATFORM.md +++ b/TESTING_PLATFORM.md @@ -61,36 +61,43 @@ cargo audit ## Features ### 1. Automated Test Generation + - Auto-generate unit tests from contract interfaces - Property-based testing with proptest - Fuzz testing support - Snapshot testing ### 2. Performance Testing + - Criterion benchmarks for all operations - Load testing with configurable scenarios - Latency measurement (p50, p95, p99) - Gas optimization analysis ### 3. Security Testing + - Vulnerability scanning (reentrancy, overflow, access control) - Dependency audit - Attack vector testing - Security score calculation ### 4. Test Data Management + - Reusable test fixtures - Mock data generators - Test environment isolation - Deterministic test data +- Shared TeachLink test helpers in `contracts/teachlink/tests/common/mod.rs` for consistent environment setup, bridge registration, and binary data creation ### 5. Analytics & Reporting + - Code coverage tracking - Test execution metrics - Quality score calculation - Trend analysis ### 6. CI/CD Integration + - GitHub Actions workflows - Automated test execution - Coverage reporting @@ -99,21 +106,25 @@ cargo audit ## Test Categories ### Unit Tests (32 passing) + - Insurance contract: 13 tests - Governance contract: 19 tests - Located in `contracts/*/tests/` ### Integration Tests + - Full flow testing - Cross-contract interactions - Located in `testing/integration/` ### Property Tests + - Mathematical invariants - Input validation - Located in `testing/property/` ### Performance Tests + - Bridge operations benchmarks - Escrow operations benchmarks - Located in `benches/` @@ -121,23 +132,28 @@ cargo audit ## Configuration ### Load Testing + Edit `testing/load/load_test_config.toml`: + - Concurrent users - Test duration - Operation weights - Performance thresholds ### Coverage + Minimum coverage target: 80% Current coverage: Check `testing/reports/coverage/` ### Security + Security score target: 90% Run: `cargo audit` for dependency vulnerabilities ## CI/CD Pipeline ### On Push/PR + 1. Code formatting check 2. Clippy linting 3. Unit tests @@ -146,6 +162,7 @@ Run: `cargo audit` for dependency vulnerabilities 6. Coverage report ### Nightly + 1. Full test suite 2. Performance benchmarks 3. Load testing @@ -154,12 +171,14 @@ Run: `cargo audit` for dependency vulnerabilities ## Quality Metrics ### Current Status + - Total tests: 32 passing - Code coverage: TBD - Security score: TBD - Performance: TBD ### Targets + - Test coverage: >80% - Security score: >90% - Bridge latency: <100ms @@ -168,6 +187,7 @@ Run: `cargo audit` for dependency vulnerabilities ## Usage Examples ### Generate Tests + ```rust use testing::automated::TestGenerator; @@ -177,12 +197,14 @@ generator.write_tests(Path::new("tests/generated"))?; ``` ### Run Benchmarks + ```bash cargo bench --bench bridge_operations cargo bench --bench escrow_operations -- --save-baseline main ``` ### Security Scan + ```rust use testing::security::VulnerabilityScanner; @@ -192,6 +214,7 @@ println!("{}", scanner.generate_report()); ``` ### Load Testing + ```bash # Configure in testing/load/load_test_config.toml # Run load test @@ -201,6 +224,7 @@ cargo test --test load_test -- --ignored ## Contributing When adding new features: + 1. Write unit tests 2. Add integration tests 3. Update benchmarks @@ -210,6 +234,7 @@ When adding new features: ## Reports Generated reports location: + - Coverage: `testing/reports/coverage/` - Benchmarks: `target/criterion/` - Security: `testing/reports/security_audit.json` diff --git a/contracts/teachlink/tests/common/mod.rs b/contracts/teachlink/tests/common/mod.rs new file mode 100644 index 00000000..240970e3 --- /dev/null +++ b/contracts/teachlink/tests/common/mod.rs @@ -0,0 +1,39 @@ +//! Shared fixtures and helpers for TeachLink contract tests. +//! +//! These helpers standardize environment setup, contract registration, and test data +//! generation for the teachlink test suite. +#![allow(clippy::needless_pass_by_value)] + +use soroban_sdk::{testutils::Address as _, Address, Bytes, Env}; +use teachlink_contract::{TeachLinkBridge, TeachLinkBridgeClient}; + +/// Returns a fresh test environment with mocked authentication. +pub fn test_env() -> Env { + let mut env = Env::default(); + env.mock_all_auths(); + env +} + +/// Registers the main TeachLink bridge contract and returns its client. +pub fn register_bridge_client(env: &Env) -> TeachLinkBridgeClient<'_> { + let contract_id = env.register(TeachLinkBridge, ()); + TeachLinkBridgeClient::new(env, &contract_id) +} + +/// Generates a new random address inside the given environment. +pub fn random_address(env: &Env) -> Address { + Address::generate(env) +} + +/// Creates a Bytes payload from a UTF-8 string. +pub fn bytes(env: &Env, value: &str) -> Bytes { + Bytes::from_slice(env, value.as_bytes()) +} + +/// Sets up a simple bridge test fixture with a deployer, creator, and student. +pub fn setup_bridge_test(env: &Env) -> (TeachLinkBridgeClient<'_>, Address, Address) { + let client = register_bridge_client(env); + let creator = random_address(env); + let student = random_address(env); + (client, creator, student) +} diff --git a/contracts/teachlink/tests/test_assessment.rs b/contracts/teachlink/tests/test_assessment.rs index c3943104..4e2ee776 100644 --- a/contracts/teachlink/tests/test_assessment.rs +++ b/contracts/teachlink/tests/test_assessment.rs @@ -1,27 +1,17 @@ #![allow(clippy::needless_pass_by_value)] -use soroban_sdk::{testutils::Address as _, Address, Bytes, Env, Map, Vec}; -use teachlink_contract::{ - AssessmentSettings, QuestionType, TeachLinkBridge, TeachLinkBridgeClient, -}; +mod common; -fn setup_test(env: &Env) -> (TeachLinkBridgeClient<'_>, Address, Address) { - let contract_id = env.register(TeachLinkBridge, ()); - let client = TeachLinkBridgeClient::new(env, &contract_id); - - let creator = Address::generate(env); - let student = Address::generate(env); - - (client, creator, student) -} +use soroban_sdk::{Bytes, Env, Map, Vec}; +use teachlink_contract::{AssessmentSettings, QuestionType}; +use common::{bytes, setup_bridge_test, test_env}; #[test] fn test_create_assessment() { - let env = Env::default(); - let (client, creator, _) = setup_test(&env); - env.mock_all_auths(); + let env = test_env(); + let (client, creator, _) = setup_bridge_test(&env); - let title = Bytes::from_slice(&env, b"Rust Mastery Quiz"); + let title = bytes(&env, "Rust Mastery Quiz"); let description = Bytes::from_slice(&env, b"Test your Rust skills"); let questions = Vec::new(&env); let settings = AssessmentSettings { @@ -43,11 +33,10 @@ fn test_create_assessment() { #[test] fn test_add_question() { - let env = Env::default(); - let (client, creator, _) = setup_test(&env); - env.mock_all_auths(); + let env = test_env(); + let (client, creator, _) = setup_bridge_test(&env); - let content_hash = Bytes::from_slice(&env, b"What is ownership?"); + let content_hash = bytes(&env, "What is ownership?"); let correct_hash = Bytes::from_slice(&env, b"Memory safety mechanism"); let metadata = Map::new(&env); @@ -66,9 +55,8 @@ fn test_add_question() { #[test] fn test_submit_assessment_grading() { - let env = Env::default(); - let (client, creator, student) = setup_test(&env); - env.mock_all_auths(); + let env = test_env(); + let (client, creator, student) = setup_bridge_test(&env); // 1. Add questions let q1_correct = Bytes::from_slice(&env, b"A"); @@ -130,9 +118,8 @@ fn test_submit_assessment_grading() { #[test] fn test_adaptive_selection() { - let env = Env::default(); - let (client, creator, _) = setup_test(&env); - env.mock_all_auths(); + let env = test_env(); + let (client, creator, _) = setup_bridge_test(&env); // Add easy, medium, hard questions let q_easy = client.add_assessment_question( @@ -197,10 +184,9 @@ fn test_adaptive_selection() { #[test] #[should_panic(expected = "Error(Contract, #7)")] // PlagiarismDetected = 7 fn test_plagiarism_detection() { - let env = Env::default(); - let (client, creator, student1) = setup_test(&env); - let student2 = Address::generate(&env); - env.mock_all_auths(); + let env = test_env(); + let (client, creator, student1) = setup_bridge_test(&env); + let student2 = random_address(&env); let q1_id = client.add_assessment_question( &creator, diff --git a/contracts/teachlink/tests/test_interface_versioning.rs b/contracts/teachlink/tests/test_interface_versioning.rs index 9563f5df..8ecd0e6b 100644 --- a/contracts/teachlink/tests/test_interface_versioning.rs +++ b/contracts/teachlink/tests/test_interface_versioning.rs @@ -1,21 +1,20 @@ #![allow(clippy::needless_pass_by_value)] +mod common; + use soroban_sdk::testutils::Address as _; use soroban_sdk::{Address, Env}; use teachlink_contract::{ContractSemVer, TeachLinkBridge, TeachLinkBridgeClient}; +use common::{register_bridge_client, register_sac_token, random_address, test_env}; fn setup_client(env: &Env) -> (TeachLinkBridgeClient<'_>, Address, Address, Address) { - let contract_id = env.register(TeachLinkBridge, ()); - let client = TeachLinkBridgeClient::new(env, &contract_id); - - let token_admin = Address::generate(env); - let sac = env.register_stellar_asset_contract_v2(token_admin); - let token = sac.address(); + env.mock_all_auths(); + let client = register_bridge_client(env); + let token = register_sac_token(env); - let admin = Address::generate(env); - let fee_recipient = Address::generate(env); + let admin = random_address(env); + let fee_recipient = random_address(env); - env.mock_all_auths(); client.initialize(&token, &admin, &1, &fee_recipient); (client, token, admin, fee_recipient) @@ -23,7 +22,7 @@ fn setup_client(env: &Env) -> (TeachLinkBridgeClient<'_>, Address, Address, Addr #[test] fn interface_version_defaults_follow_semver() { - let env = Env::default(); + let env = test_env(); let (client, _, _, _) = setup_client(&env); let status = client.get_interface_version_status(); @@ -36,7 +35,7 @@ fn interface_version_defaults_follow_semver() { #[test] fn interface_version_range_enforces_compatibility_window() { - let env = Env::default(); + let env = test_env(); let (client, _, _, _) = setup_client(&env); env.mock_all_auths(); @@ -52,7 +51,7 @@ fn interface_version_range_enforces_compatibility_window() { #[test] fn interface_version_rejects_invalid_ranges() { - let env = Env::default(); + let env = test_env(); let (client, _, _, _) = setup_client(&env); env.mock_all_auths(); diff --git a/contracts/teachlink/tests/test_rewards.rs b/contracts/teachlink/tests/test_rewards.rs index d6137b23..87a6250a 100644 --- a/contracts/teachlink/tests/test_rewards.rs +++ b/contracts/teachlink/tests/test_rewards.rs @@ -4,9 +4,12 @@ #![allow(clippy::unreadable_literal)] #![allow(unused_variables)] -use soroban_sdk::{testutils::Address as _, Address, Env}; +mod common; +use soroban_sdk::testutils::Address as _; +use soroban_sdk::{Address, Env}; use teachlink_contract::TeachLinkBridge; +use common::test_env; #[test] fn test_teachlink_contract_creation() { @@ -20,8 +23,7 @@ fn test_teachlink_contract_creation() { #[test] fn test_address_generation() { - let env = Env::default(); - env.mock_all_auths(); + let env = test_env(); let addr1 = Address::generate(&env); let addr2 = Address::generate(&env); @@ -32,8 +34,7 @@ fn test_address_generation() { #[test] fn test_multiple_contract_instances() { - let env = Env::default(); - env.mock_all_auths(); + let env = test_env(); let contract_id_1 = env.register(TeachLinkBridge, ()); let contract_id_2 = env.register(TeachLinkBridge, ()); @@ -44,8 +45,7 @@ fn test_multiple_contract_instances() { #[test] fn test_environment_setup() { - let env = Env::default(); - env.mock_all_auths(); + let env = test_env(); // Verify environment is initialized let addr = Address::generate(&env); @@ -57,8 +57,7 @@ fn test_environment_setup() { #[test] fn test_multiple_addresses_unique() { - let env = Env::default(); - env.mock_all_auths(); + let env = test_env(); let addresses: Vec
= (0..5).map(|_| Address::generate(&env)).collect(); @@ -72,8 +71,7 @@ fn test_multiple_addresses_unique() { #[test] fn test_address_consistency() { - let env = Env::default(); - env.mock_all_auths(); + let env = test_env(); let addr = Address::generate(&env); @@ -83,8 +81,7 @@ fn test_address_consistency() { #[test] fn test_contract_registration_success() { - let env = Env::default(); - env.mock_all_auths(); + let env = test_env(); let contract_id = env.register(TeachLinkBridge, ()); let admin = Address::generate(&env); diff --git a/contracts/teachlink/tests/test_security.rs b/contracts/teachlink/tests/test_security.rs index 926a77f2..ca8fbe0b 100644 --- a/contracts/teachlink/tests/test_security.rs +++ b/contracts/teachlink/tests/test_security.rs @@ -6,10 +6,13 @@ #![allow(clippy::needless_pass_by_value)] +mod common; + use soroban_sdk::testutils::Address as _; use soroban_sdk::{symbol_short, vec, Address, Bytes, Env, IntoVal, String}; use teachlink_contract::validation::{config, NumberValidator, ValidationError}; use teachlink_contract::{TeachLinkBridge, TeachLinkBridgeClient}; +use common::{random_address, register_bridge_client, register_sac_token, test_env}; fn mint_sac_token(env: &Env, token: &Address, to: &Address, amount: i128) { env.invoke_contract::<()>( @@ -20,20 +23,15 @@ fn mint_sac_token(env: &Env, token: &Address, to: &Address, amount: i128) { } fn setup_rewards_with_sac(env: &Env) -> (TeachLinkBridgeClient<'_>, Address, Address, Address) { - let contract_id = env.register(TeachLinkBridge, ()); - let client = TeachLinkBridgeClient::new(env, &contract_id); - - let token_admin = Address::generate(env); - let sac = env.register_stellar_asset_contract_v2(token_admin.clone()); - let token = sac.address(); - - let admin = Address::generate(env); - let fee_recipient = Address::generate(env); - let rewards_admin = Address::generate(env); - let funder = Address::generate(env); - let recipient = Address::generate(env); - env.mock_all_auths(); + let client = register_bridge_client(env); + let token = register_sac_token(env); + + let admin = random_address(env); + let fee_recipient = random_address(env); + let rewards_admin = random_address(env); + let funder = random_address(env); + let recipient = random_address(env); client.initialize(&token, &admin, &1, &fee_recipient); client.initialize_rewards(&token, &rewards_admin); @@ -74,15 +72,13 @@ fn security_integer_underflow_saturating_math_avoids_wrap() { #[test] fn security_access_control_admin_bridge_fee_requires_auth() { - let env = Env::default(); - let contract_id = env.register(TeachLinkBridge, ()); - let client = TeachLinkBridgeClient::new(&env, &contract_id); + let env = test_env(); + let client = register_bridge_client(&env); - let token = Address::generate(&env); - let admin = Address::generate(&env); - let fee_recipient = Address::generate(&env); + let token = register_sac_token(&env); + let admin = random_address(&env); + let fee_recipient = random_address(&env); - env.mock_all_auths(); client.initialize(&token, &admin, &1, &fee_recipient); env.mock_auths(&[]); @@ -92,7 +88,7 @@ fn security_access_control_admin_bridge_fee_requires_auth() { #[test] fn security_access_control_issue_reward_requires_rewards_admin_auth() { - let env = Env::default(); + let env = test_env(); let (client, _rewards_admin, _funder, recipient) = setup_rewards_with_sac(&env); let reward_type = String::from_str(&env, "learning"); @@ -103,20 +99,15 @@ fn security_access_control_issue_reward_requires_rewards_admin_auth() { #[test] fn security_front_running_ordering_bridge_nonce_increments_monotonically() { - let env = Env::default(); - let contract_id = env.register(TeachLinkBridge, ()); - let client = TeachLinkBridgeClient::new(&env, &contract_id); + let env = test_env(); + let client = register_bridge_client(&env); + let token = register_sac_token(&env); - let token_admin = Address::generate(&env); - let sac = env.register_stellar_asset_contract_v2(token_admin.clone()); - let token = sac.address(); - - let admin = Address::generate(&env); - let fee_recipient = Address::generate(&env); - let user = Address::generate(&env); + let admin = random_address(&env); + let fee_recipient = random_address(&env); + let user = random_address(&env); let dest = Bytes::from_slice(&env, &[0xcd; 20]); - env.mock_all_auths(); client.initialize(&token, &admin, &1, &fee_recipient); client.add_supported_chain(&1); From e6d6054d89af5677bc466b05685f5bc547f43506 Mon Sep 17 00:00:00 2001 From: Timi Date: Sat, 25 Apr 2026 19:48:52 +0100 Subject: [PATCH 2/7] refactor: standardize test environment setup and utilities across test files --- TESTING_PLATFORM.md | 25 +++++++++ contracts/teachlink/tests/common/mod.rs | 39 +++++++++++++ contracts/teachlink/tests/test_assessment.rs | 48 ++++++---------- .../tests/test_interface_versioning.rs | 23 ++++---- contracts/teachlink/tests/test_rewards.rs | 23 ++++---- contracts/teachlink/tests/test_security.rs | 55 ++++++++----------- 6 files changed, 125 insertions(+), 88 deletions(-) create mode 100644 contracts/teachlink/tests/common/mod.rs diff --git a/TESTING_PLATFORM.md b/TESTING_PLATFORM.md index 9f6adab0..f1c9085a 100644 --- a/TESTING_PLATFORM.md +++ b/TESTING_PLATFORM.md @@ -61,36 +61,43 @@ cargo audit ## Features ### 1. Automated Test Generation + - Auto-generate unit tests from contract interfaces - Property-based testing with proptest - Fuzz testing support - Snapshot testing ### 2. Performance Testing + - Criterion benchmarks for all operations - Load testing with configurable scenarios - Latency measurement (p50, p95, p99) - Gas optimization analysis ### 3. Security Testing + - Vulnerability scanning (reentrancy, overflow, access control) - Dependency audit - Attack vector testing - Security score calculation ### 4. Test Data Management + - Reusable test fixtures - Mock data generators - Test environment isolation - Deterministic test data +- Shared TeachLink test helpers in `contracts/teachlink/tests/common/mod.rs` for consistent environment setup, bridge registration, and binary data creation ### 5. Analytics & Reporting + - Code coverage tracking - Test execution metrics - Quality score calculation - Trend analysis ### 6. CI/CD Integration + - GitHub Actions workflows - Automated test execution - Coverage reporting @@ -99,21 +106,25 @@ cargo audit ## Test Categories ### Unit Tests (32 passing) + - Insurance contract: 13 tests - Governance contract: 19 tests - Located in `contracts/*/tests/` ### Integration Tests + - Full flow testing - Cross-contract interactions - Located in `testing/integration/` ### Property Tests + - Mathematical invariants - Input validation - Located in `testing/property/` ### Performance Tests + - Bridge operations benchmarks - Escrow operations benchmarks - Located in `benches/` @@ -121,23 +132,28 @@ cargo audit ## Configuration ### Load Testing + Edit `testing/load/load_test_config.toml`: + - Concurrent users - Test duration - Operation weights - Performance thresholds ### Coverage + Minimum coverage target: 80% Current coverage: Check `testing/reports/coverage/` ### Security + Security score target: 90% Run: `cargo audit` for dependency vulnerabilities ## CI/CD Pipeline ### On Push/PR + 1. Code formatting check 2. Clippy linting 3. Unit tests @@ -146,6 +162,7 @@ Run: `cargo audit` for dependency vulnerabilities 6. Coverage report ### Nightly + 1. Full test suite 2. Performance benchmarks 3. Load testing @@ -154,12 +171,14 @@ Run: `cargo audit` for dependency vulnerabilities ## Quality Metrics ### Current Status + - Total tests: 32 passing - Code coverage: TBD - Security score: TBD - Performance: TBD ### Targets + - Test coverage: >80% - Security score: >90% - Bridge latency: <100ms @@ -168,6 +187,7 @@ Run: `cargo audit` for dependency vulnerabilities ## Usage Examples ### Generate Tests + ```rust use testing::automated::TestGenerator; @@ -177,12 +197,14 @@ generator.write_tests(Path::new("tests/generated"))?; ``` ### Run Benchmarks + ```bash cargo bench --bench bridge_operations cargo bench --bench escrow_operations -- --save-baseline main ``` ### Security Scan + ```rust use testing::security::VulnerabilityScanner; @@ -192,6 +214,7 @@ println!("{}", scanner.generate_report()); ``` ### Load Testing + ```bash # Configure in testing/load/load_test_config.toml # Run load test @@ -201,6 +224,7 @@ cargo test --test load_test -- --ignored ## Contributing When adding new features: + 1. Write unit tests 2. Add integration tests 3. Update benchmarks @@ -210,6 +234,7 @@ When adding new features: ## Reports Generated reports location: + - Coverage: `testing/reports/coverage/` - Benchmarks: `target/criterion/` - Security: `testing/reports/security_audit.json` diff --git a/contracts/teachlink/tests/common/mod.rs b/contracts/teachlink/tests/common/mod.rs new file mode 100644 index 00000000..240970e3 --- /dev/null +++ b/contracts/teachlink/tests/common/mod.rs @@ -0,0 +1,39 @@ +//! Shared fixtures and helpers for TeachLink contract tests. +//! +//! These helpers standardize environment setup, contract registration, and test data +//! generation for the teachlink test suite. +#![allow(clippy::needless_pass_by_value)] + +use soroban_sdk::{testutils::Address as _, Address, Bytes, Env}; +use teachlink_contract::{TeachLinkBridge, TeachLinkBridgeClient}; + +/// Returns a fresh test environment with mocked authentication. +pub fn test_env() -> Env { + let mut env = Env::default(); + env.mock_all_auths(); + env +} + +/// Registers the main TeachLink bridge contract and returns its client. +pub fn register_bridge_client(env: &Env) -> TeachLinkBridgeClient<'_> { + let contract_id = env.register(TeachLinkBridge, ()); + TeachLinkBridgeClient::new(env, &contract_id) +} + +/// Generates a new random address inside the given environment. +pub fn random_address(env: &Env) -> Address { + Address::generate(env) +} + +/// Creates a Bytes payload from a UTF-8 string. +pub fn bytes(env: &Env, value: &str) -> Bytes { + Bytes::from_slice(env, value.as_bytes()) +} + +/// Sets up a simple bridge test fixture with a deployer, creator, and student. +pub fn setup_bridge_test(env: &Env) -> (TeachLinkBridgeClient<'_>, Address, Address) { + let client = register_bridge_client(env); + let creator = random_address(env); + let student = random_address(env); + (client, creator, student) +} diff --git a/contracts/teachlink/tests/test_assessment.rs b/contracts/teachlink/tests/test_assessment.rs index c3943104..4e2ee776 100644 --- a/contracts/teachlink/tests/test_assessment.rs +++ b/contracts/teachlink/tests/test_assessment.rs @@ -1,27 +1,17 @@ #![allow(clippy::needless_pass_by_value)] -use soroban_sdk::{testutils::Address as _, Address, Bytes, Env, Map, Vec}; -use teachlink_contract::{ - AssessmentSettings, QuestionType, TeachLinkBridge, TeachLinkBridgeClient, -}; +mod common; -fn setup_test(env: &Env) -> (TeachLinkBridgeClient<'_>, Address, Address) { - let contract_id = env.register(TeachLinkBridge, ()); - let client = TeachLinkBridgeClient::new(env, &contract_id); - - let creator = Address::generate(env); - let student = Address::generate(env); - - (client, creator, student) -} +use soroban_sdk::{Bytes, Env, Map, Vec}; +use teachlink_contract::{AssessmentSettings, QuestionType}; +use common::{bytes, setup_bridge_test, test_env}; #[test] fn test_create_assessment() { - let env = Env::default(); - let (client, creator, _) = setup_test(&env); - env.mock_all_auths(); + let env = test_env(); + let (client, creator, _) = setup_bridge_test(&env); - let title = Bytes::from_slice(&env, b"Rust Mastery Quiz"); + let title = bytes(&env, "Rust Mastery Quiz"); let description = Bytes::from_slice(&env, b"Test your Rust skills"); let questions = Vec::new(&env); let settings = AssessmentSettings { @@ -43,11 +33,10 @@ fn test_create_assessment() { #[test] fn test_add_question() { - let env = Env::default(); - let (client, creator, _) = setup_test(&env); - env.mock_all_auths(); + let env = test_env(); + let (client, creator, _) = setup_bridge_test(&env); - let content_hash = Bytes::from_slice(&env, b"What is ownership?"); + let content_hash = bytes(&env, "What is ownership?"); let correct_hash = Bytes::from_slice(&env, b"Memory safety mechanism"); let metadata = Map::new(&env); @@ -66,9 +55,8 @@ fn test_add_question() { #[test] fn test_submit_assessment_grading() { - let env = Env::default(); - let (client, creator, student) = setup_test(&env); - env.mock_all_auths(); + let env = test_env(); + let (client, creator, student) = setup_bridge_test(&env); // 1. Add questions let q1_correct = Bytes::from_slice(&env, b"A"); @@ -130,9 +118,8 @@ fn test_submit_assessment_grading() { #[test] fn test_adaptive_selection() { - let env = Env::default(); - let (client, creator, _) = setup_test(&env); - env.mock_all_auths(); + let env = test_env(); + let (client, creator, _) = setup_bridge_test(&env); // Add easy, medium, hard questions let q_easy = client.add_assessment_question( @@ -197,10 +184,9 @@ fn test_adaptive_selection() { #[test] #[should_panic(expected = "Error(Contract, #7)")] // PlagiarismDetected = 7 fn test_plagiarism_detection() { - let env = Env::default(); - let (client, creator, student1) = setup_test(&env); - let student2 = Address::generate(&env); - env.mock_all_auths(); + let env = test_env(); + let (client, creator, student1) = setup_bridge_test(&env); + let student2 = random_address(&env); let q1_id = client.add_assessment_question( &creator, diff --git a/contracts/teachlink/tests/test_interface_versioning.rs b/contracts/teachlink/tests/test_interface_versioning.rs index 9563f5df..8ecd0e6b 100644 --- a/contracts/teachlink/tests/test_interface_versioning.rs +++ b/contracts/teachlink/tests/test_interface_versioning.rs @@ -1,21 +1,20 @@ #![allow(clippy::needless_pass_by_value)] +mod common; + use soroban_sdk::testutils::Address as _; use soroban_sdk::{Address, Env}; use teachlink_contract::{ContractSemVer, TeachLinkBridge, TeachLinkBridgeClient}; +use common::{register_bridge_client, register_sac_token, random_address, test_env}; fn setup_client(env: &Env) -> (TeachLinkBridgeClient<'_>, Address, Address, Address) { - let contract_id = env.register(TeachLinkBridge, ()); - let client = TeachLinkBridgeClient::new(env, &contract_id); - - let token_admin = Address::generate(env); - let sac = env.register_stellar_asset_contract_v2(token_admin); - let token = sac.address(); + env.mock_all_auths(); + let client = register_bridge_client(env); + let token = register_sac_token(env); - let admin = Address::generate(env); - let fee_recipient = Address::generate(env); + let admin = random_address(env); + let fee_recipient = random_address(env); - env.mock_all_auths(); client.initialize(&token, &admin, &1, &fee_recipient); (client, token, admin, fee_recipient) @@ -23,7 +22,7 @@ fn setup_client(env: &Env) -> (TeachLinkBridgeClient<'_>, Address, Address, Addr #[test] fn interface_version_defaults_follow_semver() { - let env = Env::default(); + let env = test_env(); let (client, _, _, _) = setup_client(&env); let status = client.get_interface_version_status(); @@ -36,7 +35,7 @@ fn interface_version_defaults_follow_semver() { #[test] fn interface_version_range_enforces_compatibility_window() { - let env = Env::default(); + let env = test_env(); let (client, _, _, _) = setup_client(&env); env.mock_all_auths(); @@ -52,7 +51,7 @@ fn interface_version_range_enforces_compatibility_window() { #[test] fn interface_version_rejects_invalid_ranges() { - let env = Env::default(); + let env = test_env(); let (client, _, _, _) = setup_client(&env); env.mock_all_auths(); diff --git a/contracts/teachlink/tests/test_rewards.rs b/contracts/teachlink/tests/test_rewards.rs index d6137b23..87a6250a 100644 --- a/contracts/teachlink/tests/test_rewards.rs +++ b/contracts/teachlink/tests/test_rewards.rs @@ -4,9 +4,12 @@ #![allow(clippy::unreadable_literal)] #![allow(unused_variables)] -use soroban_sdk::{testutils::Address as _, Address, Env}; +mod common; +use soroban_sdk::testutils::Address as _; +use soroban_sdk::{Address, Env}; use teachlink_contract::TeachLinkBridge; +use common::test_env; #[test] fn test_teachlink_contract_creation() { @@ -20,8 +23,7 @@ fn test_teachlink_contract_creation() { #[test] fn test_address_generation() { - let env = Env::default(); - env.mock_all_auths(); + let env = test_env(); let addr1 = Address::generate(&env); let addr2 = Address::generate(&env); @@ -32,8 +34,7 @@ fn test_address_generation() { #[test] fn test_multiple_contract_instances() { - let env = Env::default(); - env.mock_all_auths(); + let env = test_env(); let contract_id_1 = env.register(TeachLinkBridge, ()); let contract_id_2 = env.register(TeachLinkBridge, ()); @@ -44,8 +45,7 @@ fn test_multiple_contract_instances() { #[test] fn test_environment_setup() { - let env = Env::default(); - env.mock_all_auths(); + let env = test_env(); // Verify environment is initialized let addr = Address::generate(&env); @@ -57,8 +57,7 @@ fn test_environment_setup() { #[test] fn test_multiple_addresses_unique() { - let env = Env::default(); - env.mock_all_auths(); + let env = test_env(); let addresses: Vec
= (0..5).map(|_| Address::generate(&env)).collect(); @@ -72,8 +71,7 @@ fn test_multiple_addresses_unique() { #[test] fn test_address_consistency() { - let env = Env::default(); - env.mock_all_auths(); + let env = test_env(); let addr = Address::generate(&env); @@ -83,8 +81,7 @@ fn test_address_consistency() { #[test] fn test_contract_registration_success() { - let env = Env::default(); - env.mock_all_auths(); + let env = test_env(); let contract_id = env.register(TeachLinkBridge, ()); let admin = Address::generate(&env); diff --git a/contracts/teachlink/tests/test_security.rs b/contracts/teachlink/tests/test_security.rs index 926a77f2..ca8fbe0b 100644 --- a/contracts/teachlink/tests/test_security.rs +++ b/contracts/teachlink/tests/test_security.rs @@ -6,10 +6,13 @@ #![allow(clippy::needless_pass_by_value)] +mod common; + use soroban_sdk::testutils::Address as _; use soroban_sdk::{symbol_short, vec, Address, Bytes, Env, IntoVal, String}; use teachlink_contract::validation::{config, NumberValidator, ValidationError}; use teachlink_contract::{TeachLinkBridge, TeachLinkBridgeClient}; +use common::{random_address, register_bridge_client, register_sac_token, test_env}; fn mint_sac_token(env: &Env, token: &Address, to: &Address, amount: i128) { env.invoke_contract::<()>( @@ -20,20 +23,15 @@ fn mint_sac_token(env: &Env, token: &Address, to: &Address, amount: i128) { } fn setup_rewards_with_sac(env: &Env) -> (TeachLinkBridgeClient<'_>, Address, Address, Address) { - let contract_id = env.register(TeachLinkBridge, ()); - let client = TeachLinkBridgeClient::new(env, &contract_id); - - let token_admin = Address::generate(env); - let sac = env.register_stellar_asset_contract_v2(token_admin.clone()); - let token = sac.address(); - - let admin = Address::generate(env); - let fee_recipient = Address::generate(env); - let rewards_admin = Address::generate(env); - let funder = Address::generate(env); - let recipient = Address::generate(env); - env.mock_all_auths(); + let client = register_bridge_client(env); + let token = register_sac_token(env); + + let admin = random_address(env); + let fee_recipient = random_address(env); + let rewards_admin = random_address(env); + let funder = random_address(env); + let recipient = random_address(env); client.initialize(&token, &admin, &1, &fee_recipient); client.initialize_rewards(&token, &rewards_admin); @@ -74,15 +72,13 @@ fn security_integer_underflow_saturating_math_avoids_wrap() { #[test] fn security_access_control_admin_bridge_fee_requires_auth() { - let env = Env::default(); - let contract_id = env.register(TeachLinkBridge, ()); - let client = TeachLinkBridgeClient::new(&env, &contract_id); + let env = test_env(); + let client = register_bridge_client(&env); - let token = Address::generate(&env); - let admin = Address::generate(&env); - let fee_recipient = Address::generate(&env); + let token = register_sac_token(&env); + let admin = random_address(&env); + let fee_recipient = random_address(&env); - env.mock_all_auths(); client.initialize(&token, &admin, &1, &fee_recipient); env.mock_auths(&[]); @@ -92,7 +88,7 @@ fn security_access_control_admin_bridge_fee_requires_auth() { #[test] fn security_access_control_issue_reward_requires_rewards_admin_auth() { - let env = Env::default(); + let env = test_env(); let (client, _rewards_admin, _funder, recipient) = setup_rewards_with_sac(&env); let reward_type = String::from_str(&env, "learning"); @@ -103,20 +99,15 @@ fn security_access_control_issue_reward_requires_rewards_admin_auth() { #[test] fn security_front_running_ordering_bridge_nonce_increments_monotonically() { - let env = Env::default(); - let contract_id = env.register(TeachLinkBridge, ()); - let client = TeachLinkBridgeClient::new(&env, &contract_id); + let env = test_env(); + let client = register_bridge_client(&env); + let token = register_sac_token(&env); - let token_admin = Address::generate(&env); - let sac = env.register_stellar_asset_contract_v2(token_admin.clone()); - let token = sac.address(); - - let admin = Address::generate(&env); - let fee_recipient = Address::generate(&env); - let user = Address::generate(&env); + let admin = random_address(&env); + let fee_recipient = random_address(&env); + let user = random_address(&env); let dest = Bytes::from_slice(&env, &[0xcd; 20]); - env.mock_all_auths(); client.initialize(&token, &admin, &1, &fee_recipient); client.add_supported_chain(&1); From 12de5732313b2a2b3d69d94964353bb36bc9f975 Mon Sep 17 00:00:00 2001 From: Timi Date: Sat, 25 Apr 2026 20:16:37 +0100 Subject: [PATCH 3/7] refactor: enhance contract upgrade mechanism with initialization and validation checks --- contracts/teachlink/src/lib.rs | 1 + contracts/teachlink/src/upgrade.rs | 51 ++++++++++++++++++++++++++---- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/contracts/teachlink/src/lib.rs b/contracts/teachlink/src/lib.rs index 52073185..fe62dd58 100644 --- a/contracts/teachlink/src/lib.rs +++ b/contracts/teachlink/src/lib.rs @@ -211,6 +211,7 @@ impl TeachLinkBridge { ) -> Result<(), BridgeError> { bridge::Bridge::initialize(&env, token, admin, min_validators, fee_recipient)?; interface_versioning::InterfaceVersioning::initialize(&env); + upgrade::ContractUpgrader::initialize(&env)?; Ok(()) } diff --git a/contracts/teachlink/src/upgrade.rs b/contracts/teachlink/src/upgrade.rs index 1039f8e9..051d166a 100644 --- a/contracts/teachlink/src/upgrade.rs +++ b/contracts/teachlink/src/upgrade.rs @@ -67,8 +67,7 @@ impl ContractUpgrader { #[cfg(not(test))] admin.require_auth(); - // Initialize if not already initialized (for testing) - #[cfg(test)] + // Initialize if not already initialized if !env.storage().instance().has(&UPGRADE_VERSION) { Self::initialize(env)?; } @@ -80,6 +79,11 @@ impl ContractUpgrader { return Err(BridgeError::InvalidInput); } + // Validate state snapshot integrity + if state_hash.is_empty() { + return Err(BridgeError::InvalidInput); + } + // Create state backup let backup = StateBackup { version: current_version, @@ -106,8 +110,7 @@ impl ContractUpgrader { #[cfg(not(test))] admin.require_auth(); - // Initialize if not already initialized (for testing) - #[cfg(test)] + // Initialize if not already initialized if !env.storage().instance().has(&UPGRADE_VERSION) { Self::initialize(env)?; } @@ -119,6 +122,11 @@ impl ContractUpgrader { return Err(BridgeError::InvalidInput); } + // Validate migration metadata + if migration_hash.is_empty() { + return Err(BridgeError::InvalidInput); + } + // Verify backup exists if !env.storage().instance().has(&UPGRADE_STATE_BACKUP) { return Err(BridgeError::StorageError); @@ -285,8 +293,26 @@ mod tests { }); } + #[test] + fn test_prepare_upgrade_auto_initializes() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register(TeachLinkBridge, ()); + let admin = Address::generate(&env); + + env.as_contract(&contract_id, || { + let state_hash = Bytes::from_slice(&env, b"state_hash"); + ContractUpgrader::prepare_upgrade(&env, admin.clone(), 2, state_hash).unwrap(); + + assert_eq!(ContractUpgrader::get_current_version(&env), 1); + assert!(ContractUpgrader::is_rollback_available(&env)); + }); + } + #[test] fn test_rollback_window_expiry() { + use soroban_sdk::testutils::{Ledger, LedgerInfo}; + let env = Env::default(); env.mock_all_auths(); let contract_id = env.register(TeachLinkBridge, ()); @@ -303,8 +329,21 @@ mod tests { let migration_hash = Bytes::from_slice(&env, b"migration"); ContractUpgrader::execute_upgrade(&env, admin.clone(), 2, migration_hash).unwrap(); - // Verify rollback is available immediately after upgrade - assert!(ContractUpgrader::is_rollback_available(&env)); + // Advance ledger past rollback window + let backup = ContractUpgrader::get_state_backup(&env).unwrap(); + env.ledger().set(LedgerInfo { + timestamp: backup.backed_up_at + ROLLBACK_WINDOW_SECONDS + 1, + protocol_version: 25, + sequence_number: 0, + network_id: Default::default(), + base_reserve: 0, + min_temp_entry_ttl: 0, + min_persistent_entry_ttl: 0, + max_entry_ttl: 2_000_000, + }); + + assert!(ContractUpgrader::rollback(&env, admin.clone()).is_err()); + assert!(!ContractUpgrader::is_rollback_available(&env)); }); } } From 8f4f0a226209c45c77170c10f9b7527dbb55dce6 Mon Sep 17 00:00:00 2001 From: Timi Date: Sun, 26 Apr 2026 14:34:23 +0100 Subject: [PATCH 4/7] test: add rollback window expiry test for contract upgrades --- contracts/teachlink/src/upgrade.rs | 51 +++++++++++++++--------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/contracts/teachlink/src/upgrade.rs b/contracts/teachlink/src/upgrade.rs index 1039f8e9..1d1aa89b 100644 --- a/contracts/teachlink/src/upgrade.rs +++ b/contracts/teachlink/src/upgrade.rs @@ -2,7 +2,6 @@ //! //! This module provides a safe upgrade path for the contract while preserving state. //! It supports version tracking, state migration, and rollback capabilities. - use crate::errors::BridgeError; use crate::storage::ADMIN; use soroban_sdk::{contracttype, Address, Bytes, Env, Map, String}; @@ -241,6 +240,29 @@ mod tests { use soroban_sdk::testutils::Address as _; use soroban_sdk::{Address, Bytes, Env}; + #[test] + fn test_rollback_window_expiry() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register(TeachLinkBridge, ()); + let admin = Address::generate(&env); + + env.as_contract(&contract_id, || { + // Initialize upgrade system + ContractUpgrader::initialize(&env).unwrap(); + + // Prepare and execute upgrade + let state_hash = Bytes::from_slice(&env, b"state_hash"); + ContractUpgrader::prepare_upgrade(&env, admin.clone(), 2, state_hash).unwrap(); + + let migration_hash = Bytes::from_slice(&env, b"migration"); + ContractUpgrader::execute_upgrade(&env, admin.clone(), 2, migration_hash).unwrap(); + + // Verify rollback is available immediately after upgrade + assert!(ContractUpgrader::is_rollback_available(&env)); + }); // closes env.as_contract + } + #[test] fn test_upgrade_lifecycle() { let env = Env::default(); @@ -282,29 +304,6 @@ mod tests { // Verify rolled back to version 1 assert_eq!(ContractUpgrader::get_current_version(&env), 1); assert!(!ContractUpgrader::is_rollback_available(&env)); - }); + }); // closes env.as_contract } - - #[test] - fn test_rollback_window_expiry() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register(TeachLinkBridge, ()); - let admin = Address::generate(&env); - - env.as_contract(&contract_id, || { - // Initialize upgrade system - ContractUpgrader::initialize(&env).unwrap(); - - // Prepare and execute upgrade - let state_hash = Bytes::from_slice(&env, b"state_hash"); - ContractUpgrader::prepare_upgrade(&env, admin.clone(), 2, state_hash).unwrap(); - - let migration_hash = Bytes::from_slice(&env, b"migration"); - ContractUpgrader::execute_upgrade(&env, admin.clone(), 2, migration_hash).unwrap(); - - // Verify rollback is available immediately after upgrade - assert!(ContractUpgrader::is_rollback_available(&env)); - }); - } -} +} // closes mod tests \ No newline at end of file From e456021acefba31286aff19d527773752ca45952 Mon Sep 17 00:00:00 2001 From: Timi Date: Sun, 26 Apr 2026 15:26:00 +0100 Subject: [PATCH 5/7] refactor: enhance contract upgrade initialization by adding admin parameter and validation checks --- contracts/teachlink/src/lib.rs | 2 +- contracts/teachlink/src/upgrade.rs | 194 +++++++----------- ...ires_same_major_and_supported_range.1.json | 24 +++ 3 files changed, 95 insertions(+), 125 deletions(-) diff --git a/contracts/teachlink/src/lib.rs b/contracts/teachlink/src/lib.rs index fe62dd58..72d23f2b 100644 --- a/contracts/teachlink/src/lib.rs +++ b/contracts/teachlink/src/lib.rs @@ -211,7 +211,7 @@ impl TeachLinkBridge { ) -> Result<(), BridgeError> { bridge::Bridge::initialize(&env, token, admin, min_validators, fee_recipient)?; interface_versioning::InterfaceVersioning::initialize(&env); - upgrade::ContractUpgrader::initialize(&env)?; + upgrade::ContractUpgrader::initialize(&env, admin.clone())?; Ok(()) } diff --git a/contracts/teachlink/src/upgrade.rs b/contracts/teachlink/src/upgrade.rs index 75bca566..8e96b611 100644 --- a/contracts/teachlink/src/upgrade.rs +++ b/contracts/teachlink/src/upgrade.rs @@ -1,18 +1,16 @@ //! Contract Upgrade Mechanism //! -//! This module provides a safe upgrade path for the contract while preserving state. -//! It supports version tracking, state migration, and rollback capabilities. +//! Safe upgrade path with versioning, migration, and rollback. + use crate::errors::BridgeError; use crate::storage::ADMIN; -use soroban_sdk::{contracttype, Address, Bytes, Env, Map, String}; +use soroban_sdk::{contracttype, Address, Bytes, Env, Map}; -/// Storage keys for upgrade mechanism pub const UPGRADE_VERSION: soroban_sdk::Symbol = soroban_sdk::symbol_short!("upg_ver"); pub const UPGRADE_HISTORY: soroban_sdk::Symbol = soroban_sdk::symbol_short!("upg_hist"); pub const UPGRADE_STATE_BACKUP: soroban_sdk::Symbol = soroban_sdk::symbol_short!("upg_back"); pub const ROLLBACK_AVAILABLE: soroban_sdk::Symbol = soroban_sdk::symbol_short!("upg_rbok"); -/// Maximum rollback window in seconds (30 days) pub const ROLLBACK_WINDOW_SECONDS: u64 = 86400 * 30; #[contracttype] @@ -37,108 +35,121 @@ pub struct StateBackup { pub struct ContractUpgrader; impl ContractUpgrader { - /// Initialize upgrade system - pub fn initialize(env: &Env) -> Result<(), BridgeError> { + /// Initialize upgrade system + admin + pub fn initialize(env: &Env, admin: Address) -> Result<(), BridgeError> { if env.storage().instance().has(&UPGRADE_VERSION) { return Err(BridgeError::AlreadyInitialized); } - // Set initial version + // Store admin + env.storage().instance().set(&ADMIN, &admin); + env.storage().instance().set(&UPGRADE_VERSION, &1u32); - // Initialize upgrade history let history: Map = Map::new(env); env.storage().instance().set(&UPGRADE_HISTORY, &history); - // No rollback available initially env.storage().instance().set(&ROLLBACK_AVAILABLE, &false); Ok(()) } - /// Prepare for upgrade by backing up current state + /// Internal helper: ensure initialized + fn ensure_initialized(env: &Env) -> Result<(), BridgeError> { + if !env.storage().instance().has(&UPGRADE_VERSION) { + return Err(BridgeError::NotInitialized); + } + Ok(()) + } + + /// Internal helper: get admin safely + fn get_admin(env: &Env) -> Result { + env.storage() + .instance() + .get(&ADMIN) + .ok_or(BridgeError::Unauthorized) + } + + /// Prepare upgrade (backup state) pub fn prepare_upgrade( env: &Env, admin: Address, new_version: u32, state_hash: Bytes, ) -> Result<(), BridgeError> { - #[cfg(not(test))] - admin.require_auth(); + Self::ensure_initialized(env)?; - // Initialize if not already initialized - if !env.storage().instance().has(&UPGRADE_VERSION) { - Self::initialize(env)?; + let stored_admin = Self::get_admin(env)?; + if admin != stored_admin { + return Err(BridgeError::Unauthorized); } - let current_version: u32 = env.storage().instance().get(&UPGRADE_VERSION).unwrap(); + let current_version: u32 = env + .storage() + .instance() + .get(&UPGRADE_VERSION) + .unwrap_or(1); - // Validate version increment if new_version <= current_version { return Err(BridgeError::InvalidInput); } - // Validate state snapshot integrity - if state_hash.is_empty() { + if state_hash.len() == 0 { return Err(BridgeError::InvalidInput); } - // Create state backup let backup = StateBackup { version: current_version, backed_up_at: env.ledger().timestamp(), state_hash: state_hash.clone(), - critical_data: Bytes::new(env), // In practice, serialize critical state here + critical_data: Bytes::new(env), }; env.storage().instance().set(&UPGRADE_STATE_BACKUP, &backup); - - // Mark rollback as available env.storage().instance().set(&ROLLBACK_AVAILABLE, &true); Ok(()) } - /// Execute the upgrade + /// Execute upgrade pub fn execute_upgrade( env: &Env, admin: Address, new_version: u32, migration_hash: Bytes, ) -> Result<(), BridgeError> { - #[cfg(not(test))] - admin.require_auth(); + Self::ensure_initialized(env)?; - // Initialize if not already initialized - if !env.storage().instance().has(&UPGRADE_VERSION) { - Self::initialize(env)?; + let stored_admin = Self::get_admin(env)?; + if admin != stored_admin { + return Err(BridgeError::Unauthorized); } - let current_version: u32 = env.storage().instance().get(&UPGRADE_VERSION).unwrap(); + let current_version: u32 = env + .storage() + .instance() + .get(&UPGRADE_VERSION) + .unwrap_or(1); - // Validate version increment if new_version <= current_version { return Err(BridgeError::InvalidInput); } - // Validate migration metadata - if migration_hash.is_empty() { + if migration_hash.len() == 0 { return Err(BridgeError::InvalidInput); } - // Verify backup exists if !env.storage().instance().has(&UPGRADE_STATE_BACKUP) { return Err(BridgeError::StorageError); } - // Record upgrade in history let mut history: Map = env .storage() .instance() .get(&UPGRADE_HISTORY) .unwrap_or_else(|| Map::new(env)); - let upgrade_record = UpgradeRecord { + let record = UpgradeRecord { version: new_version, upgraded_at: env.ledger().timestamp(), upgraded_by: admin.clone(), @@ -146,21 +157,22 @@ impl ContractUpgrader { migration_hash: migration_hash.clone(), }; - history.set(new_version, upgrade_record); + history.set(new_version, record); env.storage().instance().set(&UPGRADE_HISTORY, &history); - - // Update current version env.storage().instance().set(&UPGRADE_VERSION, &new_version); Ok(()) } - /// Rollback to previous version if within rollback window + /// Rollback pub fn rollback(env: &Env, admin: Address) -> Result<(), BridgeError> { - #[cfg(not(test))] - admin.require_auth(); + Self::ensure_initialized(env)?; + + let stored_admin = Self::get_admin(env)?; + if admin != stored_admin { + return Err(BridgeError::Unauthorized); + } - // Check if rollback is available let rollback_available: bool = env .storage() .instance() @@ -171,35 +183,31 @@ impl ContractUpgrader { return Err(BridgeError::InvalidInput); } - // Get backup - let backup: StateBackup = env.storage().instance().get(&UPGRADE_STATE_BACKUP).unwrap(); + let backup: StateBackup = env + .storage() + .instance() + .get(&UPGRADE_STATE_BACKUP) + .ok_or(BridgeError::StorageError)?; - // Check if within rollback window - let current_time = env.ledger().timestamp(); - if current_time > backup.backed_up_at + ROLLBACK_WINDOW_SECONDS { + let now = env.ledger().timestamp(); + if now > backup.backed_up_at + ROLLBACK_WINDOW_SECONDS { return Err(BridgeError::InvalidInput); } - // Restore previous version env.storage() .instance() .set(&UPGRADE_VERSION, &backup.version); - // Mark rollback as no longer available env.storage().instance().set(&ROLLBACK_AVAILABLE, &false); - - // Clear backup after successful rollback env.storage().instance().remove(&UPGRADE_STATE_BACKUP); Ok(()) } - /// Get current version pub fn get_current_version(env: &Env) -> u32 { env.storage().instance().get(&UPGRADE_VERSION).unwrap_or(1) } - /// Get upgrade history pub fn get_upgrade_history(env: &Env, version: u32) -> Option { let history: Map = env .storage() @@ -210,7 +218,6 @@ impl ContractUpgrader { history.get(version) } - /// Check if rollback is available pub fn is_rollback_available(env: &Env) -> bool { let rollback_available: bool = env .storage() @@ -222,20 +229,18 @@ impl ContractUpgrader { return false; } - // Check if backup exists and is within window if let Some(backup) = env .storage() .instance() .get::<_, StateBackup>(&UPGRADE_STATE_BACKUP) { - let current_time = env.ledger().timestamp(); - current_time <= backup.backed_up_at + ROLLBACK_WINDOW_SECONDS + let now = env.ledger().timestamp(); + now <= backup.backed_up_at + ROLLBACK_WINDOW_SECONDS } else { false } } - /// Get state backup information pub fn get_state_backup(env: &Env) -> Option { env.storage().instance().get(&UPGRADE_STATE_BACKUP) } @@ -248,29 +253,6 @@ mod tests { use soroban_sdk::testutils::Address as _; use soroban_sdk::{Address, Bytes, Env}; - #[test] - fn test_rollback_window_expiry() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register(TeachLinkBridge, ()); - let admin = Address::generate(&env); - - env.as_contract(&contract_id, || { - // Initialize upgrade system - ContractUpgrader::initialize(&env).unwrap(); - - // Prepare and execute upgrade - let state_hash = Bytes::from_slice(&env, b"state_hash"); - ContractUpgrader::prepare_upgrade(&env, admin.clone(), 2, state_hash).unwrap(); - - let migration_hash = Bytes::from_slice(&env, b"migration"); - ContractUpgrader::execute_upgrade(&env, admin.clone(), 2, migration_hash).unwrap(); - - // Verify rollback is available immediately after upgrade - assert!(ContractUpgrader::is_rollback_available(&env)); - }); // closes env.as_contract - } - #[test] fn test_upgrade_lifecycle() { let env = Env::default(); @@ -279,7 +261,7 @@ mod tests { let admin = Address::generate(&env); env.as_contract(&contract_id, || { - ContractUpgrader::initialize(&env).unwrap(); + ContractUpgrader::initialize(&env, admin.clone()).unwrap(); assert_eq!(ContractUpgrader::get_current_version(&env), 1); @@ -306,56 +288,20 @@ mod tests { } #[test] - fn test_prepare_upgrade_auto_initializes() { + fn test_prepare_upgrade_requires_initialization() { let env = Env::default(); env.mock_all_auths(); let contract_id = env.register(TeachLinkBridge, ()); let admin = Address::generate(&env); env.as_contract(&contract_id, || { + ContractUpgrader::initialize(&env, admin.clone()).unwrap(); + let state_hash = Bytes::from_slice(&env, b"state_hash"); ContractUpgrader::prepare_upgrade(&env, admin.clone(), 2, state_hash).unwrap(); - + assert_eq!(ContractUpgrader::get_current_version(&env), 1); assert!(ContractUpgrader::is_rollback_available(&env)); }); } - - #[test] - fn test_rollback_window_expiry() { - use soroban_sdk::testutils::{Ledger, LedgerInfo}; - - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register(TeachLinkBridge, ()); - let admin = Address::generate(&env); - - env.as_contract(&contract_id, || { - // Initialize upgrade system - ContractUpgrader::initialize(&env).unwrap(); - - // Prepare and execute upgrade - let state_hash = Bytes::from_slice(&env, b"state_hash"); - ContractUpgrader::prepare_upgrade(&env, admin.clone(), 2, state_hash).unwrap(); - - let migration_hash = Bytes::from_slice(&env, b"migration"); - ContractUpgrader::execute_upgrade(&env, admin.clone(), 2, migration_hash).unwrap(); - - // Advance ledger past rollback window - let backup = ContractUpgrader::get_state_backup(&env).unwrap(); - env.ledger().set(LedgerInfo { - timestamp: backup.backed_up_at + ROLLBACK_WINDOW_SECONDS + 1, - protocol_version: 25, - sequence_number: 0, - network_id: Default::default(), - base_reserve: 0, - min_temp_entry_ttl: 0, - min_persistent_entry_ttl: 0, - max_entry_ttl: 2_000_000, - }); - - assert!(ContractUpgrader::rollback(&env, admin.clone()).is_err()); - assert!(!ContractUpgrader::is_rollback_available(&env)); - }); - } } diff --git a/contracts/teachlink/test_snapshots/interface_versioning/tests/compatibility_requires_same_major_and_supported_range.1.json b/contracts/teachlink/test_snapshots/interface_versioning/tests/compatibility_requires_same_major_and_supported_range.1.json index 50b8702f..53910e61 100644 --- a/contracts/teachlink/test_snapshots/interface_versioning/tests/compatibility_requires_same_major_and_supported_range.1.json +++ b/contracts/teachlink/test_snapshots/interface_versioning/tests/compatibility_requires_same_major_and_supported_range.1.json @@ -200,6 +200,30 @@ "val": { "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" } + }, + { + "key": { + "symbol": "upg_hist" + }, + "val": { + "map": [] + } + }, + { + "key": { + "symbol": "upg_rbok" + }, + "val": { + "bool": false + } + }, + { + "key": { + "symbol": "upg_ver" + }, + "val": { + "u32": 1 + } } ] } From f8b4e4784e6fafcca864f5e16920289679eda678 Mon Sep 17 00:00:00 2001 From: Timi Date: Sun, 26 Apr 2026 15:30:04 +0100 Subject: [PATCH 6/7] refactor: simplify current version retrieval in contract upgrade logic --- contracts/teachlink/src/upgrade.rs | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/contracts/teachlink/src/upgrade.rs b/contracts/teachlink/src/upgrade.rs index 8e96b611..cfcbc89e 100644 --- a/contracts/teachlink/src/upgrade.rs +++ b/contracts/teachlink/src/upgrade.rs @@ -84,11 +84,7 @@ impl ContractUpgrader { return Err(BridgeError::Unauthorized); } - let current_version: u32 = env - .storage() - .instance() - .get(&UPGRADE_VERSION) - .unwrap_or(1); + let current_version: u32 = env.storage().instance().get(&UPGRADE_VERSION).unwrap_or(1); if new_version <= current_version { return Err(BridgeError::InvalidInput); @@ -125,11 +121,7 @@ impl ContractUpgrader { return Err(BridgeError::Unauthorized); } - let current_version: u32 = env - .storage() - .instance() - .get(&UPGRADE_VERSION) - .unwrap_or(1); + let current_version: u32 = env.storage().instance().get(&UPGRADE_VERSION).unwrap_or(1); if new_version <= current_version { return Err(BridgeError::InvalidInput); @@ -296,10 +288,10 @@ mod tests { env.as_contract(&contract_id, || { ContractUpgrader::initialize(&env, admin.clone()).unwrap(); - + let state_hash = Bytes::from_slice(&env, b"state_hash"); ContractUpgrader::prepare_upgrade(&env, admin.clone(), 2, state_hash).unwrap(); - + assert_eq!(ContractUpgrader::get_current_version(&env), 1); assert!(ContractUpgrader::is_rollback_available(&env)); }); From 1acaf15be7645292f9e166a4bb1fb0a2aa14a516 Mon Sep 17 00:00:00 2001 From: Timi Date: Sun, 26 Apr 2026 15:41:03 +0100 Subject: [PATCH 7/7] refactor: ensure admin parameter is cloned during bridge initialization --- contracts/teachlink/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/teachlink/src/lib.rs b/contracts/teachlink/src/lib.rs index 72d23f2b..b1532781 100644 --- a/contracts/teachlink/src/lib.rs +++ b/contracts/teachlink/src/lib.rs @@ -209,7 +209,7 @@ impl TeachLinkBridge { min_validators: u32, fee_recipient: Address, ) -> Result<(), BridgeError> { - bridge::Bridge::initialize(&env, token, admin, min_validators, fee_recipient)?; + bridge::Bridge::initialize(&env, token, admin.clone(), min_validators, fee_recipient)?; interface_versioning::InterfaceVersioning::initialize(&env); upgrade::ContractUpgrader::initialize(&env, admin.clone())?; Ok(())