From a7f8950a2695efb5dbe4925b96b033fdc8ab7004 Mon Sep 17 00:00:00 2001 From: dubemoyibe-star Date: Sun, 19 Jul 2026 11:27:41 +0100 Subject: [PATCH] feat: implement timelocked emergency admin controls --- betting/src/lib.rs | 128 ++++++++++++++++++----------- oracle/src/lib.rs | 75 +++++++++++------ oracle/src/test.rs | 120 ++++++++++++++-------------- rewards/src/lib.rs | 116 +++++++++++++++++---------- vault/src/lib.rs | 195 +++++++++++++++++++++++++++++++++------------ vault/src/test.rs | 154 ++++++++++++++++++----------------- 6 files changed, 493 insertions(+), 295 deletions(-) diff --git a/betting/src/lib.rs b/betting/src/lib.rs index 020f660..9531727 100644 --- a/betting/src/lib.rs +++ b/betting/src/lib.rs @@ -27,10 +27,8 @@ //! - `refund_bet` : user pulls a refund after the deadline if no settlement //! - `claim_payout` : user pulls parimutuel winnings once the match is settled -use soroban_sdk::{ - contract, contractimpl, contracttype, token, Address, BytesN, Env, Symbol, Vec, -}; use renaissance_core::PlatformError; +use soroban_sdk::{contract, contractimpl, contracttype, token, Address, BytesN, Env, Symbol, Vec}; // ── Outcomes ────────────────────────────────────────────────────────────────── @@ -139,7 +137,12 @@ impl RenaissanceBettingContract { } fn ensure_not_paused(env: &Env) -> Result<(), PlatformError> { - if env.storage().instance().get(&DataKey::Paused).unwrap_or(false) { + if env + .storage() + .instance() + .get(&DataKey::Paused) + .unwrap_or(false) + { return Err(PlatformError::Paused); } Ok(()) @@ -155,16 +158,26 @@ impl RenaissanceBettingContract { return Err(PlatformError::Unauthorized); } env.storage().instance().set(&DataKey::Admin, &admin); - env.storage().instance().set(&DataKey::PlatformAdmin, &admin); - env.storage().instance().set(&DataKey::SecurityAdmin, &admin); - env.storage().instance().set(&DataKey::TreasuryAdmin, &admin); + env.storage() + .instance() + .set(&DataKey::PlatformAdmin, &admin); + env.storage() + .instance() + .set(&DataKey::SecurityAdmin, &admin); + env.storage() + .instance() + .set(&DataKey::TreasuryAdmin, &admin); env.storage().instance().set(&DataKey::Paused, &false); env.events() .publish((Symbol::new(&env, "ContractInitialized"),), admin); Ok(()) } - pub fn upgrade(env: Env, caller: Address, new_wasm_hash: BytesN<32>) -> Result<(), PlatformError> { + pub fn upgrade( + env: Env, + caller: Address, + new_wasm_hash: BytesN<32>, + ) -> Result<(), PlatformError> { let platform: Address = env .storage() .instance() @@ -196,13 +209,20 @@ impl RenaissanceBettingContract { approvals.push_back(caller.clone()); if approvals.len() >= 2 { - env.storage().instance().set(&DataKey::WasmHash, &new_wasm_hash); - env.deployer().update_current_contract_wasm(new_wasm_hash.clone()); - env.storage().instance().set(&DataKey::UpgradeApprovals, &Vec::
::new(&env)); + env.storage() + .instance() + .set(&DataKey::WasmHash, &new_wasm_hash); + env.deployer() + .update_current_contract_wasm(new_wasm_hash.clone()); + env.storage() + .instance() + .set(&DataKey::UpgradeApprovals, &Vec::
::new(&env)); env.events() .publish((Symbol::new(&env, "Upgraded"),), new_wasm_hash); } else { - env.storage().instance().set(&DataKey::UpgradeApprovals, &approvals); + env.storage() + .instance() + .set(&DataKey::UpgradeApprovals, &approvals); } Ok(()) } @@ -210,7 +230,12 @@ impl RenaissanceBettingContract { pub fn pause(env: Env) -> Result<(), PlatformError> { let admin = Self::require_admin(&env)?; admin.require_auth(); - if env.storage().instance().get(&DataKey::Paused).unwrap_or(false) { + if env + .storage() + .instance() + .get(&DataKey::Paused) + .unwrap_or(false) + { return Ok(()); } env.storage().instance().set(&DataKey::Paused, &true); @@ -222,12 +247,16 @@ impl RenaissanceBettingContract { let admin = Self::require_admin(&env)?; admin.require_auth(); env.storage().instance().set(&DataKey::Paused, &false); - env.events().publish((Symbol::new(&env, "Unpaused"),), admin); + env.events() + .publish((Symbol::new(&env, "Unpaused"),), admin); Ok(()) } pub fn is_paused(env: Env) -> bool { - env.storage().instance().get(&DataKey::Paused).unwrap_or(false) + env.storage() + .instance() + .get(&DataKey::Paused) + .unwrap_or(false) } /// Define a new match id with its authorized oracle, settlement token, @@ -273,17 +302,15 @@ impl RenaissanceBettingContract { .instance() .set(&DataKey::Stats(match_id), &stats); - env.events() - .publish((Symbol::new(&env, "MatchRegistered"), admin), (match_id, oracle, token, deadline)); + env.events().publish( + (Symbol::new(&env, "MatchRegistered"), admin), + (match_id, oracle, token, deadline), + ); Ok(()) } /// Replace the oracle of an unsettled match. Admin only. - pub fn set_oracle( - env: Env, - match_id: u64, - new_oracle: Address, - ) -> Result<(), PlatformError> { + pub fn set_oracle(env: Env, match_id: u64, new_oracle: Address) -> Result<(), PlatformError> { Self::ensure_not_paused(&env)?; let admin: Address = env .storage() @@ -370,9 +397,10 @@ impl RenaissanceBettingContract { .ok_or(PlatformError::InternalError)?; let idx = outcome.index(); let prev = stats.pools.get(idx).unwrap_or(0); - stats - .pools - .set(idx, prev.checked_add(amount).ok_or(PlatformError::Overflow)?); + stats.pools.set( + idx, + prev.checked_add(amount).ok_or(PlatformError::Overflow)?, + ); stats.bettors.push_back(user.clone()); env.storage() .instance() @@ -432,11 +460,7 @@ impl RenaissanceBettingContract { /// ``` /// /// Emits `BetClaimed` with the paid amount. - pub fn claim_payout( - env: Env, - user: Address, - match_id: u64, - ) -> Result { + pub fn claim_payout(env: Env, user: Address, match_id: u64) -> Result { Self::ensure_not_paused(&env)?; user.require_auth(); let key = DataKey::Bet(match_id, user.clone()); @@ -504,21 +528,15 @@ impl RenaissanceBettingContract { let token_client = token::Client::new(&env, &m.token); token_client.transfer(&env.current_contract_address(), &user, &payout); - env.events().publish( - (Symbol::new(&env, "BetClaimed"), user), - (match_id, payout), - ); + env.events() + .publish((Symbol::new(&env, "BetClaimed"), user), (match_id, payout)); Ok(payout) } /// Refund the user's full bet amount if the match has passed its deadline /// without being settled. Idempotent per `(user, match_id)`. /// Emits `BetRefunded`. - pub fn refund_bet( - env: Env, - user: Address, - match_id: u64, - ) -> Result<(), PlatformError> { + pub fn refund_bet(env: Env, user: Address, match_id: u64) -> Result<(), PlatformError> { Self::ensure_not_paused(&env)?; user.require_auth(); let key = DataKey::Bet(match_id, user.clone()); @@ -557,7 +575,12 @@ impl RenaissanceBettingContract { /// Look up a user's bet on a match. Returns `None` if the bet does /// not exist. pub fn get_bet(env: Env, user: Address, match_id: u64) -> Option { - if env.storage().instance().get(&DataKey::Paused).unwrap_or(false) { + if env + .storage() + .instance() + .get(&DataKey::Paused) + .unwrap_or(false) + { return None; } env.storage() @@ -568,7 +591,12 @@ impl RenaissanceBettingContract { /// Look up a match descriptor. Returns `None` if the match id is /// not registered. pub fn get_match(env: Env, match_id: u64) -> Option { - if env.storage().instance().get(&DataKey::Paused).unwrap_or(false) { + if env + .storage() + .instance() + .get(&DataKey::Paused) + .unwrap_or(false) + { return None; } env.storage().instance().get(&DataKey::Match(match_id)) @@ -618,9 +646,15 @@ mod test { let approver_a = Address::generate(&env); let approver_b = Address::generate(&env); env.as_contract(&contract_id, || { - env.storage().instance().set(&DataKey::PlatformAdmin, &approver_a); - env.storage().instance().set(&DataKey::SecurityAdmin, &approver_b); - env.storage().instance().set(&DataKey::TreasuryAdmin, &admin); + env.storage() + .instance() + .set(&DataKey::PlatformAdmin, &approver_a); + env.storage() + .instance() + .set(&DataKey::SecurityAdmin, &approver_b); + env.storage() + .instance() + .set(&DataKey::TreasuryAdmin, &admin); }); let new_hash = BytesN::from_array(&env, &[9; 32]); @@ -763,8 +797,7 @@ mod test { client.register_match(&8u64, &oracle, &token, &deadline_in(&env, 3_600)); let user = Address::generate(&env); mint(&env, &token, &user, 100); - let res = - client.try_place_bet(&user, &8u64, &Outcome::HomeWin, &500i128); + let res = client.try_place_bet(&user, &8u64, &Outcome::HomeWin, &500i128); match res { Err(Ok(e)) => assert_eq!(e, PlatformError::InsufficientBalance), _ => panic!("expected InsufficientBalance contract error"), @@ -780,8 +813,7 @@ mod test { mint(&env, &token, &user, 1_000); env.ledger().set_timestamp(deadline_in(&env, 3_600) + 1); - let res = - client.try_place_bet(&user, &9u64, &Outcome::HomeWin, &100i128); + let res = client.try_place_bet(&user, &9u64, &Outcome::HomeWin, &100i128); match res { Err(Ok(e)) => assert_eq!(e, PlatformError::ExpiredDeadline), _ => panic!("expected ExpiredDeadline contract error"), diff --git a/oracle/src/lib.rs b/oracle/src/lib.rs index 420de80..8c45304 100644 --- a/oracle/src/lib.rs +++ b/oracle/src/lib.rs @@ -90,7 +90,7 @@ impl FootballOracleContract { .instance() .get(&DataKey::Oracles) .ok_or(PlatformError::Unauthorized)?; - + let caller = env.current_contract_address(); if !oracles.contains(&caller) { return Err(PlatformError::Unauthorized); @@ -100,7 +100,12 @@ impl FootballOracleContract { /// Helper to check that contract is not paused fn ensure_not_paused(env: &Env) -> Result<(), PlatformError> { - if env.storage().instance().get(&DataKey::Paused).unwrap_or(false) { + if env + .storage() + .instance() + .get(&DataKey::Paused) + .unwrap_or(false) + { return Err(PlatformError::Paused); } Ok(()) @@ -108,7 +113,11 @@ impl FootballOracleContract { /// One-time initialization of the oracle contract. /// Sets up the admin and initial list of authorized oracles. - pub fn initialize(env: Env, admin: Address, initial_oracles: Vec
) -> Result<(), PlatformError> { + pub fn initialize( + env: Env, + admin: Address, + initial_oracles: Vec
, + ) -> Result<(), PlatformError> { admin.require_auth(); if env.storage().instance().has(&DataKey::Admin) { @@ -121,14 +130,14 @@ impl FootballOracleContract { } env.storage().instance().set(&DataKey::Admin, &admin); - env.storage().instance().set(&DataKey::Oracles, &initial_oracles); + env.storage() + .instance() + .set(&DataKey::Oracles, &initial_oracles); env.storage().instance().set(&DataKey::Paused, &false); // Extend TTL for instance storage const MAX_TTL: u32 = 518400; // 30 days in ledgers (~5s per ledger) - env.storage() - .instance() - .extend_ttl(MAX_TTL, MAX_TTL); + env.storage().instance().extend_ttl(MAX_TTL, MAX_TTL); Ok(()) } @@ -168,23 +177,31 @@ impl FootballOracleContract { finished_at: u64, ) -> Result<(), OracleError> { Self::ensure_not_paused(&env).map_err(|_| OracleError::OracleUnauthorized)?; - + let oracles = Self::ensure_oracle(&env).map_err(|_| OracleError::OracleUnauthorized)?; oracle.require_auth(); let caller = oracle; - + // Validate caller is in the oracle list if !oracles.contains(&caller) { return Err(OracleError::OracleUnauthorized); } // Check if match is already finalized - if env.storage().persistent().has(&DataKey::FinalizedResult(match_id)) { + if env + .storage() + .persistent() + .has(&DataKey::FinalizedResult(match_id)) + { return Err(OracleError::MatchAlreadyFinalized); } // Check if result is already pending - if env.storage().persistent().has(&DataKey::PendingResult(match_id)) { + if env + .storage() + .persistent() + .has(&DataKey::PendingResult(match_id)) + { return Err(OracleError::ResultAlreadyExists); } @@ -210,8 +227,10 @@ impl FootballOracleContract { .set(&DataKey::PendingResult(match_id), &pending); // Emit ResultSubmitted event - env.events() - .publish((get_event_topic_by_string(&env, "ResultSubmitted"),), (match_id, caller)); + env.events().publish( + (get_event_topic_by_string(&env, "ResultSubmitted"),), + (match_id, caller), + ); Ok(()) } @@ -220,18 +239,22 @@ impl FootballOracleContract { /// When the second confirmation is received, the result is finalized. pub fn confirm_result(env: Env, oracle: Address, match_id: u64) -> Result<(), OracleError> { Self::ensure_not_paused(&env).map_err(|_| OracleError::OracleUnauthorized)?; - + let oracles = Self::ensure_oracle(&env).map_err(|_| OracleError::OracleUnauthorized)?; oracle.require_auth(); let caller = oracle; - + // Validate caller is in the oracle list if !oracles.contains(&caller) { return Err(OracleError::OracleUnauthorized); } // Check if match is already finalized - if env.storage().persistent().has(&DataKey::FinalizedResult(match_id)) { + if env + .storage() + .persistent() + .has(&DataKey::FinalizedResult(match_id)) + { return Err(OracleError::MatchAlreadyFinalized); } @@ -259,8 +282,10 @@ impl FootballOracleContract { .set(&DataKey::PendingResult(match_id), &pending); // Emit ResultConfirmed event - env.events() - .publish((get_event_topic_by_string(&env, "ResultConfirmed"),), (match_id, caller)); + env.events().publish( + (get_event_topic_by_string(&env, "ResultConfirmed"),), + (match_id, caller), + ); // Check if we have enough confirmations (2-of-N, so at least 1 confirmation since submitter is one) if pending.confirmations.len() >= 1 { @@ -284,8 +309,10 @@ impl FootballOracleContract { .remove(&DataKey::PendingResult(match_id)); // Emit ResultFinalized event - env.events() - .publish((get_event_topic_by_string(&env, "ResultFinalized"),), match_id); + env.events().publish( + (get_event_topic_by_string(&env, "ResultFinalized"),), + match_id, + ); } Ok(()) @@ -298,13 +325,15 @@ impl FootballOracleContract { .persistent() .get(&DataKey::FinalizedResult(match_id)) .ok_or(OracleError::ResultNotFound)?; - + Ok(result) } /// Check if a match result is finalized pub fn is_finalized(env: Env, match_id: u64) -> bool { - env.storage().persistent().has(&DataKey::FinalizedResult(match_id)) + env.storage() + .persistent() + .has(&DataKey::FinalizedResult(match_id)) } /// Get the list of all authorized oracles @@ -317,4 +346,4 @@ impl FootballOracleContract { } #[cfg(test)] -mod test; \ No newline at end of file +mod test; diff --git a/oracle/src/test.rs b/oracle/src/test.rs index b08c534..70802c5 100644 --- a/oracle/src/test.rs +++ b/oracle/src/test.rs @@ -1,8 +1,8 @@ #![cfg(test)] use super::*; -use soroban_sdk::{testutils::AddressEnvTestUtils, Address, Env, Vec}; use soroban_sdk::testutils::Ledger; +use soroban_sdk::{testutils::AddressEnvTestUtils, Address, Env, Vec}; #[test] fn test_initialize() { @@ -10,18 +10,18 @@ fn test_initialize() { let admin = Address::generate(&env); let oracle1 = Address::generate(&env); let oracle2 = Address::generate(&env); - + let mut oracles = Vec::new(&env); oracles.push_back(oracle1); oracles.push_back(oracle2); - + // Register the contract let contract_id = env.register_contract(None, FootballOracleContract); let client = FootballOracleContractClient::new(&env, &contract_id); - + // Initialize client.initialize(&admin, &oracles); - + // Check that oracles are correctly set let stored_oracles = client.get_oracles(); assert_eq!(stored_oracles.len(), 2); @@ -35,13 +35,13 @@ fn test_initialize_insufficient_oracles() { let env = Env::default(); let admin = Address::generate(&env); let oracle1 = Address::generate(&env); - + let mut oracles = Vec::new(&env); oracles.push_back(oracle1); // Only one oracle - should fail - + let contract_id = env.register_contract(None, FootballOracleContract); let client = FootballOracleContractClient::new(&env, &contract_id); - + client.initialize(&admin, &oracles); } @@ -51,28 +51,28 @@ fn test_submit_result_success() { let admin = Address::generate(&env); let oracle1 = Address::generate(&env); let oracle2 = Address::generate(&env); - + let mut oracles = Vec::new(&env); oracles.push_back(oracle1.clone()); oracles.push_back(oracle2.clone()); - + let contract_id = env.register_contract(None, FootballOracleContract); let client = FootballOracleContractClient::new(&env, &contract_id); - + client.initialize(&admin, &oracles); - + // Set ledger timestamp let now = 1620000000; env.ledger().set_timestamp(now); - + // Submit a valid result let match_id = 123; let started_at = now - 3600; // 1 hour ago - let finished_at = now - 600; // 10 minutes ago - + let finished_at = now - 600; // 10 minutes ago + oracle1.require_auth(); client.submit_result(&match_id, &2, &1, &started_at, &finished_at); - + // Result shouldn't be finalized yet (needs second confirmation) assert!(!client.is_finalized(&match_id)); } @@ -85,20 +85,20 @@ fn test_submit_result_unauthorized() { let oracle1 = Address::generate(&env); let oracle2 = Address::generate(&env); let unauthorized = Address::generate(&env); - + let mut oracles = Vec::new(&env); oracles.push_back(oracle1); oracles.push_back(oracle2); - + let contract_id = env.register_contract(None, FootballOracleContract); let client = FootballOracleContractClient::new(&env, &contract_id); - + client.initialize(&admin, &oracles); - + // Set ledger timestamp let now = 1620000000; env.ledger().set_timestamp(now); - + // Unauthorized address tries to submit let match_id = 123; client.submit_result(&match_id, &2, &1, &(now - 3600), &(now - 600)); @@ -111,25 +111,25 @@ fn test_submit_result_late_submission() { let admin = Address::generate(&env); let oracle1 = Address::generate(&env); let oracle2 = Address::generate(&env); - + let mut oracles = Vec::new(&env); oracles.push_back(oracle1.clone()); oracles.push_back(oracle2.clone()); - + let contract_id = env.register_contract(None, FootballOracleContract); let client = FootballOracleContractClient::new(&env, &contract_id); - + client.initialize(&admin, &oracles); - + // Set ledger timestamp let now = 1620000000; env.ledger().set_timestamp(now); - + // Try to submit with finished_at in the future let match_id = 123; - let started_at = now + 3600; // Starts in 1 hour + let started_at = now + 3600; // Starts in 1 hour let finished_at = now + 7200; // Finishes in 2 hours - + oracle1.require_auth(); client.submit_result(&match_id, &2, &1, &started_at, &finished_at); } @@ -141,28 +141,28 @@ fn test_double_submit() { let admin = Address::generate(&env); let oracle1 = Address::generate(&env); let oracle2 = Address::generate(&env); - + let mut oracles = Vec::new(&env); oracles.push_back(oracle1.clone()); oracles.push_back(oracle2.clone()); - + let contract_id = env.register_contract(None, FootballOracleContract); let client = FootballOracleContractClient::new(&env, &contract_id); - + client.initialize(&admin, &oracles); - + // Set ledger timestamp let now = 1620000000; env.ledger().set_timestamp(now); - + let match_id = 123; let started_at = now - 3600; let finished_at = now - 600; - + // First submission from oracle1 oracle1.require_auth(); client.submit_result(&match_id, &2, &1, &started_at, &finished_at); - + // Second submission from oracle2 for the same match - should fail oracle2.require_auth(); client.submit_result(&match_id, &3, &1, &started_at, &finished_at); @@ -174,35 +174,35 @@ fn test_confirm_and_finalize() { let admin = Address::generate(&env); let oracle1 = Address::generate(&env); let oracle2 = Address::generate(&env); - + let mut oracles = Vec::new(&env); oracles.push_back(oracle1.clone()); oracles.push_back(oracle2.clone()); - + let contract_id = env.register_contract(None, FootballOracleContract); let client = FootballOracleContractClient::new(&env, &contract_id); - + client.initialize(&admin, &oracles); - + // Set ledger timestamp let now = 1620000000; env.ledger().set_timestamp(now); - + let match_id = 123; let started_at = now - 3600; let finished_at = now - 600; - + // Submit from oracle1 oracle1.require_auth(); client.submit_result(&match_id, &2, &1, &started_at, &finished_at); - + // Confirm from oracle2 - this should finalize oracle2.require_auth(); client.confirm_result(&match_id); - + // Check if finalized assert!(client.is_finalized(&match_id)); - + // Get the result and verify let result = client.get_result(&match_id); assert_eq!(result.home_score, 2); @@ -218,28 +218,28 @@ fn test_cannot_confirm_own_submission() { let admin = Address::generate(&env); let oracle1 = Address::generate(&env); let oracle2 = Address::generate(&env); - + let mut oracles = Vec::new(&env); oracles.push_back(oracle1.clone()); oracles.push_back(oracle2.clone()); - + let contract_id = env.register_contract(None, FootballOracleContract); let client = FootballOracleContractClient::new(&env, &contract_id); - + client.initialize(&admin, &oracles); - + // Set ledger timestamp let now = 1620000000; env.ledger().set_timestamp(now); - + let match_id = 123; let started_at = now - 3600; let finished_at = now - 600; - + // Submit from oracle1 oracle1.require_auth(); client.submit_result(&match_id, &2, &1, &started_at, &finished_at); - + // Try to confirm own submission - should fail oracle1.require_auth(); client.confirm_result(&match_id); @@ -253,34 +253,34 @@ fn test_double_confirm() { let oracle1 = Address::generate(&env); let oracle2 = Address::generate(&env); let oracle3 = Address::generate(&env); - + let mut oracles = Vec::new(&env); oracles.push_back(oracle1.clone()); oracles.push_back(oracle2.clone()); oracles.push_back(oracle3.clone()); - + let contract_id = env.register_contract(None, FootballOracleContract); let client = FootballOracleContractClient::new(&env, &contract_id); - + client.initialize(&admin, &oracles); - + // Set ledger timestamp let now = 1620000000; env.ledger().set_timestamp(now); - + let match_id = 123; let started_at = now - 3600; let finished_at = now - 600; - + // Submit from oracle1 oracle1.require_auth(); client.submit_result(&match_id, &2, &1, &started_at, &finished_at); - + // Confirm from oracle2 oracle2.require_auth(); client.confirm_result(&match_id); - + // Try to confirm again from oracle2 - should fail oracle2.require_auth(); client.confirm_result(&match_id); -} \ No newline at end of file +} diff --git a/rewards/src/lib.rs b/rewards/src/lib.rs index 9ec613d..44eb9ba 100644 --- a/rewards/src/lib.rs +++ b/rewards/src/lib.rs @@ -55,7 +55,12 @@ impl FanRewardsContract { } fn ensure_not_paused(env: &Env) -> Result<(), PlatformError> { - if env.storage().instance().get(&DataKey::Paused).unwrap_or(false) { + if env + .storage() + .instance() + .get(&DataKey::Paused) + .unwrap_or(false) + { return Err(PlatformError::Paused); } Ok(()) @@ -75,9 +80,15 @@ impl FanRewardsContract { } env.storage().instance().set(&DataKey::Admin, &admin); - env.storage().instance().set(&DataKey::PlatformAdmin, &admin); - env.storage().instance().set(&DataKey::SecurityAdmin, &admin); - env.storage().instance().set(&DataKey::TreasuryAdmin, &admin); + env.storage() + .instance() + .set(&DataKey::PlatformAdmin, &admin); + env.storage() + .instance() + .set(&DataKey::SecurityAdmin, &admin); + env.storage() + .instance() + .set(&DataKey::TreasuryAdmin, &admin); env.storage().instance().set(&DataKey::Paused, &false); // Re-bump instance storage TTL so config survives long inactivity. env.storage() @@ -87,7 +98,11 @@ impl FanRewardsContract { Ok(()) } - pub fn upgrade(env: Env, caller: Address, new_wasm_hash: BytesN<32>) -> Result<(), PlatformError> { + pub fn upgrade( + env: Env, + caller: Address, + new_wasm_hash: BytesN<32>, + ) -> Result<(), PlatformError> { let platform: Address = env .storage() .instance() @@ -119,13 +134,20 @@ impl FanRewardsContract { approvals.push_back(caller.clone()); if approvals.len() >= 2 { - env.storage().instance().set(&DataKey::WasmHash, &new_wasm_hash); - env.deployer().update_current_contract_wasm(new_wasm_hash.clone()); - env.storage().instance().set(&DataKey::UpgradeApprovals, &Vec::
::new(&env)); + env.storage() + .instance() + .set(&DataKey::WasmHash, &new_wasm_hash); + env.deployer() + .update_current_contract_wasm(new_wasm_hash.clone()); + env.storage() + .instance() + .set(&DataKey::UpgradeApprovals, &Vec::
::new(&env)); env.events() .publish((Symbol::new(&env, "Upgraded"),), new_wasm_hash); } else { - env.storage().instance().set(&DataKey::UpgradeApprovals, &approvals); + env.storage() + .instance() + .set(&DataKey::UpgradeApprovals, &approvals); } Ok(()) } @@ -133,7 +155,12 @@ impl FanRewardsContract { pub fn pause(env: Env) -> Result<(), PlatformError> { let admin = Self::require_admin(&env)?; admin.require_auth(); - if env.storage().instance().get(&DataKey::Paused).unwrap_or(false) { + if env + .storage() + .instance() + .get(&DataKey::Paused) + .unwrap_or(false) + { return Ok(()); } env.storage().instance().set(&DataKey::Paused, &true); @@ -145,12 +172,16 @@ impl FanRewardsContract { let admin = Self::require_admin(&env)?; admin.require_auth(); env.storage().instance().set(&DataKey::Paused, &false); - env.events().publish((Symbol::new(&env, "Unpaused"),), admin); + env.events() + .publish((Symbol::new(&env, "Unpaused"),), admin); Ok(()) } pub fn is_paused(env: Env) -> bool { - env.storage().instance().get(&DataKey::Paused).unwrap_or(false) + env.storage() + .instance() + .get(&DataKey::Paused) + .unwrap_or(false) } /// Return the currently configured platform admin, if any. @@ -187,9 +218,7 @@ impl FanRewardsContract { let key = DataKey::Balance(user.clone()); let current: i128 = env.storage().persistent().get(&key).unwrap_or(0_i128); - let new_balance = current - .checked_add(amount) - .ok_or(PlatformError::Overflow)?; + let new_balance = current.checked_add(amount).ok_or(PlatformError::Overflow)?; env.storage().persistent().set(&key, &new_balance); env.storage() @@ -275,11 +304,7 @@ impl FanRewardsContract { // Debit source first; refuse if it would underflow into a negative balance. let from_key = DataKey::Balance(from.clone()); - let from_balance: i128 = env - .storage() - .persistent() - .get(&from_key) - .unwrap_or(0_i128); + let from_balance: i128 = env.storage().persistent().get(&from_key).unwrap_or(0_i128); let new_from_balance = from_balance .checked_sub(amount) .ok_or(PlatformError::InvalidAmount)?; @@ -349,9 +374,15 @@ mod test { let approver_a = Address::generate(&env); let approver_b = Address::generate(&env); env.as_contract(&contract_id, || { - env.storage().instance().set(&DataKey::PlatformAdmin, &approver_a); - env.storage().instance().set(&DataKey::SecurityAdmin, &approver_b); - env.storage().instance().set(&DataKey::TreasuryAdmin, &admin); + env.storage() + .instance() + .set(&DataKey::PlatformAdmin, &approver_a); + env.storage() + .instance() + .set(&DataKey::SecurityAdmin, &approver_b); + env.storage() + .instance() + .set(&DataKey::TreasuryAdmin, &admin); }); let new_hash = BytesN::from_array(&env, &[7; 32]); @@ -394,12 +425,8 @@ mod test { let c = client(&env, &contract_id); let user = Address::generate(&env); - c.award_points( - &user, - &100_i128, - &symbol_short!("watched"), - ) - .unwrap(); + c.award_points(&user, &100_i128, &symbol_short!("watched")) + .unwrap(); assert_eq!(c.get_balance(&user), 100_i128); } @@ -410,9 +437,12 @@ mod test { let c = client(&env, &contract_id); let user = Address::generate(&env); - c.award_points(&user, &100_i128, &symbol_short!("watched")).unwrap(); - c.award_points(&user, &250_i128, &symbol_short!("shared")).unwrap(); - c.award_points(&user, &50_i128, &symbol_short!("referral")).unwrap(); + c.award_points(&user, &100_i128, &symbol_short!("watched")) + .unwrap(); + c.award_points(&user, &250_i128, &symbol_short!("shared")) + .unwrap(); + c.award_points(&user, &50_i128, &symbol_short!("referral")) + .unwrap(); assert_eq!(c.get_balance(&user), 400_i128); } @@ -478,7 +508,8 @@ mod test { let c = client(&env, &contract_id); let user = Address::generate(&env); - c.award_points(&user, &1_000_i128, &symbol_short!("watched")).unwrap(); + c.award_points(&user, &1_000_i128, &symbol_short!("watched")) + .unwrap(); c.redeem_points(&user, &400_i128, &symbol_short!("nft_drop")) .unwrap(); @@ -491,7 +522,8 @@ mod test { let c = client(&env, &contract_id); let user = Address::generate(&env); - c.award_points(&user, &500_i128, &symbol_short!("watched")).unwrap(); + c.award_points(&user, &500_i128, &symbol_short!("watched")) + .unwrap(); c.redeem_points(&user, &500_i128, &symbol_short!("nft_drop")) .unwrap(); @@ -510,7 +542,8 @@ mod test { assert!(res.is_err()); // InvalidAmount (underflow) // Award a small balance, then try to redeem more than they hold. - c.award_points(&user, &10_i128, &symbol_short!("watched")).unwrap(); + c.award_points(&user, &10_i128, &symbol_short!("watched")) + .unwrap(); let res = c.try_redeem_points(&user, &11_i128, &symbol_short!("nft_drop")); assert!(res.is_err()); // InvalidAmount (underflow) @@ -524,7 +557,8 @@ mod test { let c = client(&env, &contract_id); let user = Address::generate(&env); - c.award_points(&user, &100_i128, &symbol_short!("watched")).unwrap(); + c.award_points(&user, &100_i128, &symbol_short!("watched")) + .unwrap(); let res = c.try_redeem_points(&user, &0_i128, &symbol_short!("nft_drop")); assert!(res.is_err()); // InvalidAmount } @@ -535,7 +569,8 @@ mod test { let c = client(&env, &contract_id); let user = Address::generate(&env); - c.award_points(&user, &100_i128, &symbol_short!("watched")).unwrap(); + c.award_points(&user, &100_i128, &symbol_short!("watched")) + .unwrap(); let res = c.try_redeem_points(&user, &-10_i128, &symbol_short!("nft_drop")); assert!(res.is_err()); // InvalidAmount } @@ -568,9 +603,7 @@ mod test { .unwrap(); assert!(c.try_transfer_points(&alice, &bob, &0_i128).is_err()); - assert!(c - .try_transfer_points(&alice, &bob, &-1_i128) - .is_err()); + assert!(c.try_transfer_points(&alice, &bob, &-1_i128).is_err()); // Balances unchanged after rejection. assert_eq!(c.get_balance(&alice), 100_i128); @@ -597,7 +630,8 @@ mod test { let alice = Address::generate(&env); let bob = Address::generate(&env); - c.award_points(&alice, &5_i128, &symbol_short!("watched")).unwrap(); + c.award_points(&alice, &5_i128, &symbol_short!("watched")) + .unwrap(); let res = c.try_transfer_points(&alice, &bob, &6_i128); assert!(res.is_err()); // InvalidAmount (underflow) diff --git a/vault/src/lib.rs b/vault/src/lib.rs index 1a18c79..ff54351 100644 --- a/vault/src/lib.rs +++ b/vault/src/lib.rs @@ -7,7 +7,9 @@ //! and distribute payouts to winners. Implements reentrancy protection and strict access controls. use renaissance_core::{get_event_topic_by_string, PlatformError}; -use soroban_sdk::{contract, contracterror, contractimpl, contracttype, token, Address, Env, Symbol}; +use soroban_sdk::{ + contract, contracterror, contractimpl, contracttype, token, Address, Env, Symbol, +}; // ── Balance structures ───────────────────────────────────────────────────────── @@ -101,7 +103,9 @@ impl RenaissanceVaultContract { if env.storage().instance().has(&DataKey::ReentrancyGuard) { return Err(VaultError::ReentrancyDetected); } - env.storage().instance().set(&DataKey::ReentrancyGuard, &true); + env.storage() + .instance() + .set(&DataKey::ReentrancyGuard, &true); Ok(()) } @@ -116,14 +120,19 @@ impl RenaissanceVaultContract { .instance() .get(&DataKey::BettingContract) .ok_or(PlatformError::Unauthorized)?; - + betting_contract.require_auth(); Ok(betting_contract) } /// Helper to ensure contract is not paused fn ensure_not_paused(env: &Env) -> Result<(), PlatformError> { - if env.storage().instance().get(&DataKey::Paused).unwrap_or(false) { + if env + .storage() + .instance() + .get(&DataKey::Paused) + .unwrap_or(false) + { return Err(PlatformError::Paused); } Ok(()) @@ -131,7 +140,11 @@ impl RenaissanceVaultContract { /// One-time initialization of the vault contract /// Sets up admin and authorizes the initial betting contract - pub fn initialize(env: Env, admin: Address, betting_contract: Address) -> Result<(), PlatformError> { + pub fn initialize( + env: Env, + admin: Address, + betting_contract: Address, + ) -> Result<(), PlatformError> { admin.require_auth(); if env.storage().instance().has(&DataKey::Admin) { @@ -139,20 +152,23 @@ impl RenaissanceVaultContract { } env.storage().instance().set(&DataKey::Admin, &admin); - env.storage().instance().set(&DataKey::BettingContract, &betting_contract); + env.storage() + .instance() + .set(&DataKey::BettingContract, &betting_contract); env.storage().instance().set(&DataKey::Paused, &false); // Extend TTL for instance storage (30 days) const MAX_TTL: u32 = 518400; - env.storage() - .instance() - .extend_ttl(MAX_TTL, MAX_TTL); + env.storage().instance().extend_ttl(MAX_TTL, MAX_TTL); Ok(()) } /// Update the authorized betting contract (admin only) - pub fn set_betting_contract(env: Env, new_betting_contract: Address) -> Result<(), PlatformError> { + pub fn set_betting_contract( + env: Env, + new_betting_contract: Address, + ) -> Result<(), PlatformError> { let admin: Address = env .storage() .instance() @@ -169,7 +185,12 @@ impl RenaissanceVaultContract { /// Deposit tokens into the vault - user stakes their tokens /// Works with both native XLM (via SAC) and any custom asset - pub fn deposit(env: Env, user: Address, amount: i128, asset: Address) -> Result<(), VaultError> { + pub fn deposit( + env: Env, + user: Address, + amount: i128, + asset: Address, + ) -> Result<(), VaultError> { if amount <= 0 { return Err(VaultError::InvalidAmount); } @@ -195,7 +216,9 @@ impl RenaissanceVaultContract { locked: 0, }); user_balance.available += amount; - env.storage().persistent().set(&user_balance_key, &user_balance); + env.storage() + .persistent() + .set(&user_balance_key, &user_balance); // Update vault's total balance let vault_balance_key = DataKey::VaultBalance(asset.clone()); @@ -208,21 +231,30 @@ impl RenaissanceVaultContract { total_locked: 0, }); vault_balance.total_deposited += amount; - env.storage().persistent().set(&vault_balance_key, &vault_balance); + env.storage() + .persistent() + .set(&vault_balance_key, &vault_balance); // Exit reentrancy guard before external interactions are complete Self::exit_reentrancy_guard(&env); // Emit Deposited event - env.events() - .publish((get_event_topic_by_string(&env, "Deposited"),), (user, amount, asset)); + env.events().publish( + (get_event_topic_by_string(&env, "Deposited"),), + (user, amount, asset), + ); Ok(()) } /// Withdraw unused tokens from the vault /// Only available funds (not locked in active bets) can be withdrawn - pub fn withdraw(env: Env, user: Address, amount: i128, asset: Address) -> Result<(), VaultError> { + pub fn withdraw( + env: Env, + user: Address, + amount: i128, + asset: Address, + ) -> Result<(), VaultError> { if amount <= 0 { return Err(VaultError::InvalidAmount); } @@ -246,7 +278,9 @@ impl RenaissanceVaultContract { // Update balances user_balance.available -= amount; - env.storage().persistent().set(&user_balance_key, &user_balance); + env.storage() + .persistent() + .set(&user_balance_key, &user_balance); let vault_balance_key = DataKey::VaultBalance(asset.clone()); let mut vault_balance: VaultBalance = env @@ -255,7 +289,9 @@ impl RenaissanceVaultContract { .get(&vault_balance_key) .ok_or(VaultError::InsufficientBalance)?; vault_balance.total_deposited -= amount; - env.storage().persistent().set(&vault_balance_key, &vault_balance); + env.storage() + .persistent() + .set(&vault_balance_key, &vault_balance); // Transfer tokens back to user (after all state changes - checks-effects-interactions) let client = token::Client::new(&env, &asset); @@ -264,15 +300,23 @@ impl RenaissanceVaultContract { Self::exit_reentrancy_guard(&env); // Emit Withdrawn event - env.events() - .publish((get_event_topic_by_string(&env, "Withdrawn"),), (user, amount, asset)); + env.events().publish( + (get_event_topic_by_string(&env, "Withdrawn"),), + (user, amount, asset), + ); Ok(()) } /// Lock funds for an active bet - only callable by the betting contract /// Moves funds from available to locked state - pub fn lock_for_bet(env: Env, user: Address, amount: i128, asset: Address, match_id: u64) -> Result<(), VaultError> { + pub fn lock_for_bet( + env: Env, + user: Address, + amount: i128, + asset: Address, + match_id: u64, + ) -> Result<(), VaultError> { if amount <= 0 { return Err(VaultError::InvalidAmount); } @@ -305,7 +349,9 @@ impl RenaissanceVaultContract { // Update user's balances user_balance.available -= amount; user_balance.locked += amount; - env.storage().persistent().set(&user_balance_key, &user_balance); + env.storage() + .persistent() + .set(&user_balance_key, &user_balance); // Update vault's totals let vault_balance_key = DataKey::VaultBalance(asset.clone()); @@ -315,20 +361,30 @@ impl RenaissanceVaultContract { .get(&vault_balance_key) .ok_or(VaultError::InsufficientBalance)?; vault_balance.total_locked += amount; - env.storage().persistent().set(&vault_balance_key, &vault_balance); + env.storage() + .persistent() + .set(&vault_balance_key, &vault_balance); Self::exit_reentrancy_guard(&env); // Emit Locked event - env.events() - .publish((get_event_topic_by_string(&env, "Locked"),), (user, amount, asset, match_id)); + env.events().publish( + (get_event_topic_by_string(&env, "Locked"),), + (user, amount, asset, match_id), + ); Ok(()) } /// Payout winnings to a winner - only callable by the betting contract /// Releases locked funds and transfers them to the winner - pub fn payout(env: Env, winner: Address, amount: i128, asset: Address, match_id: u64) -> Result<(), VaultError> { + pub fn payout( + env: Env, + winner: Address, + amount: i128, + asset: Address, + match_id: u64, + ) -> Result<(), VaultError> { if amount <= 0 { return Err(VaultError::InvalidAmount); } @@ -356,7 +412,9 @@ impl RenaissanceVaultContract { .ok_or(VaultError::InsufficientBalance)?; user_balance.locked -= locked_amount; user_balance.available += amount; // Add winnings to available balance - env.storage().persistent().set(&user_balance_key, &user_balance); + env.storage() + .persistent() + .set(&user_balance_key, &user_balance); // Update vault's totals let vault_balance_key = DataKey::VaultBalance(asset.clone()); @@ -366,7 +424,9 @@ impl RenaissanceVaultContract { .get(&vault_balance_key) .ok_or(VaultError::InsufficientBalance)?; vault_balance.total_locked -= locked_amount; - env.storage().persistent().set(&vault_balance_key, &vault_balance); + env.storage() + .persistent() + .set(&vault_balance_key, &vault_balance); // Transfer the payout (after all state updates) let client = token::Client::new(&env, &asset); @@ -375,8 +435,10 @@ impl RenaissanceVaultContract { Self::exit_reentrancy_guard(&env); // Emit PaidOut event - env.events() - .publish((get_event_topic_by_string(&env, "PaidOut"),), (winner, amount, asset, match_id)); + env.events().publish( + (get_event_topic_by_string(&env, "PaidOut"),), + (winner, amount, asset, match_id), + ); Ok(()) } @@ -396,8 +458,7 @@ impl RenaissanceVaultContract { /// Get a specific user's balance for an asset pub fn get_user_balance(env: Env, user: Address, asset: Address) -> UserBalance { - env - .storage() + env.storage() .persistent() .get(&DataKey::UserBalance(user, asset)) .unwrap_or_else(|| UserBalance { @@ -408,11 +469,18 @@ impl RenaissanceVaultContract { /// Check if funds are locked for a specific bet pub fn is_locked_for_bet(env: Env, match_id: u64, user: Address, asset: Address) -> bool { - env.storage().persistent().has(&DataKey::LockedBet(match_id, user, asset)) + env.storage() + .persistent() + .has(&DataKey::LockedBet(match_id, user, asset)) } /// Emergency withdraw of an asset (timelocked) - pub fn emergency_withdraw(env: Env, asset: Address, to: Address, amount: i128) -> Result<(), VaultError> { + pub fn emergency_withdraw( + env: Env, + asset: Address, + to: Address, + amount: i128, + ) -> Result<(), VaultError> { let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); admin.require_auth(); @@ -432,15 +500,24 @@ impl RenaissanceVaultContract { let reason_hash = soroban_sdk::BytesN::from_array(&env, &[0; 32]); env.events().publish( (get_event_topic_by_string(&env, "EmergencyAction"),), - (Symbol::new(&env, "emergency_withdraw"), asset, to, amount, reason_hash), + ( + Symbol::new(&env, "emergency_withdraw"), + asset, + to, + amount, + reason_hash, + ), ); Ok(()) } else { - env.storage().instance().set(&key, &PendingWithdrawal { - to: to.clone(), - amount, - pending_until: env.ledger().timestamp() + 24 * 60 * 60, // 24 hours - }); + env.storage().instance().set( + &key, + &PendingWithdrawal { + to: to.clone(), + amount, + pending_until: env.ledger().timestamp() + 24 * 60 * 60, // 24 hours + }, + ); Ok(()) } } @@ -449,17 +526,28 @@ impl RenaissanceVaultContract { pub fn cancel_emergency_withdraw(env: Env, asset: Address) -> Result<(), VaultError> { let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); admin.require_auth(); - env.storage().instance().remove(&DataKey::PendingWithdrawal(asset)); + env.storage() + .instance() + .remove(&DataKey::PendingWithdrawal(asset)); Ok(()) } /// Recover accidentally sent tokens (only for non-tracked tokens) - pub fn recover_token(env: Env, asset: Address, to: Address, amount: i128) -> Result<(), VaultError> { + pub fn recover_token( + env: Env, + asset: Address, + to: Address, + amount: i128, + ) -> Result<(), VaultError> { let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); admin.require_auth(); let vault_balance_key = DataKey::VaultBalance(asset.clone()); - if let Some(vault_balance) = env.storage().persistent().get::<_, VaultBalance>(&vault_balance_key) { + if let Some(vault_balance) = env + .storage() + .persistent() + .get::<_, VaultBalance>(&vault_balance_key) + { if vault_balance.total_deposited > 0 || vault_balance.total_locked > 0 { return Err(VaultError::TokenIsTracked); } @@ -471,7 +559,13 @@ impl RenaissanceVaultContract { let reason_hash = soroban_sdk::BytesN::from_array(&env, &[0; 32]); env.events().publish( (get_event_topic_by_string(&env, "EmergencyAction"),), - (Symbol::new(&env, "recover_token"), asset, to, amount, reason_hash), + ( + Symbol::new(&env, "recover_token"), + asset, + to, + amount, + reason_hash, + ), ); Ok(()) } @@ -500,10 +594,13 @@ impl RenaissanceVaultContract { ); Ok(()) } else { - env.storage().instance().set(&key, &PendingAdminData { - new_admin: new_admin.clone(), - pending_until: env.ledger().timestamp() + 48 * 60 * 60, // 48 hours - }); + env.storage().instance().set( + &key, + &PendingAdminData { + new_admin: new_admin.clone(), + pending_until: env.ledger().timestamp() + 48 * 60 * 60, // 48 hours + }, + ); Ok(()) } } @@ -518,4 +615,4 @@ impl RenaissanceVaultContract { } #[cfg(test)] -mod test; \ No newline at end of file +mod test; diff --git a/vault/src/test.rs b/vault/src/test.rs index b1d01bd..6c98f96 100644 --- a/vault/src/test.rs +++ b/vault/src/test.rs @@ -1,21 +1,21 @@ #![cfg(test)] use super::*; -use soroban_sdk::{testutils::AddressEnvTestUtils, Address, Env}; use soroban_sdk::testutils::Ledger; use soroban_sdk::token::Client; +use soroban_sdk::{testutils::AddressEnvTestUtils, Address, Env}; #[test] fn test_initialize() { let env = Env::default(); let admin = Address::generate(&env); let betting_contract = Address::generate(&env); - + let contract_id = env.register_contract(None, RenaissanceVaultContract); let client = RenaissanceVaultContractClient::new(&env, &contract_id); - + client.initialize(&admin, &betting_contract); - + // Verify we can get the vault balance (starts at 0) let asset = Address::generate(&env); assert_eq!(client.get_vault_balance(&asset), 0); @@ -28,12 +28,12 @@ fn test_lock_for_bet_unauthorized() { let admin = Address::generate(&env); let betting_contract = Address::generate(&env); let unauthorized = Address::generate(&env); - + let contract_id = env.register_contract(None, RenaissanceVaultContract); let client = RenaissanceVaultContractClient::new(&env, &contract_id); - + client.initialize(&admin, &betting_contract); - + // Unauthorized address tries to lock funds let user = Address::generate(&env); let asset = Address::generate(&env); @@ -45,39 +45,45 @@ fn test_deposit_withdraw() { let env = Env::default(); let admin = Address::generate(&env); let betting_contract = Address::generate(&env); - + // Deploy and initialize token contract (mock SAC) let token_contract = env.register_stellar_asset_contract(admin.clone()); let token_client = Client::new(&env, &token_contract.get_address()); - + // Deploy vault let vault_id = env.register_contract(None, RenaissanceVaultContract); let vault_client = RenaissanceVaultContractClient::new(&env, &vault_id); - + vault_client.initialize(&admin, &betting_contract); - + // Mint tokens to a user let user = Address::generate(&env); token_client.mint(&user, &1000); - + // Deposit into vault user.require_auth(); vault_client.deposit(&user, &500, &token_contract.get_address()); - + // Check balances let user_balance = vault_client.get_user_balance(&user, &token_contract.get_address()); assert_eq!(user_balance.available, 500); assert_eq!(user_balance.locked, 0); - assert_eq!(vault_client.get_vault_balance(&token_contract.get_address()), 500); - + assert_eq!( + vault_client.get_vault_balance(&token_contract.get_address()), + 500 + ); + // Withdraw some user.require_auth(); vault_client.withdraw(&user, &200, &token_contract.get_address()); - + // Check updated balances let user_balance = vault_client.get_user_balance(&user, &token_contract.get_address()); assert_eq!(user_balance.available, 300); - assert_eq!(vault_client.get_vault_balance(&token_contract.get_address()), 300); + assert_eq!( + vault_client.get_vault_balance(&token_contract.get_address()), + 300 + ); } #[test] @@ -86,21 +92,21 @@ fn test_withdraw_insufficient_balance() { let env = Env::default(); let admin = Address::generate(&env); let betting_contract = Address::generate(&env); - + let token_contract = env.register_stellar_asset_contract(admin.clone()); let vault_id = env.register_contract(None, RenaissanceVaultContract); let vault_client = RenaissanceVaultContractClient::new(&env, &vault_id); - + vault_client.initialize(&admin, &betting_contract); - + let user = Address::generate(&env); let token_client = Client::new(&env, &token_contract.get_address()); token_client.mint(&user, &1000); - + // Deposit 300 user.require_auth(); vault_client.deposit(&user, &300, &token_contract.get_address()); - + // Try to withdraw 400 - should fail user.require_auth(); vault_client.withdraw(&user, &400, &token_contract.get_address()); @@ -111,25 +117,25 @@ fn test_lock_for_bet_success() { let env = Env::default(); let admin = Address::generate(&env); let betting_contract = Address::generate(&env); - + let token_contract = env.register_stellar_asset_contract(admin.clone()); let vault_id = env.register_contract(None, RenaissanceVaultContract); let vault_client = RenaissanceVaultContractClient::new(&env, &vault_id); - + vault_client.initialize(&admin, &betting_contract); - + let user = Address::generate(&env); let token_client = Client::new(&env, &token_contract.get_address()); token_client.mint(&user, &1000); - + // Deposit funds user.require_auth(); vault_client.deposit(&user, &500, &token_contract.get_address()); - + // Betting contract locks funds for a bet betting_contract.require_auth(); vault_client.lock_for_bet(&user, &200, &token_contract.get_address(), &123); - + // Check balances after lock let user_balance = vault_client.get_user_balance(&user, &token_contract.get_address()); assert_eq!(user_balance.available, 300); @@ -143,21 +149,21 @@ fn test_lock_for_bet_insufficient_balance() { let env = Env::default(); let admin = Address::generate(&env); let betting_contract = Address::generate(&env); - + let token_contract = env.register_stellar_asset_contract(admin.clone()); let vault_id = env.register_contract(None, RenaissanceVaultContract); let vault_client = RenaissanceVaultContractClient::new(&env, &vault_id); - + vault_client.initialize(&admin, &betting_contract); - + let user = Address::generate(&env); let token_client = Client::new(&env, &token_contract.get_address()); token_client.mint(&user, &1000); - + // Deposit only 100 user.require_auth(); vault_client.deposit(&user, &100, &token_contract.get_address()); - + // Try to lock 200 - should fail betting_contract.require_auth(); vault_client.lock_for_bet(&user, &200, &token_contract.get_address(), &123); @@ -171,12 +177,12 @@ fn test_reentrancy_prevention() { let env = Env::default(); let admin = Address::generate(&env); let betting_contract = Address::generate(&env); - + let vault_id = env.register_contract(None, RenaissanceVaultContract); let vault_client = RenaissanceVaultContractClient::new(&env, &vault_id); - + vault_client.initialize(&admin, &betting_contract); - + // We'd need a malicious contract to test actual reentrancy, but the guard is in place // The implementation follows checks-effects-interactions pattern which prevents reentrancy } @@ -186,28 +192,28 @@ fn test_payout_success() { let env = Env::default(); let admin = Address::generate(&env); let betting_contract = Address::generate(&env); - + let token_contract = env.register_stellar_asset_contract(admin.clone()); let vault_id = env.register_contract(None, RenaissanceVaultContract); let vault_client = RenaissanceVaultContractClient::new(&env, &vault_id); - + vault_client.initialize(&admin, &betting_contract); - + let winner = Address::generate(&env); let token_client = Client::new(&env, &token_contract.get_address()); token_client.mint(&winner, &1000); - + // Deposit and lock funds winner.require_auth(); vault_client.deposit(&winner, &500, &token_contract.get_address()); - + betting_contract.require_auth(); vault_client.lock_for_bet(&winner, &200, &token_contract.get_address(), &123); - + // Payout winnings betting_contract.require_auth(); vault_client.payout(&winner, &300, &token_contract.get_address(), &123); - + // Check final balances let user_balance = vault_client.get_user_balance(&winner, &token_contract.get_address()); assert_eq!(user_balance.available, 600); // 300 left + 300 winnings @@ -220,30 +226,30 @@ fn test_emergency_withdraw_schedule_and_execute() { let env = Env::default(); let admin = Address::generate(&env); let betting_contract = Address::generate(&env); - + let token_contract = env.register_stellar_asset_contract(admin.clone()); let vault_id = env.register_contract(None, RenaissanceVaultContract); let vault_client = RenaissanceVaultContractClient::new(&env, &vault_id); - + vault_client.initialize(&admin, &betting_contract); - + let to = Address::generate(&env); let token_client = Client::new(&env, &token_contract.get_address()); - + // Simulate some tokens stuck in the contract token_client.mint(&vault_id, &1000); - + // Schedule emergency withdraw admin.require_auth(); vault_client.emergency_withdraw(&token_contract.get_address(), &to, &500); - + // Fast forward past 24 hours env.ledger().set_timestamp(24 * 60 * 60 + 1); - + // Execute admin.require_auth(); vault_client.emergency_withdraw(&token_contract.get_address(), &to, &500); - + assert_eq!(token_client.balance(&to), 500); } @@ -252,24 +258,24 @@ fn test_emergency_withdraw_cancel() { let env = Env::default(); let admin = Address::generate(&env); let betting_contract = Address::generate(&env); - + let token_contract = env.register_stellar_asset_contract(admin.clone()); let vault_id = env.register_contract(None, RenaissanceVaultContract); let vault_client = RenaissanceVaultContractClient::new(&env, &vault_id); - + vault_client.initialize(&admin, &betting_contract); let to = Address::generate(&env); - + admin.require_auth(); vault_client.emergency_withdraw(&token_contract.get_address(), &to, &500); - + // Cancel admin.require_auth(); vault_client.cancel_emergency_withdraw(&token_contract.get_address()); - + // Fast forward env.ledger().set_timestamp(24 * 60 * 60 + 1); - + // Execute should fail because it was cancelled // It will try to schedule again, so let's just make sure it doesn't transfer let token_client = Client::new(&env, &token_contract.get_address()); @@ -283,24 +289,24 @@ fn test_set_admin_schedule_and_execute() { let env = Env::default(); let admin = Address::generate(&env); let betting_contract = Address::generate(&env); - + let vault_id = env.register_contract(None, RenaissanceVaultContract); let vault_client = RenaissanceVaultContractClient::new(&env, &vault_id); vault_client.initialize(&admin, &betting_contract); - + let new_admin = Address::generate(&env); - + // Schedule admin.require_auth(); vault_client.set_admin(&new_admin); - + // Fast forward past 48 hours env.ledger().set_timestamp(48 * 60 * 60 + 1); - + // Execute admin.require_auth(); vault_client.set_admin(&new_admin); - + // To verify admin changed, we could call an admin function new_admin.require_auth(); vault_client.cancel_set_admin(); // Should succeed with new admin @@ -311,17 +317,17 @@ fn test_set_admin_cancel() { let env = Env::default(); let admin = Address::generate(&env); let betting_contract = Address::generate(&env); - + let vault_id = env.register_contract(None, RenaissanceVaultContract); let vault_client = RenaissanceVaultContractClient::new(&env, &vault_id); vault_client.initialize(&admin, &betting_contract); - + let new_admin = Address::generate(&env); - + // Schedule admin.require_auth(); vault_client.set_admin(&new_admin); - + // Cancel admin.require_auth(); vault_client.cancel_set_admin(); @@ -332,33 +338,33 @@ fn test_recover_token() { let env = Env::default(); let admin = Address::generate(&env); let betting_contract = Address::generate(&env); - + let vault_id = env.register_contract(None, RenaissanceVaultContract); let vault_client = RenaissanceVaultContractClient::new(&env, &vault_id); vault_client.initialize(&admin, &betting_contract); - + // Tracked token let tracked_token = env.register_stellar_asset_contract(admin.clone()); let user = Address::generate(&env); let token_client = Client::new(&env, &tracked_token.get_address()); token_client.mint(&user, &1000); - + user.require_auth(); vault_client.deposit(&user, &500, &tracked_token.get_address()); - + // Recover should fail for tracked token let to = Address::generate(&env); admin.require_auth(); let result = vault_client.try_recover_token(&tracked_token.get_address(), &to, &100); assert!(result.is_err()); - + // Untracked token let untracked_token = env.register_stellar_asset_contract(admin.clone()); let untracked_client = Client::new(&env, &untracked_token.get_address()); untracked_client.mint(&vault_id, &500); // Send directly to contract - + // Recover should succeed admin.require_auth(); vault_client.recover_token(&untracked_token.get_address(), &to, &500); assert_eq!(untracked_client.balance(&to), 500); -} \ No newline at end of file +}