Skip to content
Open
Show file tree
Hide file tree
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
67 changes: 57 additions & 10 deletions packages/contracts/blend-adapter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,18 @@ fn b_tokens_to_usdc(b_tokens: i128, b_rate: i128) -> Result<i128, ContractError>
.ok_or(ContractError::Overflow)
}

// Reads an instance-storage value, panicking with the typed NotInitialized
// error instead of an opaque unwrap trap if it's unset. In practice this
// branch is unreachable on any contract deployed via __constructor (see its
// doc comment), since POOL_KEY is always set before any other method is
// reachable; this exists as a defensive, correctly-typed fallback rather
// than a path expected to actually fire. Collapses what was previously a
// repeated 6-line `unwrap_or_else(|| { panic_with_error!(...) })` block into
// one call site per use.
fn get_or_not_initialized<T>(env: &Env, value: Option<T>) -> T {
value.unwrap_or_else(|| panic_with_error!(env, ContractError::NotInitialized))
}

// ---------------------------------------------------------------------------
// Blend pool interface types
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -133,6 +145,8 @@ pub enum ContractError {
AlreadyInitialized = 1,
/// An intermediate arithmetic operation would overflow `i128`.
Overflow = 2,
/// A state-mutating call was made before `initialize`.
NotInitialized = 3,
}

