diff --git a/packages/contracts/blend-adapter/src/lib.rs b/packages/contracts/blend-adapter/src/lib.rs index 0702b4f9..d142d39e 100644 --- a/packages/contracts/blend-adapter/src/lib.rs +++ b/packages/contracts/blend-adapter/src/lib.rs @@ -368,15 +368,25 @@ impl MeridianBlendAdapter { Ok(()) } - /// Refreshes the cached total_assets to include yield accrued since the - /// last call, satisfying the shared YieldAdapterInterface contract. - /// Currently just calls accrue(), which remains a public, - /// permissionless entry point in its own right. - /// - /// Panics on failure rather than returning a Result: the shared adapter - /// interface's refresh() has no error return, so propagating accrue()'s - /// Result would mean changing that interface's ABI across both adapters - /// and the vault's calls into them. Panicking here instead of silently + pub fn total_shares(env: Env) -> i128 { + let pool: Address = adapter_common::get_or_not_initialized::<_, ContractError>( + &env, + env.storage().instance().get(&POOL_KEY), + ); + let usdc = get_usdc(&env); + let client = BlendPoolClient::new(&env, &pool); + let index = client.get_reserve(&usdc).config.index; + client + .get_positions(&env.current_contract_address()) + .collateral + .get(index) + .unwrap_or(0) + } + + /// Refreshes the adapter's cached total_assets by attempting to accrue yield. + /// A failure to accrue (e.g., if the underlying pool reverts) must bubble up + /// rather than being swallowed, to prevent the vault from operating on stale + /// pricing. This adapter relies on the standard panic handler: bubbling by /// discarding the error preserves this function's pre-existing /// fail-loud behaviour (accrue()'s storage read used to be a bare /// unwrap(), which panicked directly) rather than downgrading a real diff --git a/packages/contracts/defindex-adapter/src/lib.rs b/packages/contracts/defindex-adapter/src/lib.rs index 101cbc7c..2df547cf 100644 --- a/packages/contracts/defindex-adapter/src/lib.rs +++ b/packages/contracts/defindex-adapter/src/lib.rs @@ -193,6 +193,16 @@ impl MeridianDefindexAdapter { amounts.get(0).unwrap_or(0) } + pub fn total_shares(env: Env) -> i128 { + let dfx: Address = adapter_common::get_or_not_initialized::<_, ContractError>( + &env, + env.storage().instance().get(&DFX_VAULT), + ); + let adapter = env.current_contract_address(); + let client = DefindexVaultClient::new(&env, &dfx); + client.balance(&adapter) + } + /// No-op: DeFindex's total_assets() already prices live on every call /// via the vault's exchange rate, so there is no cache to refresh. pub fn refresh(_env: Env) {} diff --git a/packages/contracts/vault/src/lib.rs b/packages/contracts/vault/src/lib.rs index d3a606a4..505b5cdf 100644 --- a/packages/contracts/vault/src/lib.rs +++ b/packages/contracts/vault/src/lib.rs @@ -40,6 +40,10 @@ pub trait YieldAdapterInterface { fn deposit(env: Env, amount: i128) -> i128; fn withdraw(env: Env, shares: i128, recipient: Address) -> i128; fn total_assets(env: Env) -> i128; + /// The adapter's current protocol-share balance, read from the underlying + /// protocol's own ledger rather than self-tracked. Lets the vault reconcile + /// ADPT_SH instead of estimating its decrements. + fn total_shares(env: Env) -> i128; /// Refreshes the adapter's cached total_assets before it is read for /// deposit/withdraw pricing. A no-op for adapters that already price /// live on every call. @@ -241,7 +245,7 @@ impl MeridianVault { .set(&TOTAL_SH, &(total_shares + shares_to_mint)); env.storage() .instance() - .set(&ADPT_SH, &(total_adapter_shares + adapter_shares)); + .set(&ADPT_SH, &AdapterClient::new(&env, &adapter_addr).total_shares()); // Stamp the entry time on the caller's first deposit; top-ups keep // the original time. Keyed off whether an entry record exists rather @@ -354,7 +358,7 @@ impl MeridianVault { .set(&TOTAL_SH, &(total_shares - shares)); env.storage() .instance() - .set(&ADPT_SH, &(total_adapter_shares - adapter_shares_to_burn)); + .set(&ADPT_SH, &AdapterClient::new(&env, &adapter_addr).total_shares()); let remaining = caller_shares - shares; @@ -809,6 +813,10 @@ mod tests { mock_total_assets(&env, &usdc) } + pub fn total_shares(env: Env) -> i128 { + env.storage().instance().get(&MA_SH).unwrap_or(0) + } + pub fn refresh(_env: Env) { // No-op: MockAdapter already prices total_assets() live. } @@ -868,6 +876,10 @@ mod tests { mock_total_assets(&env, &usdc) } + pub fn total_shares(env: Env) -> i128 { + env.storage().instance().get(&LA_SH).unwrap_or(0) + } + pub fn refresh(_env: Env) { // No-op: LossyMockAdapter already prices total_assets() live. } @@ -919,6 +931,10 @@ mod tests { mock_total_assets(&env, &usdc) } + pub fn total_shares(env: Env) -> i128 { + env.storage().instance().get(&ZS_SH).unwrap_or(0) + } + pub fn refresh(_env: Env) { // No-op: ZeroShareMockAdapter already prices total_assets() live. } @@ -994,6 +1010,10 @@ mod tests { env.storage().instance().get(&CM_TOTAL).unwrap_or(0) } + pub fn total_shares(env: Env) -> i128 { + env.storage().instance().get(&CM_SH).unwrap_or(0) + } + pub fn refresh(env: Env) { // USDC address is always set in initialize(), so this is safe. let usdc: Address =