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.
- ✅ 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