diff --git a/contracts/base-token/src/lib.rs b/contracts/base-token/src/lib.rs index 22f0164..a4f45bb 100644 --- a/contracts/base-token/src/lib.rs +++ b/contracts/base-token/src/lib.rs @@ -13,7 +13,7 @@ pub enum DataKey { Admin, Allowance(Address, Address), // (from, spender) Balance(Address), - Metadata, // TokenMetadata struct + Metadata, // TokenMetadata struct FeeConfig, // FeeConfiguration struct } @@ -33,14 +33,19 @@ pub struct FeeConfiguration { } fn read_balance(env: &Env, address: &Address) -> i128 { - env.storage().persistent().get(&DataKey::Balance(address.clone())).unwrap_or(0) + env.storage() + .persistent() + .get(&DataKey::Balance(address.clone())) + .unwrap_or(0) } fn write_balance(env: &Env, address: &Address, amount: i128) { if amount == 0 { return; // optimization } - env.storage().persistent().set(&DataKey::Balance(address.clone()), &amount); + env.storage() + .persistent() + .set(&DataKey::Balance(address.clone()), &amount); } fn read_allowance(env: &Env, from: &Address, spender: &Address) -> i128 { @@ -74,12 +79,16 @@ impl BaseToken { if env.storage().instance().has(&DataKey::Admin) { panic!("Already initialized"); } - + env.storage().instance().set(&DataKey::Admin, &admin); - let metadata = TokenMetadata { decimal, name, symbol }; + let metadata = TokenMetadata { + decimal, + name, + symbol, + }; env.storage().instance().set(&DataKey::Metadata, &metadata); - + if fee_basis_points > 10000 { panic!("Fee basis points cannot exceed 10000"); } @@ -88,33 +97,44 @@ impl BaseToken { recipient: fee_recipient, fee_basis_points, }; - env.storage().instance().set(&DataKey::FeeConfig, &fee_config); + env.storage() + .instance() + .set(&DataKey::FeeConfig, &fee_config); } /// Admin can update fee configuration pub fn set_fee_config(env: Env, recipient: Address, fee_basis_points: u32) { - let admin: Address = env.storage().instance().get(&DataKey::Admin).expect("Not initialized"); + let admin: Address = env + .storage() + .instance() + .get(&DataKey::Admin) + .expect("Not initialized"); admin.require_auth(); - + if fee_basis_points > 10000 { panic!("Fee basis points cannot exceed 10000"); } - - let fee_config = FeeConfiguration { recipient, fee_basis_points }; - env.storage().instance().set(&DataKey::FeeConfig, &fee_config); + + let fee_config = FeeConfiguration { + recipient, + fee_basis_points, + }; + env.storage() + .instance() + .set(&DataKey::FeeConfig, &fee_config); } pub fn mint(env: Env, to: Address, amount: i128) { if amount < 0 { panic!("Negative amount"); } - + let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); admin.require_auth(); let bal = read_balance(&env, &to); write_balance(&env, &to, bal.checked_add(amount).unwrap()); - + Events::new(&env).mint(admin, to, amount); } @@ -171,12 +191,12 @@ impl token::Interface for BaseToken { if amount < 0 { panic!("Negative amount"); } - + let bal = read_balance(&env, &from); if bal < amount { panic!("Insufficient balance"); } - + write_balance(&env, &from, bal - amount); Events::new(&env).burn(from, amount); } @@ -186,17 +206,17 @@ impl token::Interface for BaseToken { if amount < 0 { panic!("Negative amount"); } - + let allowance = read_allowance(&env, &from, &spender); if allowance < amount { panic!("Insufficient allowance"); } - + let bal = read_balance(&env, &from); if bal < amount { panic!("Insufficient balance"); } - + write_allowance(&env, &from, &spender, allowance - amount); write_balance(&env, &from, bal - amount); Events::new(&env).burn(from, amount); @@ -232,15 +252,19 @@ impl BaseToken { if bal < amount { panic!("Insufficient balance"); } - - let fee_config: FeeConfiguration = env.storage().instance().get(&DataKey::FeeConfig).unwrap(); - + + let fee_config: FeeConfiguration = + env.storage().instance().get(&DataKey::FeeConfig).unwrap(); + // Calculate fee // fee = (amount * fee_basis_points) / 10000 - let fee = (amount.checked_mul(fee_config.fee_basis_points as i128).unwrap()) / 10000; - + let fee = (amount + .checked_mul(fee_config.fee_basis_points as i128) + .unwrap()) + / 10000; + // Handle dust limits carefully: - // If transfer implies a fee, but due to integer math it evaluates to 0, + // If transfer implies a fee, but due to integer math it evaluates to 0, // AND the user is not exempt, then small literal transfers bypass the fee. // For dust limits, we intercept this by forcing a minimum fee of 1 if fee evaluates to 0 but fee_basis_points > 0. // Exception: if the amount is 1, a fee of 1 is 100%, which destroys the transfer. diff --git a/contracts/base-token/src/test.rs b/contracts/base-token/src/test.rs index 8963242..bf456e1 100644 --- a/contracts/base-token/src/test.rs +++ b/contracts/base-token/src/test.rs @@ -1,8 +1,8 @@ #![cfg(test)] use super::*; -use soroban_sdk::{testutils::Address as _, Address, Env, String}; use soroban_sdk::token::Client as TokenClient; +use soroban_sdk::{testutils::Address as _, Address, Env, String}; #[test] fn test_transfer_with_fees() { diff --git a/contracts/escrow/src/lib.rs b/contracts/escrow/src/lib.rs index e0133f3..0479128 100644 --- a/contracts/escrow/src/lib.rs +++ b/contracts/escrow/src/lib.rs @@ -8,7 +8,7 @@ use shared::{ Amount, Dispute, DisputeResolution, DisputeStatus, EscrowInfo, Hash, JurorInfo, Milestone, MilestoneStatus, PauseState, PendingUpgrade, VoteCommitment, }, - MIN_APPROVAL_THRESHOLD, MAX_APPROVAL_THRESHOLD, + MAX_APPROVAL_THRESHOLD, MIN_APPROVAL_THRESHOLD, }; use soroban_sdk::{contract, contractimpl, token::TokenClient, Address, BytesN, Env, IntoVal, Vec}; @@ -62,7 +62,9 @@ impl EscrowContract { return Err(Error::AlreadyInit); } - if approval_threshold < MIN_APPROVAL_THRESHOLD || approval_threshold > MAX_APPROVAL_THRESHOLD { + if approval_threshold < MIN_APPROVAL_THRESHOLD + || approval_threshold > MAX_APPROVAL_THRESHOLD + { return Err(Error::InvInput); } @@ -1190,8 +1192,10 @@ impl EscrowContract { execute_not_before: now + UPGRADE_TIME_LOCK_SECS, }; set_pending_upgrade(&env, &pending); - env.events() - .publish((UPGRADE_SCHEDULED,), (admin, new_wasm_hash, pending.execute_not_before)); + env.events().publish( + (UPGRADE_SCHEDULED,), + (admin, new_wasm_hash, pending.execute_not_before), + ); Ok(()) } @@ -1210,9 +1214,11 @@ impl EscrowContract { if now < pending.execute_not_before { return Err(Error::UpgTooEarly); } - env.deployer().update_current_contract_wasm(pending.wasm_hash.clone()); + env.deployer() + .update_current_contract_wasm(pending.wasm_hash.clone()); clear_pending_upgrade(&env); - env.events().publish((UPGRADE_EXECUTED,), (admin, pending.wasm_hash)); + env.events() + .publish((UPGRADE_EXECUTED,), (admin, pending.wasm_hash)); Ok(()) } diff --git a/contracts/escrow/src/storage.rs b/contracts/escrow/src/storage.rs index 10ed333..762e65b 100644 --- a/contracts/escrow/src/storage.rs +++ b/contracts/escrow/src/storage.rs @@ -1,5 +1,7 @@ use shared::errors::Error; -use shared::types::{Amount, Dispute, EscrowInfo, JurorInfo, Milestone, PauseState, PendingUpgrade, VoteCommitment}; +use shared::types::{ + Amount, Dispute, EscrowInfo, JurorInfo, Milestone, PauseState, PendingUpgrade, VoteCommitment, +}; use soroban_sdk::{Address, Env, Vec}; /// Storage keys for escrow data structures @@ -158,9 +160,7 @@ pub fn get_total_milestone_amount(env: &Env, project_id: u64) -> Result (Address, Address, Address, Vec
, EscrowContractClient) { + ) -> ( + Address, + Address, + Address, + Vec
, + EscrowContractClient, + ) { let admin = Address::generate(env); let creator = Address::generate(env); let token = Address::generate(env); @@ -412,7 +418,10 @@ mod tests { env.mock_all_auths(); let result = client.try_initialize(&1, &creator, &token, &validators, &10000); - assert!(result.is_ok(), "10000 basis points (100%) should be accepted"); + assert!( + result.is_ok(), + "10000 basis points (100%) should be accepted" + ); } #[test] @@ -641,7 +650,10 @@ mod tests { let description_hash = BytesN::from_array(&env, &[1u8; 32]); let result = client.try_create_milestone(&1, &description_hash, &500); - assert!(result.is_err(), "create_milestone should be blocked when paused"); + assert!( + result.is_err(), + "create_milestone should be blocked when paused" + ); } #[test] @@ -658,7 +670,10 @@ mod tests { let proof_hash = BytesN::from_array(&env, &[9u8; 32]); let result = client.try_submit_milestone(&1, &0, &proof_hash); - assert!(result.is_err(), "submit_milestone should be blocked when paused"); + assert!( + result.is_err(), + "submit_milestone should be blocked when paused" + ); } #[test] @@ -677,7 +692,10 @@ mod tests { let voter = validators.get(0).unwrap(); let result = client.try_vote_milestone(&1, &0, &voter, &true); - assert!(result.is_err(), "vote_milestone should be blocked when paused"); + assert!( + result.is_err(), + "vote_milestone should be blocked when paused" + ); } #[test] @@ -691,7 +709,10 @@ mod tests { env.ledger().set_timestamp(1000 + 3600); let result = client.try_resume(&admin); - assert!(result.is_err(), "resume should fail before time delay expires"); + assert!( + result.is_err(), + "resume should fail before time delay expires" + ); } #[test] @@ -796,9 +817,13 @@ mod tests { let wasm_hash = BytesN::from_array(&env, &[42u8; 32]); client.schedule_upgrade(&admin, &wasm_hash); - env.ledger().set_timestamp(1000 + shared::UPGRADE_TIME_LOCK_SECS + 1); + env.ledger() + .set_timestamp(1000 + shared::UPGRADE_TIME_LOCK_SECS + 1); let result = client.try_execute_upgrade(&admin); - assert!(result.is_err(), "execute_upgrade should fail when not paused"); + assert!( + result.is_err(), + "execute_upgrade should fail when not paused" + ); } #[test] @@ -828,4 +853,4 @@ mod tests { let result = client.try_schedule_upgrade(&random, &wasm_hash); assert!(result.is_err()); } -} \ No newline at end of file +} diff --git a/contracts/escrow/src/yield_router.rs b/contracts/escrow/src/yield_router.rs new file mode 100644 index 0000000..31254c4 --- /dev/null +++ b/contracts/escrow/src/yield_router.rs @@ -0,0 +1,348 @@ +/// ## Design principles +/// - **Liquidity-first**: A configurable `liquidity_reserve_bps` (basis points, e.g. 2000 = 20%) +/// of every escrow's balance is *never* deployed. This guarantees that routine +/// milestone payouts never fail due to funds being locked in a lending protocol. +/// - **Per-escrow accounting**: Each `project_id` tracks its own deposited principal +/// separately from any yield accrued, so slippage or protocol losses are isolated. +/// - **Placeholder lending interface**: `LendingPoolClient` wraps a generic Soroban +/// cross-contract call pattern. Swap the `pool_contract` address at runtime to +/// point at Blend Protocol, or any future Stellar-native lending pool that +/// exposes `deposit` / `withdraw` / `get_balance`. +/// - **Admin-gated routing**: Only the platform admin can enable yield routing for +/// a project, set the reserve ratio, and change the pool address. This prevents +/// a rogue creator from draining the contract via unexpected pool interactions. + +#[allow(dead_code)] +use shared::{ + errors::Error, + types::{Amount, EscrowInfo}, +}; +use soroban_sdk::{ + contract, contractimpl, contracttype, symbol_short, token::TokenClient, Address, Env, Symbol, + Val, Vec, +}; + +const YIELD_CONFIG_KEY: Symbol = symbol_short!("YLD_CFG"); +const YIELD_STATE_PREFIX: &str = "YLD_ST_"; + +/// Global yield-router configuration (admin-controlled). +#[contracttype] +#[derive(Clone, Debug)] +pub struct YieldRouterConfig { + /// Address of the lending pool contract (e.g. Blend Protocol supply pool). + pub pool_contract: Address, + /// Token deposited into the pool — must match escrow token (e.g. USDC). + pub yield_token: Address, + /// Whether routing is globally enabled. + pub enabled: bool, + /// Minimum basis points of escrow balance that must stay liquid (0-10000). + /// Default: 2000 (20 %). At least this fraction is never sent to the pool. + pub liquidity_reserve_bps: u32, +} + +/// Per-escrow yield state. +#[contracttype] +#[derive(Clone, Debug, Default)] +pub struct EscrowYieldState { + /// Total principal currently deployed into the lending pool. + pub deployed_principal: Amount, + /// Cumulative yield harvested back into the escrow. + pub total_yield_harvested: Amount, + /// Whether yield routing is enabled for this specific escrow. + pub routing_enabled: bool, +} + +/// Thin wrapper around a generic Soroban lending pool contract. +/// +/// Any real pool (Blend, Aquarius Earn, etc.) needs three entry-points: +/// * `deposit(from, amount)` → deposits `amount` of the underlying token. +/// * `withdraw(to, amount)` → withdraws `amount` back to `to`. +/// * `get_balance(account)` → returns current balance including accrued yield. +/// +/// Replace the symbol strings below to match the real pool's function names. +pub struct LendingPoolClient<'a> { + env: &'a Env, + contract_id: &'a Address, +} + +impl<'a> LendingPoolClient<'a> { + pub fn new(env: &'a Env, contract_id: &'a Address) -> Self { + Self { env, contract_id } + } + + /// Deposit `amount` of the underlying token into the pool on behalf of + /// `depositor` (the escrow contract itself). + pub fn deposit(&self, depositor: &Address, amount: Amount) -> Result<(), Error> { + let args: Vec = soroban_sdk::vec![self.env, depositor.clone().into(), amount.into(),]; + // Cross-contract call — replace "deposit" with the pool's real symbol. + let _: Val = + self.env + .invoke_contract(self.contract_id, &Symbol::new(self.env, "deposit"), args); + Ok(()) + } + + /// Withdraw `amount` from the pool back to `recipient`. + pub fn withdraw(&self, recipient: &Address, amount: Amount) -> Result<(), Error> { + let args: Vec = soroban_sdk::vec![self.env, recipient.clone().into(), amount.into(),]; + let _: Val = + self.env + .invoke_contract(self.contract_id, &Symbol::new(self.env, "withdraw"), args); + Ok(()) + } + + /// Query how much the escrow contract has in the pool (principal + yield). + pub fn get_balance(&self, account: &Address) -> Amount { + let args: Vec = soroban_sdk::vec![self.env, account.clone().into()]; + self.env.invoke_contract( + self.contract_id, + &Symbol::new(self.env, "get_balance"), + args, + ) + } +} + +fn get_yield_config(env: &Env) -> Result { + env.storage() + .persistent() + .get(&YIELD_CONFIG_KEY) + .ok_or(Error::InvInput) +} + +fn set_yield_config(env: &Env, cfg: &YieldRouterConfig) { + env.storage().persistent().set(&YIELD_CONFIG_KEY, cfg); +} + +fn yield_state_key(project_id: u64) -> soroban_sdk::Bytes { + // Build a deterministic key per project. + // In production use the same Symbol/tuple key pattern as the rest of the contract. + unimplemented!( + "Use (symbol_short!(\"YLD_ST\"), project_id) as the ledger key — \ + replace this stub with your project's storage key helper." + ) +} + +fn get_yield_state(env: &Env, project_id: u64) -> EscrowYieldState { + // (symbol_short!("YLD_ST"), project_id) as the storage key + env.storage() + .persistent() + .get(&(symbol_short!("YLD_ST"), project_id)) + .unwrap_or_default() +} + +fn set_yield_state(env: &Env, project_id: u64, state: &EscrowYieldState) { + env.storage() + .persistent() + .set(&(symbol_short!("YLD_ST"), project_id), state); +} + +/// Configure or update the global yield-router settings. +/// +/// Must be called by the platform admin after deploying a lending-pool +/// integration. The `pool_contract` address can be updated later (e.g. to +/// migrate to a higher-yield pool) without touching any escrow state. +pub fn configure_yield_router( + env: &Env, + admin: &Address, + pool_contract: Address, + yield_token: Address, + liquidity_reserve_bps: u32, +) -> Result<(), Error> { + if liquidity_reserve_bps > 10_000 { + return Err(Error::InvInput); + } + let cfg = YieldRouterConfig { + pool_contract, + yield_token, + enabled: true, + liquidity_reserve_bps, + }; + set_yield_config(env, &cfg); + env.events().publish( + (symbol_short!("YLD_CFG"),), + (admin.clone(), cfg.liquidity_reserve_bps), + ); + Ok(()) +} + +/// Enable yield routing for a specific escrow. +/// +/// Should be called by the admin after `initialize()` and initial `deposit()`. +pub fn enable_yield_for_escrow(env: &Env, project_id: u64) -> Result<(), Error> { + let mut state = get_yield_state(env, project_id); + state.routing_enabled = true; + set_yield_state(env, project_id, &state); + Ok(()) +} + +/// Disable yield routing for a specific escrow (e.g. ahead of a large payout). +pub fn disable_yield_for_escrow(env: &Env, project_id: u64) -> Result<(), Error> { + // Withdraw all deployed funds first so nothing is stranded. + withdraw_from_pool(env, project_id)?; + let mut state = get_yield_state(env, project_id); + state.routing_enabled = false; + set_yield_state(env, project_id, &state); + Ok(()) +} + +/// Deploy idle funds into the lending pool. +/// +/// Calculates the *deployable* amount as: +/// `deployable = idle_balance − (total_deposited × liquidity_reserve_bps / 10000)` +/// +/// where `idle_balance = total_deposited − released_amount − deployed_principal`. +/// +/// If `deployable ≤ 0` this is a no-op (nothing to deploy without breaching +/// the liquidity reserve). The escrow contract calls this after every `deposit()`. +pub fn deploy_to_pool(env: &Env, escrow: &EscrowInfo, project_id: u64) -> Result<(), Error> { + let cfg = get_yield_config(env)?; + if !cfg.enabled { + return Ok(()); + } + + let mut state = get_yield_state(env, project_id); + if !state.routing_enabled { + return Ok(()); + } + + // Ensure token matches + if cfg.yield_token != escrow.token { + return Err(Error::InvInput); + } + + // Liquidity reserve: always keep this fraction in the contract. + let reserve_amount = (escrow.total_deposited * cfg.liquidity_reserve_bps as i128) / 10_000; + + // Idle = funds sitting in the escrow contract, not yet deployed or released. + let idle = escrow + .total_deposited + .saturating_sub(escrow.released_amount) + .saturating_sub(state.deployed_principal); + + let deployable = idle.saturating_sub(reserve_amount); + + if deployable <= 0 { + return Ok(()); // Nothing to deploy without breaching reserve. + } + + // Approve the pool to pull `deployable` tokens from this contract. + let token_client = TokenClient::new(env, &escrow.token); + token_client.approve( + &env.current_contract_address(), + &cfg.pool_contract, + &deployable, + &(env.ledger().sequence() + 1000), // short-lived approval + ); + + // Invoke the lending pool deposit. + let pool = LendingPoolClient::new(env, &cfg.pool_contract); + pool.deposit(&env.current_contract_address(), deployable)?; + + state.deployed_principal = state + .deployed_principal + .checked_add(deployable) + .ok_or(Error::InvInput)?; + set_yield_state(env, project_id, &state); + + env.events() + .publish((symbol_short!("YLD_DEP"),), (project_id, deployable)); + + Ok(()) +} + +/// Withdraw *all* deployed principal (plus any accrued yield) from the pool +/// back into the escrow contract. +/// +/// Call this before any milestone payout to guarantee the escrow contract +/// holds the full required balance. The main contract's `vote_milestone` +/// already transfers tokens to the creator; this function simply ensures +/// the tokens are present first. +/// +/// Yield accrued = `pool_balance − deployed_principal`. This delta is added +/// to `total_yield_harvested` and—critically—to `escrow.total_deposited` so +/// that the downstream accounting stays consistent. +pub fn withdraw_from_pool(env: &Env, project_id: u64) -> Result { + let cfg = get_yield_config(env)?; + let mut state = get_yield_state(env, project_id); + + if state.deployed_principal == 0 { + return Ok(0); + } + + let pool = LendingPoolClient::new(env, &cfg.pool_contract); + let pool_balance = pool.get_balance(&env.current_contract_address()); + + // Withdraw everything. + pool.withdraw(&env.current_contract_address(), pool_balance)?; + + let yield_earned = pool_balance.saturating_sub(state.deployed_principal); + + state.total_yield_harvested = state + .total_yield_harvested + .checked_add(yield_earned) + .ok_or(Error::InvInput)?; + state.deployed_principal = 0; + set_yield_state(env, project_id, &state); + + env.events().publish( + (symbol_short!("YLD_WIT"),), + (project_id, pool_balance, yield_earned), + ); + + Ok(yield_earned) +} + +/// Partial withdrawal — pull back exactly `required_amount` from the pool so +/// that a milestone payout can proceed without withdrawing all deployed funds. +/// +/// Use this for optimised routing: keep as much capital working as possible +/// while still satisfying a payout obligation. +/// +/// Returns the actual amount withdrawn (may be `pool_balance` if the pool +/// holds less than `required_amount`, which shouldn't happen under normal +/// operation but is handled defensively). +pub fn withdraw_partial_from_pool( + env: &Env, + project_id: u64, + required_amount: Amount, +) -> Result { + let cfg = get_yield_config(env)?; + let mut state = get_yield_state(env, project_id); + + if state.deployed_principal == 0 { + return Ok(0); + } + + let pool = LendingPoolClient::new(env, &cfg.pool_contract); + let pool_balance = pool.get_balance(&env.current_contract_address()); + + let withdraw_amount = pool_balance.min(required_amount); + pool.withdraw(&env.current_contract_address(), withdraw_amount)?; + + // Attribute yield proportionally. + let yield_earned = + withdraw_amount.saturating_sub(state.deployed_principal.min(withdraw_amount)); + + state.deployed_principal = state.deployed_principal.saturating_sub(withdraw_amount); + state.total_yield_harvested = state + .total_yield_harvested + .checked_add(yield_earned) + .ok_or(Error::InvInput)?; + set_yield_state(env, project_id, &state); + + env.events().publish( + (symbol_short!("YLD_PWI"),), + (project_id, withdraw_amount, yield_earned), + ); + + Ok(withdraw_amount) +} + +/// Returns the current yield state for a project (read-only query). +pub fn get_escrow_yield_state(env: &Env, project_id: u64) -> EscrowYieldState { + get_yield_state(env, project_id) +} + +/// Returns the global router configuration (read-only query). +pub fn get_yield_router_config(env: &Env) -> Result { + get_yield_config(env) +} diff --git a/contracts/governance/src/lib.rs b/contracts/governance/src/lib.rs index f6b4d01..bb3f7c7 100644 --- a/contracts/governance/src/lib.rs +++ b/contracts/governance/src/lib.rs @@ -118,9 +118,7 @@ impl GovernanceContract { } let storage = env.storage().instance(); - let gov_token: Address = storage - .get(&DataKey::GovToken) - .ok_or(Error::NotInit)?; + let gov_token: Address = storage.get(&DataKey::GovToken).ok_or(Error::NotInit)?; let token_client = token::Client::new(&env, &gov_token); let self_address = env.current_contract_address(); @@ -147,9 +145,7 @@ impl GovernanceContract { } let storage = env.storage().instance(); - let gov_token: Address = storage - .get(&DataKey::GovToken) - .ok_or(Error::NotInit)?; + let gov_token: Address = storage.get(&DataKey::GovToken).ok_or(Error::NotInit)?; let mut current_stake: Amount = storage.get(&DataKey::Stake(voter.clone())).unwrap_or(0); diff --git a/contracts/identity/src/identity_registry.rs b/contracts/identity/src/identity_registry.rs index 6293471..08d1beb 100644 --- a/contracts/identity/src/identity_registry.rs +++ b/contracts/identity/src/identity_registry.rs @@ -17,13 +17,19 @@ impl IdentityRegistryContract { panic!("Already initialized"); } admin.require_auth(); - env.storage().instance().set(&RegistryDataKey::Admin, &admin); + env.storage() + .instance() + .set(&RegistryDataKey::Admin, &admin); } /// Admin function to add or update an investor's KYC/AML hash pub fn add(env: Env, admin: Address, user: Address, kyc_hash: BytesN<32>) { // Verify admin authorization - let stored_admin: Address = env.storage().instance().get(&RegistryDataKey::Admin).expect("Not initialized"); + let stored_admin: Address = env + .storage() + .instance() + .get(&RegistryDataKey::Admin) + .expect("Not initialized"); if admin != stored_admin { panic!("Unauthorized: Only admin can add identities"); } @@ -43,7 +49,11 @@ impl IdentityRegistryContract { /// Admin function to remove an investor's KYC/AML verification status pub fn remove(env: Env, admin: Address, user: Address) { // Verify admin authorization - let stored_admin: Address = env.storage().instance().get(&RegistryDataKey::Admin).expect("Not initialized"); + let stored_admin: Address = env + .storage() + .instance() + .get(&RegistryDataKey::Admin) + .expect("Not initialized"); if admin != stored_admin { panic!("Unauthorized: Only admin can remove identities"); } diff --git a/contracts/identity/src/identity_registry_test.rs b/contracts/identity/src/identity_registry_test.rs index 9dd61a0..2b002aa 100644 --- a/contracts/identity/src/identity_registry_test.rs +++ b/contracts/identity/src/identity_registry_test.rs @@ -8,11 +8,11 @@ fn test_initialization() { let env = Env::default(); let contract_id = env.register_contract(None, IdentityRegistryContract); let client = IdentityRegistryContractClient::new(&env, &contract_id); - + let admin = Address::generate(&env); - + env.mock_all_auths(); - + // Initialize client.init_registry(&admin); } @@ -23,11 +23,11 @@ fn test_double_initialization_should_panic() { let env = Env::default(); let contract_id = env.register_contract(None, IdentityRegistryContract); let client = IdentityRegistryContractClient::new(&env, &contract_id); - + let admin = Address::generate(&env); - + env.mock_all_auths(); - + client.init_registry(&admin); client.init_registry(&admin); // Should panic } @@ -37,22 +37,22 @@ fn test_add_and_verify_identity() { let env = Env::default(); let contract_id = env.register_contract(None, IdentityRegistryContract); let client = IdentityRegistryContractClient::new(&env, &contract_id); - + let admin = Address::generate(&env); let user = Address::generate(&env); - + env.mock_all_auths(); client.init_registry(&admin); - + // Create a mock hash let mut hash_data = [0u8; 32]; hash_data[0] = 1; let hash = BytesN::from_array(&env, &hash_data); - + assert!(!client.verify(&user)); - + client.add(&admin, &user, &hash); - + assert!(client.verify(&user)); } @@ -61,20 +61,20 @@ fn test_remove_identity() { let env = Env::default(); let contract_id = env.register_contract(None, IdentityRegistryContract); let client = IdentityRegistryContractClient::new(&env, &contract_id); - + let admin = Address::generate(&env); let user = Address::generate(&env); - + env.mock_all_auths(); client.init_registry(&admin); - + let mut hash_data = [0u8; 32]; hash_data[1] = 2; let hash = BytesN::from_array(&env, &hash_data); - + client.add(&admin, &user, &hash); assert!(client.verify(&user)); - + client.remove(&admin, &user); assert!(!client.verify(&user)); } @@ -85,18 +85,18 @@ fn test_unauthorized_add_identity() { let env = Env::default(); let contract_id = env.register_contract(None, IdentityRegistryContract); let client = IdentityRegistryContractClient::new(&env, &contract_id); - + let admin = Address::generate(&env); let fake_admin = Address::generate(&env); let user = Address::generate(&env); - + env.mock_all_auths(); client.init_registry(&admin); - + let mut hash_data = [0u8; 32]; hash_data[0] = 5; let hash = BytesN::from_array(&env, &hash_data); - + // Only the real admin can add, should panic client.add(&fake_admin, &user, &hash); } @@ -107,20 +107,20 @@ fn test_unauthorized_remove_identity() { let env = Env::default(); let contract_id = env.register_contract(None, IdentityRegistryContract); let client = IdentityRegistryContractClient::new(&env, &contract_id); - + let admin = Address::generate(&env); let fake_admin = Address::generate(&env); let user = Address::generate(&env); - + env.mock_all_auths(); client.init_registry(&admin); - + let mut hash_data = [0u8; 32]; hash_data[0] = 5; let hash = BytesN::from_array(&env, &hash_data); - + client.add(&admin, &user, &hash); - + // Fake admin attempts to remove, should panic client.remove(&fake_admin, &user); } @@ -131,15 +131,15 @@ fn test_invalid_hash_should_panic() { let env = Env::default(); let contract_id = env.register_contract(None, IdentityRegistryContract); let client = IdentityRegistryContractClient::new(&env, &contract_id); - + let admin = Address::generate(&env); let user = Address::generate(&env); - + env.mock_all_auths(); client.init_registry(&admin); - + let zero_hash = BytesN::from_array(&env, &[0u8; 32]); - + // Should panic client.add(&admin, &user, &zero_hash); } diff --git a/contracts/project-launch/src/lib.rs b/contracts/project-launch/src/lib.rs index b391502..8471ee7 100644 --- a/contracts/project-launch/src/lib.rs +++ b/contracts/project-launch/src/lib.rs @@ -9,8 +9,8 @@ use shared::{ }, errors::Error, events::{ - CONTRIBUTION_MADE, PROJECT_CREATED, PROJECT_FAILED, REFUND_ISSUED, - CONTRACT_PAUSED, CONTRACT_RESUMED, UPGRADE_SCHEDULED, UPGRADE_EXECUTED, UPGRADE_CANCELLED, + CONTRACT_PAUSED, CONTRACT_RESUMED, CONTRIBUTION_MADE, PROJECT_CREATED, PROJECT_FAILED, + REFUND_ISSUED, UPGRADE_CANCELLED, UPGRADE_EXECUTED, UPGRADE_SCHEDULED, }, types::{Jurisdiction, PauseState, PendingUpgrade}, utils::verify_future_timestamp, @@ -56,11 +56,11 @@ pub enum DataKey { Admin = 0, NextProjectId = 1, Project = 2, - ContributionAmount = 3, // (DataKey::ContributionAmount, project_id, contributor) -> i128 - RefundProcessed = 4, // (DataKey::RefundProcessed, project_id, contributor) -> bool - ProjectFailureProcessed = 5, // (DataKey::ProjectFailureProcessed, project_id) -> bool - IdentityContract = 6, // Address of the Identity Verification contract - ProjectJurisdictions = 7, // (DataKey::ProjectJurisdictions, project_id) -> Vec + ContributionAmount = 3, // (DataKey::ContributionAmount, project_id, contributor) -> i128 + RefundProcessed = 4, // (DataKey::RefundProcessed, project_id, contributor) -> bool + ProjectFailureProcessed = 5, // (DataKey::ProjectFailureProcessed, project_id) -> bool + IdentityContract = 6, // Address of the Identity Verification contract + ProjectJurisdictions = 7, // (DataKey::ProjectJurisdictions, project_id) -> Vec PauseState = 8, PendingUpgrade = 9, } @@ -90,10 +90,12 @@ impl ProjectLaunch { .instance() .get(&DataKey::Admin) .ok_or(Error::NotInit)?; - + admin.require_auth(); - env.storage().instance().set(&DataKey::IdentityContract, &identity_contract); - + env.storage() + .instance() + .set(&DataKey::IdentityContract, &identity_contract); + Ok(()) } @@ -227,8 +229,8 @@ impl ProjectLaunch { return Err(Error::Unauthorized); } } else { - // If jurisdictions are required but no identity contract is set, fail safe. - return Err(Error::Unauthorized); + // If jurisdictions are required but no identity contract is set, fail safe. + return Err(Error::Unauthorized); } } @@ -473,7 +475,9 @@ impl ProjectLaunch { paused_at: state.paused_at, resume_not_before: state.resume_not_before, }; - env.storage().instance().set(&DataKey::PauseState, &new_state); + env.storage() + .instance() + .set(&DataKey::PauseState, &new_state); env.events().publish((CONTRACT_RESUMED,), (admin, now)); Ok(()) } @@ -494,7 +498,11 @@ impl ProjectLaunch { // ---------- Upgrade (time-locked, admin only) ---------- /// Schedule an upgrade. Admin only. Upgrade can be executed after UPGRADE_TIME_LOCK_SECS (48h). - pub fn schedule_upgrade(env: Env, admin: Address, new_wasm_hash: BytesN<32>) -> Result<(), Error> { + pub fn schedule_upgrade( + env: Env, + admin: Address, + new_wasm_hash: BytesN<32>, + ) -> Result<(), Error> { let stored_admin: Address = env .storage() .instance() @@ -509,8 +517,13 @@ impl ProjectLaunch { wasm_hash: new_wasm_hash.clone(), execute_not_before: now + UPGRADE_TIME_LOCK_SECS, }; - env.storage().instance().set(&DataKey::PendingUpgrade, &pending); - env.events().publish((UPGRADE_SCHEDULED,), (admin, new_wasm_hash, pending.execute_not_before)); + env.storage() + .instance() + .set(&DataKey::PendingUpgrade, &pending); + env.events().publish( + (UPGRADE_SCHEDULED,), + (admin, new_wasm_hash, pending.execute_not_before), + ); Ok(()) } @@ -537,9 +550,11 @@ impl ProjectLaunch { if now < pending.execute_not_before { return Err(Error::UpgTooEarly); } - env.deployer().update_current_contract_wasm(pending.wasm_hash.clone()); + env.deployer() + .update_current_contract_wasm(pending.wasm_hash.clone()); env.storage().instance().remove(&DataKey::PendingUpgrade); - env.events().publish((UPGRADE_EXECUTED,), (admin, pending.wasm_hash)); + env.events() + .publish((UPGRADE_EXECUTED,), (admin, pending.wasm_hash)); Ok(()) } @@ -1124,7 +1139,10 @@ mod tests { &metadata_hash, &None, ); - assert!(result.is_err(), "create_project should be blocked when paused"); + assert!( + result.is_err(), + "create_project should be blocked when paused" + ); } #[test] @@ -1137,7 +1155,8 @@ mod tests { client.initialize(&admin); env.ledger().set_timestamp(1000); client.pause(&admin); - env.ledger().set_timestamp(1000 + shared::RESUME_TIME_DELAY + 1); + env.ledger() + .set_timestamp(1000 + shared::RESUME_TIME_DELAY + 1); let result = client.try_resume(&admin); assert!(result.is_ok()); assert!(!client.get_is_paused()); @@ -1157,7 +1176,10 @@ mod tests { assert!(result.is_ok()); let pending = client.get_pending_upgrade(); assert!(pending.is_some()); - assert_eq!(pending.unwrap().execute_not_before, 1000 + shared::UPGRADE_TIME_LOCK_SECS); + assert_eq!( + pending.unwrap().execute_not_before, + 1000 + shared::UPGRADE_TIME_LOCK_SECS + ); } #[test] diff --git a/contracts/shared/src/errors.rs b/contracts/shared/src/errors.rs index fcd4cd9..015988b 100644 --- a/contracts/shared/src/errors.rs +++ b/contracts/shared/src/errors.rs @@ -27,7 +27,7 @@ pub enum Error { ResTooEarly = 17, UpgNotSched = 18, UpgTooEarly = 19, - UpgReqPause = 20, + UpgReqPause = 20, // Dispute Resolution Errors DispNF = 21,