From 81f5e8c88f391a8e9415edd2bffdf2ebe58763ed Mon Sep 17 00:00:00 2001 From: david akor Date: Thu, 30 Apr 2026 15:09:03 +0000 Subject: [PATCH] fix(contract): add access control to set_risk_tier (issue #50) - Add initialize(admin) to set a trusted admin address once - set_risk_tier now requires caller == admin OR caller == user - Add get_admin() helper - Add testutils dev-dependency for test utilities - Update all tests to use setup() helper with mock_all_auths - Add 3 new auth tests: admin can set any user, user can set own, stranger cannot overwrite another user's score - 22/22 tests pass, clippy clean, fmt clean Signed-off-by: david akor --- risk_score/Cargo.lock | 36 ++++++ risk_score/Cargo.toml | 3 + risk_score/src/lib.rs | 257 +++++++++++++++++++++++------------------- 3 files changed, 182 insertions(+), 114 deletions(-) diff --git a/risk_score/Cargo.lock b/risk_score/Cargo.lock index 2e8cae1f..de9c376b 100644 --- a/risk_score/Cargo.lock +++ b/risk_score/Cargo.lock @@ -29,6 +29,15 @@ dependencies = [ "libc", ] +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "ark-bls12-381" version = "0.4.0" @@ -286,6 +295,16 @@ dependencies = [ "typenum", ] +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn 2.0.101", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -385,6 +404,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "digest" version = "0.10.7" @@ -1054,6 +1084,7 @@ version = "22.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "027cd856171bfd6ad2c0ffb3b7dfe55ad7080fb3050c36ad20970f80da634472" dependencies = [ + "arbitrary", "crate-git-revision", "ethnum", "num-derive", @@ -1147,7 +1178,11 @@ version = "22.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7ac27d7573e62b745513fa1be8dab7a09b9676a7f39db97164f1d458a344749" dependencies = [ + "arbitrary", "bytes-lit", + "ctor", + "derive_arbitrary", + "ed25519-dalek", "rand", "rustc_version", "serde", @@ -1259,6 +1294,7 @@ version = "22.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ce69db907e64d1e70a3dce8d4824655d154749426a6132b25395c49136013e4" dependencies = [ + "arbitrary", "base64 0.13.1", "crate-git-revision", "escape-bytes", diff --git a/risk_score/Cargo.toml b/risk_score/Cargo.toml index 13824bca..d1732921 100644 --- a/risk_score/Cargo.toml +++ b/risk_score/Cargo.toml @@ -9,6 +9,9 @@ crate-type = ["cdylib"] [dependencies] soroban-sdk = "22.0.8" +[dev-dependencies] +soroban-sdk = { version = "22.0.8", features = ["testutils"] } + [profile.release] opt-level = "z" overflow-checks = true diff --git a/risk_score/src/lib.rs b/risk_score/src/lib.rs index c5f14569..c88b8f11 100644 --- a/risk_score/src/lib.rs +++ b/risk_score/src/lib.rs @@ -16,11 +16,42 @@ pub struct RiskTierData { pub chosen_tier: Symbol, // User's chosen tier for operations } +const ADMIN_KEY: &str = "admin"; + #[contractimpl] impl RiskTierContract { - /// Set risk score with tier classification and timestamp - /// Following Soroban persistent storage best practices with tuple keys - pub fn set_risk_tier(env: Env, user: Address, score: u32, tier: Symbol, chosen_tier: Symbol) { + /// Initialize the contract with a trusted admin address. + /// Can only be called once; subsequent calls panic. + pub fn initialize(env: Env, admin: Address) { + let key = Symbol::new(&env, ADMIN_KEY); + assert!(!env.storage().instance().has(&key), "Already initialized"); + env.storage().instance().set(&key, &admin); + } + + /// Return the current admin address. + pub fn get_admin(env: Env) -> Address { + let key = Symbol::new(&env, ADMIN_KEY); + env.storage().instance().get(&key).expect("Not initialized") + } + + /// Set risk score with tier classification and timestamp. + /// Caller must be the contract admin OR the user themselves. + pub fn set_risk_tier( + env: Env, + caller: Address, + user: Address, + score: u32, + tier: Symbol, + chosen_tier: Symbol, + ) { + // --- Access control --- + caller.require_auth(); + let admin = Self::get_admin(env.clone()); + assert!( + caller == admin || caller == user, + "Unauthorized: caller must be admin or the user" + ); + // Validate inputs assert!(score <= 100, "Score must be 0-100"); assert!( @@ -196,17 +227,80 @@ mod tests { use super::*; use soroban_sdk::{testutils::Address as _, Env}; - #[test] - fn test_set_and_get_risk_tier() { + /// Deploy the contract, initialize it with a fresh admin, and return + /// (env, contract_id, admin). Every test calls this to get a clean slate. + fn setup() -> (Env, soroban_sdk::Address, soroban_sdk::Address) { let env = Env::default(); - let contract_id = env.register_contract(None, RiskTierContract); + env.mock_all_auths(); + let contract_id = env.register(RiskTierContract, ()); + let client = RiskTierContractClient::new(&env, &contract_id); + let admin = Address::generate(&env); + client.initialize(&admin); + (env, contract_id, admin) + } + + // ── initialize ────────────────────────────────────────────────────────── + + #[test] + fn test_initialize_sets_admin() { + let (env, contract_id, admin) = setup(); + let client = RiskTierContractClient::new(&env, &contract_id); + assert_eq!(client.get_admin(), admin); + } + + #[test] + #[should_panic(expected = "Already initialized")] + fn test_initialize_twice_panics() { + let (env, contract_id, _admin) = setup(); let client = RiskTierContractClient::new(&env, &contract_id); + let other = Address::generate(&env); + client.initialize(&other); + } + + // ── access control for set_risk_tier ──────────────────────────────────── + + #[test] + fn test_admin_can_set_any_user_tier() { + let (env, contract_id, admin) = setup(); + let client = RiskTierContractClient::new(&env, &contract_id); + let user = Address::generate(&env); + let tier_1 = Symbol::new(&env, "TIER_1"); + client.set_risk_tier(&admin, &user, &25, &tier_1, &tier_1); + let data = client.get_risk_tier(&user).unwrap(); + assert_eq!(data.score, 25); + } + #[test] + fn test_user_can_set_own_tier() { + let (env, contract_id, _admin) = setup(); + let client = RiskTierContractClient::new(&env, &contract_id); let user = Address::generate(&env); let tier_1 = Symbol::new(&env, "TIER_1"); - - client.set_risk_tier(&user, &25, &tier_1, &tier_1); - + client.set_risk_tier(&user, &user, &20, &tier_1, &tier_1); + let data = client.get_risk_tier(&user).unwrap(); + assert_eq!(data.score, 20); + } + + #[test] + #[should_panic(expected = "Unauthorized")] + fn test_stranger_cannot_set_other_user_tier() { + let (env, contract_id, _admin) = setup(); + let client = RiskTierContractClient::new(&env, &contract_id); + let user = Address::generate(&env); + let stranger = Address::generate(&env); + let tier_1 = Symbol::new(&env, "TIER_1"); + client.set_risk_tier(&stranger, &user, &0, &tier_1, &tier_1); + } + + // ── existing functional tests ──────────────────────────────────────────── + + #[test] + fn test_set_and_get_risk_tier() { + let (env, contract_id, _admin) = setup(); + let client = RiskTierContractClient::new(&env, &contract_id); + let user = Address::generate(&env); + let tier_1 = Symbol::new(&env, "TIER_1"); + client.set_risk_tier(&user, &user, &25, &tier_1, &tier_1); let risk_data = client.get_risk_tier(&user).unwrap(); assert_eq!(risk_data.score, 25); assert_eq!(risk_data.tier, tier_1); @@ -215,183 +309,134 @@ mod tests { #[test] fn test_score_validation_upper_bound() { - let env = Env::default(); - let contract_id = env.register_contract(None, RiskTierContract); + let (env, contract_id, _admin) = setup(); let client = RiskTierContractClient::new(&env, &contract_id); - let user = Address::generate(&env); let tier_3 = Symbol::new(&env, "TIER_3"); - - client.set_risk_tier(&user, &100, &tier_3, &tier_3); - - let score = client.get_score(&user); - assert_eq!(score, 100); + client.set_risk_tier(&user, &user, &100, &tier_3, &tier_3); + assert_eq!(client.get_score(&user), 100); } #[test] #[should_panic(expected = "Score must be 0-100")] fn test_score_validation_exceeds_limit() { - let env = Env::default(); - let contract_id = env.register_contract(None, RiskTierContract); + let (env, contract_id, _admin) = setup(); let client = RiskTierContractClient::new(&env, &contract_id); - let user = Address::generate(&env); let tier_3 = Symbol::new(&env, "TIER_3"); - - client.set_risk_tier(&user, &101, &tier_3, &tier_3); + client.set_risk_tier(&user, &user, &101, &tier_3, &tier_3); } #[test] #[should_panic(expected = "Invalid tier")] fn test_invalid_tier_validation() { - let env = Env::default(); - let contract_id = env.register_contract(None, RiskTierContract); + let (env, contract_id, _admin) = setup(); let client = RiskTierContractClient::new(&env, &contract_id); - let user = Address::generate(&env); let invalid_tier = Symbol::new(&env, "TIER_4"); - - client.set_risk_tier(&user, &50, &invalid_tier, &invalid_tier); + client.set_risk_tier(&user, &user, &50, &invalid_tier, &invalid_tier); } #[test] fn test_tier_access_tier1_low_risk() { - let env = Env::default(); - let contract_id = env.register_contract(None, RiskTierContract); + let (env, contract_id, _admin) = setup(); let client = RiskTierContractClient::new(&env, &contract_id); - let user = Address::generate(&env); let tier_1 = Symbol::new(&env, "TIER_1"); - - client.set_risk_tier(&user, &25, &tier_1, &tier_1); - + client.set_risk_tier(&user, &user, &25, &tier_1, &tier_1); assert!(client.can_access_tier(&user, &tier_1)); } #[test] fn test_tier_access_tier1_boundary() { - let env = Env::default(); - let contract_id = env.register_contract(None, RiskTierContract); + let (env, contract_id, _admin) = setup(); let client = RiskTierContractClient::new(&env, &contract_id); - let user = Address::generate(&env); let tier_1 = Symbol::new(&env, "TIER_1"); - - client.set_risk_tier(&user, &30, &tier_1, &tier_1); - + client.set_risk_tier(&user, &user, &30, &tier_1, &tier_1); assert!(client.can_access_tier(&user, &tier_1)); } #[test] fn test_tier_access_tier1_denied() { - let env = Env::default(); - let contract_id = env.register_contract(None, RiskTierContract); + let (env, contract_id, _admin) = setup(); let client = RiskTierContractClient::new(&env, &contract_id); - let user = Address::generate(&env); let tier_2 = Symbol::new(&env, "TIER_2"); let tier_1 = Symbol::new(&env, "TIER_1"); - - client.set_risk_tier(&user, &50, &tier_2, &tier_2); - + client.set_risk_tier(&user, &user, &50, &tier_2, &tier_2); assert!(!client.can_access_tier(&user, &tier_1)); } #[test] fn test_tier_access_tier2_medium_risk() { - let env = Env::default(); - let contract_id = env.register_contract(None, RiskTierContract); + let (env, contract_id, _admin) = setup(); let client = RiskTierContractClient::new(&env, &contract_id); - let user = Address::generate(&env); let tier_2 = Symbol::new(&env, "TIER_2"); - - client.set_risk_tier(&user, &50, &tier_2, &tier_2); - + client.set_risk_tier(&user, &user, &50, &tier_2, &tier_2); assert!(client.can_access_tier(&user, &tier_2)); } #[test] fn test_tier_access_tier3_always_accessible() { - let env = Env::default(); - let contract_id = env.register_contract(None, RiskTierContract); + let (env, contract_id, _admin) = setup(); let client = RiskTierContractClient::new(&env, &contract_id); - let user = Address::generate(&env); let tier_3 = Symbol::new(&env, "TIER_3"); - - client.set_risk_tier(&user, &85, &tier_3, &tier_3); - + client.set_risk_tier(&user, &user, &85, &tier_3, &tier_3); assert!(client.can_access_tier(&user, &tier_3)); } #[test] fn test_get_tier_users() { - let env = Env::default(); - let contract_id = env.register_contract(None, RiskTierContract); + let (env, contract_id, _admin) = setup(); let client = RiskTierContractClient::new(&env, &contract_id); - let user1 = Address::generate(&env); let user2 = Address::generate(&env); let tier_1 = Symbol::new(&env, "TIER_1"); - - client.set_risk_tier(&user1, &20, &tier_1, &tier_1); - client.set_risk_tier(&user2, &25, &tier_1, &tier_1); - - let tier_users = client.get_tier_users(&tier_1); - assert_eq!(tier_users.len(), 2); + client.set_risk_tier(&user1, &user1, &20, &tier_1, &tier_1); + client.set_risk_tier(&user2, &user2, &25, &tier_1, &tier_1); + assert_eq!(client.get_tier_users(&tier_1).len(), 2); } #[test] fn test_update_chosen_tier_valid() { - let env = Env::default(); - let contract_id = env.register_contract(None, RiskTierContract); + let (env, contract_id, _admin) = setup(); let client = RiskTierContractClient::new(&env, &contract_id); - let user = Address::generate(&env); let tier_1 = Symbol::new(&env, "TIER_1"); let tier_2 = Symbol::new(&env, "TIER_2"); - - client.set_risk_tier(&user, &25, &tier_1, &tier_1); + client.set_risk_tier(&user, &user, &25, &tier_1, &tier_1); client.update_chosen_tier(&user, &tier_2); - - let chosen = client.get_chosen_tier(&user); - assert_eq!(chosen, tier_2); + assert_eq!(client.get_chosen_tier(&user), tier_2); } #[test] #[should_panic(expected = "High risk users can only access TIER_3")] fn test_update_chosen_tier_high_risk_restriction() { - let env = Env::default(); - let contract_id = env.register_contract(None, RiskTierContract); + let (env, contract_id, _admin) = setup(); let client = RiskTierContractClient::new(&env, &contract_id); - let user = Address::generate(&env); let tier_3 = Symbol::new(&env, "TIER_3"); let tier_1 = Symbol::new(&env, "TIER_1"); - - client.set_risk_tier(&user, &85, &tier_3, &tier_3); + client.set_risk_tier(&user, &user, &85, &tier_3, &tier_3); client.update_chosen_tier(&user, &tier_1); } #[test] fn test_get_tier_stats() { - let env = Env::default(); - let contract_id = env.register_contract(None, RiskTierContract); + let (env, contract_id, _admin) = setup(); let client = RiskTierContractClient::new(&env, &contract_id); - let user1 = Address::generate(&env); let user2 = Address::generate(&env); let user3 = Address::generate(&env); - let tier_1 = Symbol::new(&env, "TIER_1"); let tier_2 = Symbol::new(&env, "TIER_2"); let tier_3 = Symbol::new(&env, "TIER_3"); - - client.set_risk_tier(&user1, &20, &tier_1, &tier_1); - client.set_risk_tier(&user2, &50, &tier_2, &tier_2); - client.set_risk_tier(&user3, &80, &tier_3, &tier_3); - + client.set_risk_tier(&user1, &user1, &20, &tier_1, &tier_1); + client.set_risk_tier(&user2, &user2, &50, &tier_2, &tier_2); + client.set_risk_tier(&user3, &user3, &80, &tier_3, &tier_3); let stats = client.get_tier_stats(); assert_eq!(stats.get(tier_1).unwrap(), 1); assert_eq!(stats.get(tier_2).unwrap(), 1); @@ -400,17 +445,13 @@ mod tests { #[test] fn test_score_update_overwrites_previous() { - let env = Env::default(); - let contract_id = env.register_contract(None, RiskTierContract); + let (env, contract_id, _admin) = setup(); let client = RiskTierContractClient::new(&env, &contract_id); - let user = Address::generate(&env); let tier_2 = Symbol::new(&env, "TIER_2"); let tier_1 = Symbol::new(&env, "TIER_1"); - - client.set_risk_tier(&user, &50, &tier_2, &tier_2); - client.set_risk_tier(&user, &25, &tier_1, &tier_1); - + client.set_risk_tier(&user, &user, &50, &tier_2, &tier_2); + client.set_risk_tier(&user, &user, &25, &tier_1, &tier_1); let risk_data = client.get_risk_tier(&user).unwrap(); assert_eq!(risk_data.score, 25); assert_eq!(risk_data.tier, tier_1); @@ -418,46 +459,34 @@ mod tests { #[test] fn test_no_risk_data_returns_zero_score() { - let env = Env::default(); - let contract_id = env.register_contract(None, RiskTierContract); + let (env, contract_id, _admin) = setup(); let client = RiskTierContractClient::new(&env, &contract_id); - let user = Address::generate(&env); - - let score = client.get_score(&user); - assert_eq!(score, 0); + assert_eq!(client.get_score(&user), 0); } #[test] fn test_no_risk_data_denies_tier_access() { - let env = Env::default(); - let contract_id = env.register_contract(None, RiskTierContract); + let (env, contract_id, _admin) = setup(); let client = RiskTierContractClient::new(&env, &contract_id); - let user = Address::generate(&env); let tier_3 = Symbol::new(&env, "TIER_3"); - assert!(!client.can_access_tier(&user, &tier_3)); } #[test] fn test_multiple_users_different_tiers() { - let env = Env::default(); - let contract_id = env.register_contract(None, RiskTierContract); + let (env, contract_id, _admin) = setup(); let client = RiskTierContractClient::new(&env, &contract_id); - let user1 = Address::generate(&env); let user2 = Address::generate(&env); let user3 = Address::generate(&env); - let tier_1 = Symbol::new(&env, "TIER_1"); let tier_2 = Symbol::new(&env, "TIER_2"); let tier_3 = Symbol::new(&env, "TIER_3"); - - client.set_risk_tier(&user1, &15, &tier_1, &tier_1); - client.set_risk_tier(&user2, &45, &tier_2, &tier_2); - client.set_risk_tier(&user3, &90, &tier_3, &tier_3); - + client.set_risk_tier(&user1, &user1, &15, &tier_1, &tier_1); + client.set_risk_tier(&user2, &user2, &45, &tier_2, &tier_2); + client.set_risk_tier(&user3, &user3, &90, &tier_3, &tier_3); assert!(client.can_access_tier(&user1, &tier_1)); assert!(!client.can_access_tier(&user2, &tier_1)); assert!(client.can_access_tier(&user2, &tier_2));