Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 60 additions & 5 deletions packages/contracts/defindex-adapter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -52,8 +52,10 @@ 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,
}

impl From<AdapterError> for ContractError {
Expand Down Expand Up @@ -143,7 +145,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.
Expand Down Expand Up @@ -236,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;
Expand Down Expand Up @@ -269,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()
}

Expand Down Expand Up @@ -312,6 +324,17 @@ 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 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<i128> {
// 1:1 valuation, matching the deposit/withdraw rate used above.
vec![&env, desired_shares]
Expand Down Expand Up @@ -538,4 +561,36 @@ 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);

// 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 variants exist and are distinct.
let _ = ContractError::AlreadyInitialized;
let _ = ContractError::Overflow;
let _ = ContractError::NotInitialized;
}
}