Skip to content
Merged
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
183 changes: 181 additions & 2 deletions packages/contracts/defindex-adapter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ use adapter_common::{
AdapterError,
};
use soroban_sdk::{
auth::{ContractContext, InvokerContractAuthEntry, SubContractInvocation},
contract, contractclient, contracterror, contractimpl, panic_with_error, symbol_short,
token::TokenClient, vec, Address, Env, Symbol, Val, Vec,
token::TokenClient,
vec, Address, Env, IntoVal, Symbol, Val, Vec,
};

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -185,6 +187,20 @@ impl MeridianDefindexAdapter {
let client = DefindexVaultClient::new(&env, &dfx);
let shares_before = client.balance(&adapter);
let min_amount = min_after_slippage(amount).unwrap_or_else(|e| panic_with_error!(&env, e));

let usdc = get_usdc(&env);
env.authorize_as_current_contract(vec![

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This covers deposit. The withdraw path at lib.rs:243 still calls DeFindex's withdraw without the equivalent nested authorization, and no withdraw auth-tree test exercises it.

&env,
InvokerContractAuthEntry::Contract(SubContractInvocation {
context: ContractContext {
contract: usdc,
fn_name: Symbol::new(&env, "transfer"),
args: (adapter.clone(), dfx.clone(), amount).into_val(&env),
},
sub_invocations: Vec::new(&env),
}),
]);

let _ = client.deposit(
&vec![&env, amount],
&vec![&env, min_amount],
Expand Down Expand Up @@ -242,6 +258,26 @@ impl MeridianDefindexAdapter {
None => panic_with_error!(&env, ContractError::MalformedProtocolResponse),
};
let min_out = min_after_slippage(expected).unwrap_or_else(|e| panic_with_error!(&env, e));

// DeFindex's withdraw burns the adapter's dfTokens and calls
// `from.require_auth()` before doing so. Register that authorization
// as the current contract, matching the deposit path's nested-auth
// handling. This must be the last thing before `withdraw()`: the
// tracker is torn down once any sub-invocation returns, so an
// intervening call like `get_asset_amounts_per_shares()` above would
// expire it before `withdraw()` consumes it.
env.authorize_as_current_contract(vec![
&env,
InvokerContractAuthEntry::Contract(SubContractInvocation {
context: ContractContext {
contract: dfx.clone(),
fn_name: Symbol::new(&env, "withdraw"),
args: (shares, vec![&env, min_out], adapter.clone()).into_val(&env),
},
sub_invocations: Vec::new(&env),
}),
]);

let amounts = client.withdraw(&shares, &vec![&env, min_out], &adapter);

let usdc_out: i128 = match amounts.get(0) {
Expand Down Expand Up @@ -319,7 +355,7 @@ mod tests {
use super::*;
use soroban_sdk::{
contract, contractimpl, symbol_short,
testutils::{Address as _, Events, Ledger as _},
testutils::{Address as _, Events, Ledger as _, MockAuth, MockAuthInvoke},
token::{StellarAssetClient, TokenClient},
Address, Env,
};
Expand Down Expand Up @@ -414,6 +450,11 @@ mod tests {
min_amounts_out: Vec<i128>,
from: Address,
) -> Vec<i128> {
// DeFindex's real withdraw burns the caller's dfTokens and calls
// `from.require_auth()` before doing so. Include that check in the
// mock so the auth-tree test exercises the same authorization
// requirement instead of passing on a mock that never asks for it.
from.require_auth();
env.storage()
.instance()
.set(&MDV_WMIN, &min_amounts_out.get(0).unwrap_or(0));
Expand Down Expand Up @@ -497,6 +538,144 @@ mod tests {
(env, vault, usdc_id, adapter, dfx)
}

fn setup_without_mock_auths() -> (
Env,
Address,
Address,
Address,
MeridianDefindexAdapterClient<'static>,
MockDefindexVaultClient<'static>,
) {
let env = Env::default();
let admin = Address::generate(&env);
let vault = Address::generate(&env);

let usdc_id = env
.register_stellar_asset_contract_v2(admin.clone())
.address();

let dfx_id = env.register(MockDefindexVault, ());
let dfx = MockDefindexVaultClient::new(&env, &dfx_id);
dfx.initialize(&usdc_id);

let adapter_id = env.register(
MeridianDefindexAdapter,
(vault.clone(), dfx_id.clone(), usdc_id.clone()),
);
let adapter = MeridianDefindexAdapterClient::new(&env, &adapter_id);

(env, admin, vault, usdc_id, adapter, dfx)
}

// DeFindex's real, enforcing auth-tree check run against everything
// `deposit()` triggers underneath, including the vault's own nested
// `usdc.transfer()` call, exactly like a live network would. Guards
// `authorize_as_current_contract()` above against silently regressing
// into a shape that only passes under the mocked test harness.
#[test]
fn deposit_authorization_tree_matches_the_real_defindex_call_shape() {
let (env, admin, vault, usdc_id, adapter, _dfx) = setup_without_mock_auths();

let mint_amount = 10_000_000_000_i128;
env.mock_auths(&[MockAuth {
address: &admin,
invoke: &MockAuthInvoke {
contract: &usdc_id,
fn_name: "mint",
args: (vault.clone(), mint_amount).into_val(&env),
sub_invokes: &[],
},
}]);
StellarAssetClient::new(&env, &usdc_id).mint(&vault, &mint_amount);

let amount = 100_0000000_i128;

env.mock_auths(&[MockAuth {
address: &vault,
invoke: &MockAuthInvoke {
contract: &usdc_id,
fn_name: "transfer",
args: (vault.clone(), adapter.address.clone(), amount).into_val(&env),
sub_invokes: &[],
},
}]);
TokenClient::new(&env, &usdc_id).transfer(&vault, &adapter.address, &amount);

env.mock_auths(&[MockAuth {
address: &vault,
invoke: &MockAuthInvoke {
contract: &adapter.address,
fn_name: "deposit",
args: (amount,).into_val(&env),
sub_invokes: &[],
},
}]);
let shares = adapter.deposit(&amount);

assert_eq!(shares, amount);
}

// DeFindex's real, enforcing auth-tree check run against everything
// `withdraw()` triggers underneath, including the vault's burn of the
// adapter's dfTokens, exactly like a live network would. Guards
// `authorize_as_current_contract()` above against silently regressing
// into a shape that only passes under the mocked test harness.
#[test]
fn withdraw_authorization_tree_matches_the_real_defindex_call_shape() {
let (env, admin, vault, usdc_id, adapter, _dfx) = setup_without_mock_auths();

let mint_amount = 10_000_000_000_i128;
env.mock_auths(&[MockAuth {
address: &admin,
invoke: &MockAuthInvoke {
contract: &usdc_id,
fn_name: "mint",
args: (vault.clone(), mint_amount).into_val(&env),
sub_invokes: &[],
},
}]);
StellarAssetClient::new(&env, &usdc_id).mint(&vault, &mint_amount);

let amount = 100_0000000_i128;

env.mock_auths(&[MockAuth {
address: &vault,
invoke: &MockAuthInvoke {
contract: &usdc_id,
fn_name: "transfer",
args: (vault.clone(), adapter.address.clone(), amount).into_val(&env),
sub_invokes: &[],
},
}]);
TokenClient::new(&env, &usdc_id).transfer(&vault, &adapter.address, &amount);

env.mock_auths(&[MockAuth {
address: &vault,
invoke: &MockAuthInvoke {
contract: &adapter.address,
fn_name: "deposit",
args: (amount,).into_val(&env),
sub_invokes: &[],
},
}]);
adapter.deposit(&amount);

let recipient = Address::generate(&env);
env.mock_auths(&[MockAuth {
address: &vault,
invoke: &MockAuthInvoke {
contract: &adapter.address,
fn_name: "withdraw",
args: (amount, recipient.clone()).into_val(&env),
sub_invokes: &[],
},
}]);
let usdc_out = adapter.withdraw(&amount, &recipient);

assert_eq!(usdc_out, amount);
assert_eq!(TokenClient::new(&env, &usdc_id).balance(&recipient), amount);
}

#[test]
fn get_pool_returns_the_configured_defindex_vault() {
let (_env, _vault, _usdc, adapter, dfx) = setup();
Expand Down
Loading