impl From<AdapterError> for ContractError {
Expand Down Expand Up @@ -207,7 +221,7 @@ impl MeridianBlendAdapter {
pub fn deposit(env: Env, amount: i128) -> i128 {
require_vault_auth(&env);

let pool: Address = env.storage().instance().get(&POOL_KEY).unwrap();
let pool: Address = get_or_not_initialized(&env, env.storage().instance().get(&POOL_KEY));
let usdc = get_usdc(&env);

let adapter = env.current_contract_address();
Expand All @@ -231,6 +245,7 @@ impl MeridianBlendAdapter {

let client = BlendPoolClient::new(&env, &pool);
let index = client.get_reserve(&usdc).config.index;
// Map.get() safely returns Option, defaulting to 0 if the index doesn't exist.
let b_tokens_before = client
.get_positions(&adapter)
.collateral
Expand All @@ -251,13 +266,17 @@ impl MeridianBlendAdapter {
],
);

// Map.get() safely returns Option, defaulting to 0 if the index doesn't exist.
let b_tokens_after = client
.get_positions(&adapter)
.collateral
.get(index)
.unwrap_or(0);
let b_tokens_credited = b_tokens_after - b_tokens_before;

// Instance storage read defaults to 0 if TOTAL_KEY hasn't been set, which is safe since
// initialize() sets this key to 0. This unwrap_or pattern is the idiomatic way to handle
// optional storage values in Soroban.
let prev: i128 = env.storage().instance().get(&TOTAL_KEY).unwrap_or(0);
env.storage().instance().set(&TOTAL_KEY, &(prev + amount));

Expand All @@ -272,7 +291,7 @@ impl MeridianBlendAdapter {
pub fn withdraw(env: Env, shares: i128, recipient: Address) -> i128 {
require_vault_auth(&env);

let pool: Address = env.storage().instance().get(&POOL_KEY).unwrap();
let pool: Address = get_or_not_initialized(&env, env.storage().instance().get(&POOL_KEY));
let usdc = get_usdc(&env);

let adapter = env.current_contract_address();
Expand Down Expand Up @@ -304,6 +323,9 @@ impl MeridianBlendAdapter {
let after = usdc_client.balance(&recipient);
let delivered = after - before;

// Instance storage read defaults to 0 if TOTAL_KEY hasn't been set, which is safe since
// initialize() sets this key to 0. This unwrap_or pattern is the idiomatic way to handle
// optional storage values in Soroban.
let prev: i128 = env.storage().instance().get(&TOTAL_KEY).unwrap_or(0);
let remaining = if prev > delivered {
prev - delivered
Expand All @@ -326,13 +348,18 @@ impl MeridianBlendAdapter {
/// (`get_positions`) rather than self-tracking it, so there is no risk of
/// drift between the stored total and Blend's actual accounting.
pub fn accrue(env: Env) -> Result<(), ContractError> {
let pool: Address = env.storage().instance().get(&POOL_KEY).unwrap();
let pool: Address = env
.storage()
.instance()
.get(&POOL_KEY)
.ok_or(ContractError::NotInitialized)?;
let usdc = get_usdc(&env);
let adapter = env.current_contract_address();

let client = BlendPoolClient::new(&env, &pool);
let reserve = client.get_reserve(&usdc);
let positions = client.get_positions(&adapter);
// Map.get() safely returns Option, defaulting to 0 if the index doesn't exist.
let b_tokens = positions.collateral.get(reserve.config.index).unwrap_or(0);

let current_value = b_tokens_to_usdc(b_tokens, reserve.data.b_rate)?;
Expand All @@ -345,6 +372,17 @@ impl MeridianBlendAdapter {
/// last call, satisfying the shared YieldAdapterInterface contract.
/// Currently just calls accrue(), which remains a public,
/// permissionless entry point in its own right.
///
/// Discards accrue()'s Result rather than propagating it: the shared
/// adapter interface's refresh() has no error return, so surfacing a
/// failure here would mean changing that interface's ABI across both
/// adapters and the vault's calls into it. In practice the only error
/// accrue() can return here, NotInitialized, is unreachable on any
/// contract deployed via __constructor (see its doc comment), so nothing
/// is being silently lost on a real deployment; Overflow is the one
/// error that could genuinely fire, and a caller relying on refresh()
/// alone would not see it. Call accrue() directly instead of refresh()
/// where the Result matters.
#[allow(unused_must_use)]
pub fn refresh(env: Env) {
Self::accrue(env);
Expand All @@ -354,12 +392,15 @@ impl MeridianBlendAdapter {
/// yield only as of the last `accrue()` call; call `accrue()` first for a
/// value that includes interest accrued since then.
pub fn total_assets(env: Env) -> i128 {
// Instance storage read defaults to 0 if TOTAL_KEY hasn't been set, which is safe since
// initialize() sets this key to 0. This unwrap_or pattern is the idiomatic way to handle
// optional storage values in Soroban.
env.storage().instance().get(&TOTAL_KEY).unwrap_or(0)
}

/// Returns the Blend pool this adapter supplies to.
pub fn get_pool(env: Env) -> Address {
env.storage().instance().get(&POOL_KEY).unwrap()
get_or_not_initialized(&env, env.storage().instance().get(&POOL_KEY))
}

/// Returns "blend", identifying which protocol this adapter wraps.
Expand Down Expand Up @@ -427,8 +468,10 @@ mod tests {
to: Address,
requests: Vec<Request>,
) -> Val {
let scalar: i128 = env.storage().instance().get(&M_SCALAR).unwrap();
let rate: i128 = env.storage().instance().get(&M_RATE).unwrap();
// Scalar and rate are always set in initialize(), so these are safe.
let scalar: i128 =
get_or_not_initialized(&env, env.storage().instance().get(&M_SCALAR));
let rate: i128 = get_or_not_initialized(&env, env.storage().instance().get(&M_RATE));
let mut collateral: i128 = env.storage().instance().get(&M_COLLAT).unwrap_or(0);

for req in requests.iter() {
Expand All @@ -453,14 +496,16 @@ mod tests {
}

pub fn get_reserve(env: Env, asset: Address) -> Reserve {
let internal_scalar: i128 = env.storage().instance().get(&M_SCALAR).unwrap();
// Scalar and rate are always set in initialize(), so these are safe.
let internal_scalar: i128 =
get_or_not_initialized(&env, env.storage().instance().get(&M_SCALAR));
let scalar: i128 = env
.storage()
.instance()
.get(&M_REP_SCL)
.unwrap_or(internal_scalar);
let rate: i128 = env.storage().instance().get(&M_RATE).unwrap();
let index: u32 = env.storage().instance().get(&M_INDEX).unwrap();
let rate: i128 = get_or_not_initialized(&env, env.storage().instance().get(&M_RATE));
let index: u32 = get_or_not_initialized(&env, env.storage().instance().get(&M_INDEX));
Reserve {
asset,
config: ReserveConfig {
Expand Down Expand Up @@ -492,7 +537,9 @@ mod tests {
}

pub fn get_positions(env: Env, _address: Address) -> Positions {
let index: u32 = env.storage().instance().get(&M_INDEX).unwrap();
// Index is always set in initialize(), so this is safe.
let index: u32 = get_or_not_initialized(&env, env.storage().instance().get(&M_INDEX));
// Collateral safely defaults to 0 if not set yet, which is correct for a fresh adapter.
let collateral: i128 = env.storage().instance().get(&M_COLLAT).unwrap_or(0);
let mut collateral_map = Map::new(&env);
collateral_map.set(index, collateral);
Expand Down
40 changes: 32 additions & 8 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 All @@ -14,6 +14,18 @@ use soroban_sdk::{

const DFX_VAULT: Symbol = symbol_short!("DFXVAULT");

// Reads an instance-storage value, panicking with the typed NotInitialized
// error instead of an opaque unwrap trap if it's unset. In practice this
// branch is unreachable on any contract deployed via __constructor (see its
// doc comment), since DFX_VAULT is always set before any other method is
// reachable; this exists as a defensive, correctly-typed fallback rather
// than a path expected to actually fire. Collapses what was previously a
// repeated 6-line `unwrap_or_else(|| { panic_with_error!(...) })` block into
// one call site per use.
fn get_or_not_initialized<T>(env: &Env, value: Option<T>) -> T {
value.unwrap_or_else(|| panic_with_error!(env, ContractError::NotInitialized))
}

// ---------------------------------------------------------------------------
// DeFindex vault interface
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -52,6 +64,8 @@ pub trait DefindexVaultInterface {
pub enum ContractError {
/// `initialize` was called on an adapter that already has a vault set.
AlreadyInitialized = 1,
/// A state-mutating call was made before `initialize`.
NotInitialized = 2,
}

impl From<AdapterError> for ContractError {
Expand Down Expand Up @@ -124,7 +138,7 @@ impl MeridianDefindexAdapter {
pub fn deposit(env: Env, amount: i128) -> i128 {
require_vault_auth(&env);

let dfx: Address = env.storage().instance().get(&DFX_VAULT).unwrap();
let dfx: Address = get_or_not_initialized(&env, env.storage().instance().get(&DFX_VAULT));
let adapter = env.current_contract_address();

let client = DefindexVaultClient::new(&env, &dfx);
Expand All @@ -141,13 +155,15 @@ impl MeridianDefindexAdapter {
pub fn withdraw(env: Env, shares: i128, recipient: Address) -> i128 {
require_vault_auth(&env);

let dfx: Address = env.storage().instance().get(&DFX_VAULT).unwrap();
let dfx: Address = get_or_not_initialized(&env, env.storage().instance().get(&DFX_VAULT));
let usdc = get_usdc(&env);
let adapter = env.current_contract_address();

let amounts =
DefindexVaultClient::new(&env, &dfx).withdraw(&shares, &vec![&env, 0_i128], &adapter);

// Vec.get() returns Option which safely defaults to 0 if the vector doesn't contain
// index 0 or is empty, so this unwrap_or is safe and intentional.
let usdc_out: i128 = amounts.get(0).unwrap_or(0);
if usdc_out > 0 {
TokenClient::new(&env, &usdc).transfer(&adapter, &recipient, &usdc_out);
Expand All @@ -159,7 +175,7 @@ impl MeridianDefindexAdapter {
/// Live USDC value of the adapter's dfToken position, computed by the
/// DeFindex vault's exchange rate. Updates automatically as yield accrues.
pub fn total_assets(env: Env) -> i128 {
let dfx: Address = env.storage().instance().get(&DFX_VAULT).unwrap();
let dfx: Address = get_or_not_initialized(&env, env.storage().instance().get(&DFX_VAULT));
let adapter = env.current_contract_address();

let client = DefindexVaultClient::new(&env, &dfx);
Expand All @@ -169,6 +185,8 @@ impl MeridianDefindexAdapter {
}

let amounts = client.get_asset_amounts_per_shares(&shares);
// Vec.get() returns Option which safely defaults to 0 if the vector doesn't contain
// index 0 or is empty, so this unwrap_or is safe and intentional.
amounts.get(0).unwrap_or(0)
}

Expand All @@ -178,7 +196,7 @@ impl MeridianDefindexAdapter {

/// Returns the DeFindex vault this adapter deposits into.
pub fn get_pool(env: Env) -> Address {
env.storage().instance().get(&DFX_VAULT).unwrap()
get_or_not_initialized(&env, env.storage().instance().get(&DFX_VAULT))
}

/// Returns "defindex", identifying which protocol this adapter wraps.
Expand Down Expand Up @@ -236,7 +254,10 @@ mod tests {
from: Address,
_invest: bool,
) -> Val {
let usdc: Address = env.storage().instance().get(&MDV_USDC).unwrap();
// USDC address is always set in initialize(), so this is safe.
let usdc: Address =
get_or_not_initialized(&env, env.storage().instance().get(&MDV_USDC));
// Vec.get() safely returns Option which defaults to 0, so unwrap_or is safe.
let amount = amounts_desired.get(0).unwrap_or(0);
TokenClient::new(&env, &usdc).transfer(&from, &env.current_contract_address(), &amount);

Expand All @@ -262,7 +283,10 @@ mod tests {
.get(&MDV_WAMT)
.unwrap_or_else(|| vec![&env, withdraw_shares]);

let usdc: Address = env.storage().instance().get(&MDV_USDC).unwrap();
// USDC address is always set in initialize(), so this is safe.
let usdc: Address =
get_or_not_initialized(&env, env.storage().instance().get(&MDV_USDC));
// Vec.get() safely returns Option which defaults to 0, so unwrap_or is safe.
let payout = amounts.get(0).unwrap_or(0);
if payout > 0 {
TokenClient::new(&env, &usdc).transfer(
Expand Down
Loading
Loading