This document describes how the NeuroWealth Vault integrates with a Stellar DEX liquidity pool for the Balanced and Growth yield strategies. It covers the on-chain interface, strategy switching behaviour, integration assumptions, and operational considerations for external integrators.
It mirrors the patterns established in BLEND_INTEGRATION_RESEARCH.md.
The vault supports three yield protocols selectable by the AI agent:
| Protocol symbol | Strategy | Behaviour |
|---|---|---|
"none" |
Conservative (idle) | Funds held as USDC inside the vault. No yield deployed. |
"blend" |
Conservative / Balanced | USDC supplied to Blend lending pool; yield accrues via bToken exchange rate. |
"dex" |
Balanced / Growth | USDC deployed to a DEX liquidity pool via a single-asset adapter. |
The agent calls rebalance(protocol, expected_apy, min_out) to switch. Only
one protocol is active at a time; switching exits the current protocol first,
then enters the new one. If either leg fails, the rebalance aborts via
RebalanceFailedEvent and the active protocol is unchanged.
The following assumptions must hold for the DEX integration to function correctly. Integrators deploying a production adapter should validate each one before going live:
-
Single-asset adapter contract. Real Stellar AMMs (Soroswap, Aquarius, Comet) are two-asset constant-product pools. Deploying single-sided USDC requires a thin adapter that wraps the AMM's
deposit/withdrawand exposes the three entrypoints below. The vault is intentionally decoupled from AMM specifics and calls onlyadd_liquidity,remove_liquidity, andbalance. -
USDC is the sole asset the vault touches. The adapter is responsible for all swap legs, zaps, or LP share conversions. The vault measures outcome by its own USDC balance delta, not by any value the adapter reports. A misreporting adapter cannot inflate vault accounting.
-
Atomic pool operations.
add_liquidityandremove_liquiditymust complete atomically within a single Soroban invocation. Partial fills that silently succeed are indistinguishable from full fills and will lead to accounting drift; the adapter must either fill in full or revert. -
balancereturns the current USDC-equivalent position. The vault readsbalance(asset, user)to verify that the deployment succeeded. If the pool quotes LP shares rather than USDC, the adapter must convert internally. -
transfer_fromauthorisation is handled by the vault. The vault pre-approves the pool for the supply amount with a TTL equal toDataKey::DexApprovalTtl. The adapter must not modify or re-use that approval for any purpose other than the requested supply. -
min_outis treated as a hard floor. If the pool cannot realise at leastmin_outUSDC on a supply or withdraw leg, the vault reverts withVaultError::MinOutNotMet(#42). Pools that silently under-fill will trigger this revert. Passmin_out = 0only in tests or when slippage is genuinely irrelevant. -
The pool address must be registered before first use. The owner calls
set_dex_pool(owner, pool_address)once. The vault probesbalanceat registration time; a non-conforming address reverts at that point, not at first rebalance. -
Liquidity routing is exit-first. When switching from Blend to DEX, the vault withdraws all Blend funds and holds them idle before deploying to DEX. The vault does not support simultaneous multi-protocol deployment.
The vault deploys a single asset (USDC) into a DEX liquidity pool. The pool is modelled as a single-asset liquidity adapter exposing three entrypoints:
| Entrypoint | Purpose |
|---|---|
add_liquidity(from, asset, amount, min_out) |
Supply USDC liquidity after approve |
remove_liquidity(to, asset, amount, min_out) |
Withdraw USDC liquidity |
balance(asset, user) |
The vault's current liquidity position |
min_out is the caller's slippage floor for the leg. The vault forwards it to
the pool and independently enforces it on the realized amount (see
Slippage), so a partial fill is rejected even if the
pool ignores the hint.
Implementation: DexPoolClient in
neurowealth-vault/contracts/vault/src/lib.rs.
Note on production pools. Real Stellar AMMs (e.g. Soroswap pairs, Aquarius, Comet) are two-asset constant-product pools whose
deposit/swap/withdrawsignatures differ from the single-asset adapter above. Deploying single-sided USDC into such a pool requires an adapter contract that performs the swap/zap and returns the realized USDC value. The vault is intentionally decoupled from that detail: it callsadd_liquidity/remove_liquidity/balanceon whateverDexPooladdress the owner registers, and measures results by its own USDC balance delta. The configured address is expected to be either a compatible pool or a thin adapter implementing these three entrypoints.
env.invoke_contract::<Val>(
&pool_address,
&Symbol::new(env, "add_liquidity"),
args,
);Supply flow (mirrors Blend):
- Vault
approves the DEX pool for the supply amount (TTL =ApprovalTtl). - Vault authorizes
add_liquiditywith atransfer_fromsub-invocation. - Pool pulls USDC via
transfer_from. - Vault computes the realized amount from its own USDC balance delta.
Withdraw flow:
- Vault calls
remove_liquidity(amount0⇒ withdraw the full position). - Pool transfers USDC back to the vault.
- Vault computes the realized amount from its balance delta.
pub fn set_dex_pool(env: Env, owner: Address, pool_address: Address);
pub fn get_dex_pool(env: Env) -> Option<Address>;- Owner-only. The pool interface is validated by probing
balancebefore the address is stored (an invalid pool reverts at configuration time). - Stored under
DataKey::DexPool; emitsDexPoolConfiguredEvent(dex_cfg).
pub fn rebalance(env: Env, protocol: Symbol, expected_apy: i128, min_out: i128);- Supported
protocolsymbols:"blend","dex","none". - Switching from one protocol to another exits the current one first; an
incomplete exit emits
RebalanceFailedEventand aborts without mutating state. min_out: minimum assets per supply/withdraw leg;0disables the check.RebalanceEvent.status == "noop": no funds moved.
DataKey::CurrentProtocol:
"none": Funds idle in the vault (or not deployed)."blend": Funds deployed to Blend."dex": Funds deployed to the DEX pool.
ProtocolChangedEvent (proto_chg) is emitted whenever CurrentProtocol
changes, including transitions into and out of "dex".
On every DEX leg, min_out is enforced by require_min_out:
- Supply: if the realized supplied amount
< min_out, the call reverts withVaultError::MinOutNotMet(#42). - Withdraw: if the realized withdrawn amount
< min_out, the call reverts with the same error.
min_out == 0 disables the check.
| Event | Topic | When |
|---|---|---|
DexSupplyEvent |
dex_sup |
USDC supplied to the DEX pool |
DexWithdrawEvent |
dex_wd |
USDC withdrawn from the DEX pool |
DexPoolConfiguredEvent |
dex_cfg |
DEX pool address configured |
See EVENTS.md for full schemas.
| Layer | Command |
|---|---|
| Unit / mock pool | cargo test -p neurowealth-vault |
| DEX interface (feature) | cargo test -p neurowealth-vault --features dex-devnet |
tests/test_dex_integration.rs uses an in-env MockDexPool (same pattern as
MockBlendPool) and covers: supply/withdraw via rebalance, balance reads,
CurrentProtocol/ProtocolChangedEvent tracking, min_out enforcement,
Blend↔DEX switching, and user withdrawals that pull liquidity back from the DEX.
Manual devnet smoke (replace addresses):
soroban contract invoke --id "$DEX_POOL" --network testnet -- balance \
--asset "$USDC" --user "$VAULT"- Slippage:
min_outguard on every DEX supply/withdraw leg. - Incomplete exit: rebalance aborts (via
RebalanceFailedEvent) if a protocol switch cannot withdraw the full deployed balance. - Pool validation:
set_dex_poolprobesbalancebefore storing the address, rejecting non-conforming contracts. - Balance-delta accounting: realized amounts are derived from the vault's own USDC balance, not the pool's return value, so a misreporting pool cannot inflate accounting.
An observer watching the mempool can identify a pending rebalance() call before
it lands on-chain. The attacker may then:
- Front-run: Submit a transaction that increases the pool price before the vault's DEX swap leg executes.
- Execute the vault rebalance: The vault supplies USDC at the inflated price, receiving fewer LP shares or less collateral per unit.
- Back-run: Reverse the attacker's initial trade, profiting from the price difference while the vault absorbs the slippage.
Assume a DEX pool has a USDC-BLND pair with a spot rate of 100 USDC : 10 BLND.
- Attacker observes a pending
rebalance()that will supply 1,000,000 USDC. - Attacker front-runs with a large buy of BLND (1M USDC), pushing the rate to 100 USDC : 8 BLND (20% slippage).
- Vault's
rebalance()executes: supplies 1,000,000 USDC but receives only 80,000 BLND instead of the expected 100,000 (loss of 20,000 BLND ≈ 200k USDC). - Attacker back-runs: sells the BLND bought in step 1, pocketing roughly 180k USDC profit (after reverting the price and deducting attacker's gas).
The agent backend must compute min_out off-chain before submitting the
rebalance() call to Soroban. The recommended approach:
- Query the pool's current state (exchange rate, liquidity depth).
- Simulate the desired supply/withdraw size to estimate the output.
- Apply a conservative slippage tolerance (e.g., 0.5% - 2%, depending on pool depth).
- Set
min_out = simulated_output * (1 - tolerance). - Include a freshness check: only proceed if the pool state has not aged more than 1-2 blocks; re-query if stale.
The vault enforces min_out on both supply and withdraw legs:
- If realized amount
< min_out, the transaction reverts withVaultError::MinOutNotMet(#42). - This acts as a hard floor: any sandwich that causes slippage exceeding the tolerance will reject the entire rebalance.
Before calling rebalance(protocol, expected_apy, min_out):
- Do not hardcode
min_out = 0in production. A zero floor allows unlimited slippage and makes sandwich attacks profitable (attacker is guaranteed a fill). - Compute
min_outdynamically based on recent pool state. If the pool is thinly capitalized, increase the slippage tolerance or defer the rebalance. - Avoid scheduled rebalances at fixed times (every hour, at midnight, etc.). Predictability enables sandwich attacks. Use randomized intervals or event-driven triggers.
- Monitor large rebalances on testnet before production. Test with realistic slippage figures to identify pools that are too thin.
- Consider multi-step rebalances for very large deployments:
- Instead of deploying 100M USDC in one
rebalance(), break it into 5x 20M over multiple minutes. - This reduces the profit surface for a single sandwich attack.
- Instead of deploying 100M USDC in one
For a sandwich to be profitable, the attacker's profit must exceed:
- Slippage cost: the price movement the attacker induces.
- Gas fees: Soroban transaction costs for front-run + back-run (typically 1-5 stroops per operation, significant for repeated sandwiches).
- Liquidity risk: if the pool is deep and the attacker lacks sufficient capital, the back-run may not fully recover the front-run's profit.
Thin pools are high-risk: A pool with <5M USDC liquidity is vulnerable. A pool with >50M USDC is typically resistant (a 20% sandwich requires 50M+ in attacker capital and bears significant slippage risk).
-
Soroban's single-slot finality may reduce MEV surface compared to consensus-layered chains:
- Transactions in the same ledger cannot be reordered.
- Observing a pending rebalance in the mempool and sandwiching it requires network timing coordination across Soroban + Stellar consensus.
- In practice, Soroban's 5-second ledger close provides a narrow window, but not zero opportunity.
-
No oracle to frontrun on Soroban yet: if the vault's off-chain agent uses a centralized price feed (e.g., Binance API) to compute
min_out, a sophisticated attacker might manipulate that feed. Using Soroban oracle contracts (when available) or decentralized price sources reduces this risk. -
Backend private keys: if the agent backend's private key is compromised, the attacker can submit arbitrary
min_outvalues (including 0). Secure key management is critical.
- ✅ Research DEX interface (this document)
- ✅
DataKey::DexPool+ owner-configurable pool address - ✅
supply_to_dex/withdraw_from_dexinternal helpers (mirror Blend) - ✅
rebalancesupports thedexprotocol withmin_outslippage protection - ✅
CurrentProtocol/ProtocolChangedEventreflect DEX deployments - ✅ Integration tests with mock DEX pool
- ✅
dex-devnetfeature flag for testnet smoke tests - ⏳ Measure gas on testnet
- ⏳ Production AMM adapter (two-asset pool zap) — out of scope for this issue
BLEND_INTEGRATION_RESEARCH.md— Blend protocol integration, mirrors the DEX approachUPGRADE_MIGRATION.md— howDataKey::DexPoolandDexApprovalTtlsurvive contract upgrades../EVENTS.md— fullDexSupplyEvent,DexWithdrawEvent,DexPoolConfiguredEventschemas../SECURITY.md— slippage, incomplete-exit, and pool-validation threat analysis
- Soroswap (Soroban AMM): https://docs.soroswap.finance
- Stellar liquidity pools: https://developers.stellar.org/docs/learn/encyclopedia/sdex/liquidity-on-stellar-sdex-liquidity-pools
- Soroban SDK Documentation: https://soroban.stellar.org/docs
- Blend integration:
BLEND_INTEGRATION_RESEARCH.md