From c63120971b31f4f5c127b5909ed55941aed1e420 Mon Sep 17 00:00:00 2001 From: Andreas2410 Date: Fri, 28 Aug 2026 13:04:53 +0100 Subject: [PATCH 1/2] chore(defindex-adapter): add error-handling parity with Blend adapter (#575) Add Overflow, NotInitialized, and MalformedProtocolResponse variants to DefindexAdapter's ContractError enum, matching the Blend adapter's error coverage. Replace unchecked shares_after - shares_before subtraction with checked_sub, panicking with the typed Overflow error on failure. Co-authored-by: Andreas2410 --- .../contracts/defindex-adapter/src/lib.rs | 56 +++++++++++++++++-- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/packages/contracts/defindex-adapter/src/lib.rs b/packages/contracts/defindex-adapter/src/lib.rs index 101cbc7c..13ec4323 100644 --- a/packages/contracts/defindex-adapter/src/lib.rs +++ b/packages/contracts/defindex-adapter/src/lib.rs @@ -4,8 +4,8 @@ use adapter_common::{ get_usdc, require_not_initialized, require_vault_auth, store_vault_and_usdc, AdapterError, }; use soroban_sdk::{ - contract, contractclient, contracterror, contractimpl, symbol_short, token::TokenClient, vec, - Address, Env, Symbol, Val, Vec, + contract, contractclient, contracterror, contractimpl, panic_with_error, symbol_short, + token::TokenClient, vec, Address, Env, Symbol, Val, Vec, }; // --------------------------------------------------------------------------- @@ -52,8 +52,12 @@ pub trait DefindexVaultInterface { pub enum ContractError { /// `initialize` was called on an adapter that already has a vault set. AlreadyInitialized = 1, + /// An intermediate arithmetic operation would overflow `i128`. + Overflow = 2, /// A state-mutating call was made before `initialize`. - NotInitialized = 2, + NotInitialized = 3, + /// The DeFindex vault returned a response that could not be interpreted. + MalformedProtocolResponse = 4, } impl From for ContractError { @@ -143,7 +147,10 @@ impl MeridianDefindexAdapter { let _ = client.deposit(&vec![&env, amount], &vec![&env, 0_i128], &adapter, &true); let shares_after = client.balance(&adapter); - shares_after - shares_before + match shares_after.checked_sub(shares_before) { + Some(delta) => delta, + None => panic_with_error!(&env, ContractError::Overflow), + } } /// Called by the vault to redeem `shares` dfTokens from the DeFindex vault. @@ -312,6 +319,13 @@ mod tests { env.storage().instance().get(&MDV_SH).unwrap_or(0) } + // Allows tests to directly set the reported balance, to simulate + // an external vault returning an unexpected balance (e.g. balance + // decreasing after a deposit, which would trigger the Overflow path). + pub fn set_balance(env: Env, balance: i128) { + env.storage().instance().set(&MDV_SH, &balance); + } + pub fn get_asset_amounts_per_shares(env: Env, desired_shares: i128) -> Vec { // 1:1 valuation, matching the deposit/withdraw rate used above. vec![&env, desired_shares] @@ -538,4 +552,38 @@ mod tests { let recipient = Address::generate(&env); adapter.withdraw(&100_0000000_i128, &recipient); } + + #[test] + #[should_panic(expected = "Error(Contract, #2)")] + fn deposit_panics_with_overflow_when_balance_decreases() { + // Simulates a buggy or malicious external vault whose balance() + // returns a lower value after deposit than before. The unchecked + // subtraction would have panicked with an opaque trap; now it + // panics with the typed Overflow error (discriminant #2). + let (env, vault, usdc_id, adapter, dfx) = setup(); + let amount = 100_0000000_i128; + + TokenClient::new(&env, &usdc_id).transfer(&vault, &adapter.address, &amount); + + // First deposit succeeds normally. + adapter.deposit(&amount); + + // Tamper: set the mock balance to a value lower than what the + // adapter's own bookkeeping expects, so the next deposit sees + // shares_after < shares_before. + dfx.set_balance(&0); + + adapter.deposit(&amount); + } + + #[test] + fn contract_error_has_expected_variants() { + // Compile-time check that the new variants exist and are + // distinct. Ensures MalformedProtocolResponse (reserved for the + // total_assets zero-on-malformed-response fix) is available. + let _ = ContractError::AlreadyInitialized; + let _ = ContractError::Overflow; + let _ = ContractError::NotInitialized; + let _ = ContractError::MalformedProtocolResponse; + } } From 017f8e7e3334e9a39f9871a6476a14dea47af6e8 Mon Sep 17 00:00:00 2001 From: Andreas2410 Date: Sun, 30 Aug 2026 09:41:18 +0100 Subject: [PATCH 2/2] fix(defindex-adapter): repair overflow test and drop dead MalformedProtocolResponse --- .../contracts/defindex-adapter/src/lib.rs | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/packages/contracts/defindex-adapter/src/lib.rs b/packages/contracts/defindex-adapter/src/lib.rs index 13ec4323..449255d4 100644 --- a/packages/contracts/defindex-adapter/src/lib.rs +++ b/packages/contracts/defindex-adapter/src/lib.rs @@ -56,8 +56,6 @@ pub enum ContractError { Overflow = 2, /// A state-mutating call was made before `initialize`. NotInitialized = 3, - /// The DeFindex vault returned a response that could not be interpreted. - MalformedProtocolResponse = 4, } impl From for ContractError { @@ -243,6 +241,7 @@ mod tests { const MDV_USDC: Symbol = symbol_short!("MDV_USDC"); const MDV_SH: Symbol = symbol_short!("MDV_SH"); const MDV_WAMT: Symbol = symbol_short!("MDV_WAMT"); + const MDV_FAULTY: Symbol = symbol_short!("MDV_FT"); #[contract] pub struct MockDefindexVault; @@ -276,8 +275,14 @@ mod tests { let amount = amounts_desired.get(0).unwrap_or(0); TokenClient::new(&env, &usdc).transfer(&from, &env.current_contract_address(), &amount); + let faulty: bool = env.storage().instance().get(&MDV_FAULTY).unwrap_or(false); let prev: i128 = env.storage().instance().get(&MDV_SH).unwrap_or(0); - env.storage().instance().set(&MDV_SH, &(prev + amount)); + if faulty { + // Simulate a buggy vault whose balance decreases after a deposit. + env.storage().instance().set(&MDV_SH, &0); + } else { + env.storage().instance().set(&MDV_SH, &(prev + amount)); + } Val::VOID.into() } @@ -326,6 +331,10 @@ mod tests { env.storage().instance().set(&MDV_SH, &balance); } + pub fn set_faulty(env: Env, faulty: bool) { + env.storage().instance().set(&MDV_FAULTY, &faulty); + } + pub fn get_asset_amounts_per_shares(env: Env, desired_shares: i128) -> Vec { // 1:1 valuation, matching the deposit/withdraw rate used above. vec![&env, desired_shares] @@ -568,22 +577,20 @@ mod tests { // First deposit succeeds normally. adapter.deposit(&amount); - // Tamper: set the mock balance to a value lower than what the - // adapter's own bookkeeping expects, so the next deposit sees - // shares_after < shares_before. - dfx.set_balance(&0); + // Fund the adapter for the second deposit, then flip the mock into + // faulty mode: deposit will reset the share balance to zero, + // so shares_after (0) < shares_before (amount) → Overflow. + TokenClient::new(&env, &usdc_id).transfer(&vault, &adapter.address, &amount); + dfx.set_faulty(&true); adapter.deposit(&amount); } #[test] fn contract_error_has_expected_variants() { - // Compile-time check that the new variants exist and are - // distinct. Ensures MalformedProtocolResponse (reserved for the - // total_assets zero-on-malformed-response fix) is available. + // Compile-time check that the variants exist and are distinct. let _ = ContractError::AlreadyInitialized; let _ = ContractError::Overflow; let _ = ContractError::NotInitialized; - let _ = ContractError::MalformedProtocolResponse; } }