From 742cdf8b8b58897b59f4c0bec1a0f20098b25dd8 Mon Sep 17 00:00:00 2001 From: laurentketterle-hub Date: Wed, 12 Aug 2026 23:06:17 +0200 Subject: [PATCH 1/2] fix: restore investment_vault compilation and repair deposit-lock (Closes #310, #311, #314) The withdrawal-window merge (#36) left the crate non-compiling: get_withdrawal_window and get_volume_fee_tier were missing closing braces, the withdrawal_window_set and funding_round_ended event functions were missing closing braces, check_deposit_lock referenced an undefined last_seq and merged two incompatible lock models, and VaultError had three variants sharing discriminant 41. This change adds a LastDepositSeq(Address) storage key recorded by lock_deposit, rewrites check_deposit_lock to enforce the ledger-sequence sliding window (#36), renumbers FundingRoundActive to 42 and InvestmentCapExceeded to 43, and restores all missing closing braces. Signed-off-by: laurentketterle-hub --- investment_vault/src/events.rs | 2 ++ investment_vault/src/lib.rs | 21 ++++++++++++++++----- investment_vault/src/types.rs | 9 ++++++--- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/investment_vault/src/events.rs b/investment_vault/src/events.rs index 03570e06..7361b73e 100644 --- a/investment_vault/src/events.rs +++ b/investment_vault/src/events.rs @@ -504,6 +504,7 @@ pub struct WithdrawalWindowSet { pub fn withdrawal_window_set(env: &Env, ledgers: u32) { WithdrawalWindowSet { ledgers }.publish(env); +} /// Emitted when the admin opens a funding round (#38). #[contractevent] pub struct FundingRoundStarted {} @@ -518,6 +519,7 @@ pub struct FundingRoundEnded {} pub fn funding_round_ended(env: &Env) { FundingRoundEnded {}.publish(env); +} /// Emitted when the admin changes the per-project investment cap (#32). #[contractevent] pub struct InvestmentCapSet { diff --git a/investment_vault/src/lib.rs b/investment_vault/src/lib.rs index 1f384090..0aa23604 100644 --- a/investment_vault/src/lib.rs +++ b/investment_vault/src/lib.rs @@ -1127,7 +1127,7 @@ impl InvestmentVault { .instance() .get(&VaultKey::WithdrawalWindowLedgers) .unwrap_or(1) - // ── Dynamic fee structure (#39) ─────────────────────────────────────────── + } /// Configure a two-tier volume-discount fee schedule for deposits (#39). /// @@ -1179,6 +1179,7 @@ impl InvestmentVault { .get(&VaultKey::VolumeTierFeeBps) .unwrap_or(0); (threshold, bps) + } // ── Per-project investment cap (#32) ────────────────────────────────────── /// Set the maximum total USDC the vault may invest in any single project. Admin-only. @@ -2083,14 +2084,25 @@ fn lock_deposit(env: &Env, address: &Address) { &VaultKey::LastDeposit(address.clone()), &env.ledger().timestamp(), ); + env.storage().persistent().set( + &VaultKey::LastDepositSeq(address.clone()), + &env.ledger().sequence(), + ); } -/// Reject a withdrawal if the caller's deposit lock has not yet expired (#33). +/// Reject a withdrawal if the deposit lock has not yet expired (#36). +/// +/// Enforces the withdrawal sliding window: at least `WithdrawalWindowLedgers` +/// ledgers must elapse after the most recent deposit (or share receipt) of the +/// caller before a withdrawal is permitted. The default window of 1 ledger +/// blocks same-ledger deposit-then-withdraw exits. The older timestamp-based +/// `MIN_LOCK_PERIOD` cooldown (#33) remains exposed via +/// `get_deposit_lock_expiry` but is no longer enforced here. fn check_deposit_lock(env: &Env, address: &Address) { - if let Some(deposited_at) = env + if let Some(last_seq) = env .storage() .persistent() - .get::<_, u64>(&VaultKey::LastDeposit(address.clone())) + .get::<_, u32>(&VaultKey::LastDepositSeq(address.clone())) { let window: u32 = env .storage() @@ -2098,7 +2110,6 @@ fn check_deposit_lock(env: &Env, address: &Address) { .get(&VaultKey::WithdrawalWindowLedgers) .unwrap_or(1); if env.ledger().sequence() < last_seq.saturating_add(window) { - if env.ledger().timestamp() < deposited_at + MIN_LOCK_PERIOD { panic_with_error!(env, VaultError::DepositLocked); } } diff --git a/investment_vault/src/types.rs b/investment_vault/src/types.rs index e09392d3..91be2c88 100644 --- a/investment_vault/src/types.rs +++ b/investment_vault/src/types.rs @@ -90,9 +90,9 @@ pub enum VaultError { /// batch_deposit received an empty investor list (#178). EmptyBatchDeposit = 41, /// Share transfers are blocked because a funding round is active (#38). - FundingRoundActive = 41, + FundingRoundActive = 42, /// Funding would push cumulative investment in a project above its per-project cap (#32). - InvestmentCapExceeded = 41, + InvestmentCapExceeded = 43, } #[contracttype] @@ -154,7 +154,7 @@ pub enum VaultKey { MultiSigThreshold, /// Circuit breaker pause state. Paused, - /// Last deposit ledger sequence per address. + /// Last deposit ledger timestamp (seconds) per address (#33). LastDeposit(Address), /// Optional emergency-admin address that may pause/unpause without /// holding full owner privileges (#43). Unset means no emergency admin. @@ -178,6 +178,9 @@ pub enum VaultKey { /// Ledger timestamp (seconds) at which a project was first funded (#34). /// Used for time-weighted expected-returns calculation. InvestmentTimestamp(u32), + /// Last deposit ledger sequence per address (#36). + /// Used by `check_deposit_lock` to enforce the withdrawal sliding window. + LastDepositSeq(Address), } /// Container for wormhole bridge data keys. From edec5df6eb6d3430973aeb74e2f4d92709fa54ea Mon Sep 17 00:00:00 2001 From: laurentketterle-hub Date: Wed, 12 Aug 2026 23:21:18 +0200 Subject: [PATCH 2/2] fix: extend pause circuit-breaker to all privileged entry points (Closes #313) require_not_paused was only enforced in fund_project, deposit, and withdraw. Every other fund-moving or share-minting/burning entry point (multi-sig funding, batch funding, yield distribution, insurance payouts, bridge mint/burn, cross-chain completion, flash loans, and carbon-credit transfers) ignored the Paused flag entirely. This adds require_not_paused to all 11 listed functions and documents the coverage on pause(). Signed-off-by: laurentketterle-hub --- investment_vault/src/lib.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/investment_vault/src/lib.rs b/investment_vault/src/lib.rs index 0aa23604..1f755d73 100644 --- a/investment_vault/src/lib.rs +++ b/investment_vault/src/lib.rs @@ -249,6 +249,7 @@ impl InvestmentVault { amount: i128, approvals: Vec
, ) { + require_not_paused(&env); require_admin_approval(&env, approvals); fund_project_internal(env, project_id, amount); } @@ -257,6 +258,7 @@ impl InvestmentVault { /// /// Rejects batch requests containing duplicate project IDs to prevent double-funding. pub fn batch_fund_projects(env: Env, fundings: Vec<(u32, i128)>, approvals: Vec
) { + require_not_paused(&env); require_admin_approval(&env, approvals); let mut seen = Vec::new(&env); for funding in fundings.iter() { @@ -704,6 +706,7 @@ impl InvestmentVault { /// Called by the owner when a project makes a repayment. #[only_owner] pub fn receive_yield(env: Env, from: Address, amount: i128) { + require_not_paused(&env); require_multisig_disabled(&env); receive_yield_internal(env, from, amount); } @@ -727,6 +730,7 @@ impl InvestmentVault { /// Claim accumulated yield for `from`. Transfers claimable USDC to `from`. pub fn claim_yield(env: Env, from: Address) -> i128 { + require_not_paused(&env); require_current_state(&env); from.require_auth(); let accum: i128 = env @@ -835,6 +839,7 @@ impl InvestmentVault { /// Transfers `amount` from the insurance fund to `recipient`. #[only_owner] pub fn claim_insurance(env: Env, project_id: u32, recipient: Address, amount: i128) { + require_not_paused(&env); require_multisig_disabled(&env); claim_insurance_internal(env, project_id, recipient, amount); } @@ -847,6 +852,7 @@ impl InvestmentVault { amount: i128, approvals: Vec
, ) { + require_not_paused(&env); require_admin_approval(&env, approvals); claim_insurance_internal(env, project_id, recipient, amount); } @@ -1255,6 +1261,7 @@ impl InvestmentVault { /// Mint HBS shares resulting from an authorized cross-chain bridge transfer (#184). pub fn bridge_mint(env: Env, to: Address, amount: i128) { + require_not_paused(&env); require_current_state(&env); let bridge: Address = env .storage() @@ -1272,6 +1279,7 @@ impl InvestmentVault { /// Burn HBS shares to initiate an outbound cross-chain bridge transfer (#184). pub fn bridge_burn(env: Env, from: Address, amount: i128) { + require_not_paused(&env); require_current_state(&env); from.require_auth(); if amount <= 0 { @@ -1347,6 +1355,7 @@ impl InvestmentVault { /// Complete an inbound Wormhole cross-chain bridge transfer using a verified VAA (#184). pub fn complete_bridge_transfer(env: Env, vaa: Bytes) { + require_not_paused(&env); require_current_state(&env); let core: Address = env .storage() @@ -1457,6 +1466,7 @@ impl InvestmentVault { amount: i128, data: Bytes, ) { + require_not_paused(&env); require_current_state(&env); if amount <= 0 { panic!("amount must be positive"); @@ -1576,6 +1586,7 @@ impl InvestmentVault { /// Transfer carbon credits between accounts (#184). pub fn transfer_carbon_credits(env: Env, from: Address, to: Address, amount: i128) { + require_not_paused(&env); require_current_state(&env); from.require_auth(); @@ -2117,6 +2128,14 @@ fn check_deposit_lock(env: &Env, address: &Address) { #[contractimpl] impl InvestmentVault { + /// Pause all privileged state-mutating entry points (#72). + /// + /// When paused, every fund-moving and share-minting/burning entry point + /// (funding, deposits, withdrawals, yield distribution, insurance payouts, + /// bridge mint/burn, flash loans, and carbon-credit transfers) rejects with + /// `VaultError::Paused` via `require_not_paused`. Read-only queries and + /// `unpause` / `emergency_unpause` remain available so the vault can always + /// be resumed. #[only_owner] pub fn pause(env: Env) { env.storage().instance().set(&VaultKey::Paused, &true);