From 074d521c7cabd29c3e74584c0dd18f9e554a9757 Mon Sep 17 00:00:00 2001 From: laurentketterle-hub Date: Thu, 13 Aug 2026 04:57:46 +0200 Subject: [PATCH] fix: authorize flash loan repayment (#315) and restore compilation --- investment_vault/src/events.rs | 4 ++++ investment_vault/src/lib.rs | 24 ++++++++++++++++-------- investment_vault/src/test.rs | 2 ++ investment_vault/src/types.rs | 4 ++-- 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/investment_vault/src/events.rs b/investment_vault/src/events.rs index 03570e06..f4cb30b1 100644 --- a/investment_vault/src/events.rs +++ b/investment_vault/src/events.rs @@ -504,6 +504,8 @@ 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 +520,8 @@ 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..ce2ea7ed 100644 --- a/investment_vault/src/lib.rs +++ b/investment_vault/src/lib.rs @@ -1127,6 +1127,8 @@ 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 +1181,8 @@ 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. @@ -1475,8 +1479,18 @@ impl InvestmentVault { panic!("flash loan callback failed"); } - Base::transfer(&env, &borrower, &MuxedAddress::from(&vault), amount + fee); - Base::burn(&env, &vault, amount); + // Repayment (#315): pull `amount + fee` shares back from the borrower. + // `Base::transfer` would call `borrower.require_auth()`, which a + // *contract* borrower cannot satisfy once its `flash_loan_callback` has + // already returned — the repayment therefore failed on a real network + // and only passed under `mock_all_auths()`. The borrower's consent is + // established by returning `true` from the callback in this same + // transaction, so the vault (as the token contract) moves the shares + // directly via the unauthenticated `Base::update` primitive. + Base::update(&env, Some(&borrower), Some(&vault), amount + fee); + // Burn the principal from the vault's own balance, leaving the fee as + // the vault's flash-loan revenue. + Base::update(&env, Some(&vault), None, amount); events::flash_loan(&env, &initiator, &borrower, amount, fee); } @@ -2092,12 +2106,6 @@ fn check_deposit_lock(env: &Env, address: &Address) { .persistent() .get::<_, u64>(&VaultKey::LastDeposit(address.clone())) { - let window: u32 = env - .storage() - .instance() - .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/test.rs b/investment_vault/src/test.rs index 4be8ed36..a71cb09e 100644 --- a/investment_vault/src/test.rs +++ b/investment_vault/src/test.rs @@ -2586,6 +2586,8 @@ fn test_volume_fee_tier_is_admin_only() { }, }]); s.vault_client.set_volume_fee_tier(&500_0000000i128, &50u32); +} + // ── #179: convert_to_shares() overflow guard on extremely large deposits ────── /// Verify that `convert_to_shares` panics (rather than silently wrapping) when diff --git a/investment_vault/src/types.rs b/investment_vault/src/types.rs index e09392d3..4fa17585 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]