Skip to main content
Strategies compose the base supply, borrow, withdraw, repay, and pool flash-borrow primitives with an external swap aggregator, in one controller call. They keep the same account, oracle, and reentry safety model as the base operations. Read first: Supply and borrow, Accounts and risk.

Endpoints

EndpointWhat it does
multiplyFlash-borrow the debt token, swap it into the collateral token, supply the collateral. Opens or extends a leveraged position.
swap_debtBorrow a new debt token, swap into the existing debt token, repay the old debt.
swap_collateralWithdraw collateral, swap into a new collateral asset, resupply.
repay_debt_with_collateralWithdraw collateral, swap (or net) into the debt token, repay. Optionally close the position.
migrate_from_blendRepay Blend debt, withdraw collateral and supply, deposit as XOXNO collateral. One transaction.

The aggregator boundary

The swap argument is an opaque Bytes payload (ScVal XDR). The controller does not decode it or validate its route structure. It forwards the bytes verbatim to the configured aggregator:
// Controller → aggregator (contractclient)
fn execute_strategy(sender: Address, total_in: i128, swap_xdr: Bytes) -> i128
The controller calls this with sender = <controller address> and total_in set to the exact amount it commits to spend. The aggregator pulls total_in of the input token from the controller, performs the swap, and returns the output token. Inside the aggregator the XDR decodes to a StrategyPayload:
StrategyPayload {
    paths: Vec<SwapPath>,   // SwapPath { hops, split_ppm }, Σ split_ppm = 1_000_000
    referral_id: u64,       // 0 = no referral fee
    token_in: Address,
    token_out: Address,
    total_min_out: i128,    // slippage floor — enforced INSIDE the aggregator
}
Slippage is enforced by the aggregator, not the controller. The total_min_out floor lives inside the opaque bytes and is checked by execute_strategy. The controller adds only defense-in-depth: the swap bytes must be non-empty and amount positive, the router must not spend more than total_in, and the returned output balance delta must be positive. Build total_min_out correctly in your quote — the controller will not catch a bad price for you.
This replaces the previous design, where swap was a typed AggregatorSwap/BatchSwap with per-path amount_in/min_amount_out and controller-side path validation. Those types no longer exist.

Building the swap payload

You do not hand-assemble the XDR. Request a route from the swap aggregator’s quote service, which returns the encoded StrategyPayload bytes for the input/output pair and amount. Pass those bytes straight into the strategy call. See Stellar Aggregator for the quote API and token coverage. When the route is multi-hop or split across venues, that complexity is entirely inside the bytes; the controller and your transaction see only swap: Bytes.

Position mode

PositionMode (Normal = 0, Multiply = 1, Long = 2, Short = 3) tags the account’s intent. multiply accepts only Multiply, Long, or Short and rejects Normal. An account’s mode is fixed at creation; the other three strategies do not change it.

Multiply

multiply(
    caller: Address,
    account_id: u64,
    spoke_id: u32,
    collateral_token: Address,
    debt_to_flash_loan: i128,
    debt_token: Address,
    mode: PositionMode,
    swap: Bytes,
    initial_payment: Option<(Address, i128)>,
    convert_swap: Option<Bytes>,
) -> u64
Flow: collect any initial_payment from caller → flash-borrow debt_to_flash_loan of debt_token from the pool → execute_strategy swaps it (plus debt-denominated payment) into collateral_token → supply the combined collateral → re-check LTV, health factor, and dust floors. Rules:
  • mode is Multiply, Long, or Short; collateral_token != debt_token.
  • The strategy borrow uses the RiskIncreasing oracle policy.
  • initial_payment may be in the collateral token (added directly), the debt token (added to the flash amount), or a third token — which requires convert_swap to convert it to the collateral token, else #500 ConvertStepsRequired.

Swap debt

swap_debt(caller, account_id, existing_debt_token, amount, new_debt_token, swap: Bytes)
Borrows amount of new_debt_token, swaps to existing_debt_token, repays the old debt; over-repayment is refunded to caller. The two debt tokens must differ.

Swap collateral

swap_collateral(caller, account_id, current_collateral, amount, new_collateral, swap: Bytes)
Withdraws current_collateral, swaps to new_collateral, resupplies. Debt-free accounts use the RiskDecreasing oracle policy; accounts with debt use RiskIncreasing. The new collateral must pass deposit preflight (collateralizable on the account’s spoke, within position limits).

Repay debt with collateral

repay_debt_with_collateral(
    caller, account_id, collateral_token, collateral_amount,
    debt_token, swap: Bytes, close_position: bool,
)
Withdraws collateral_amount, swaps to debt_token (or nets directly when the tokens are the same — no route needed), repays. When close_position = true and all debt is cleared, withdraws all remaining collateral to caller.

Migrate from Blend

migrate_from_blend(
    caller, account_id, spoke_id, hub_id,
    blend_pool, collateral_assets, supply_assets, debt_caps,
) -> u64
Repays Blend debt with a zero-fee internal strategy borrow, withdraws collateral and non-collateral supply, and deposits the result as XOXNO collateral. The blend_pool must be governance-approved. Solvency is checked at strategy_finalize, not during the Blend calls. This flow does not use the flash-loan receiver pattern. See Blend migration for the two-phase submit model, refund reconciliation, and parameter guide.

Strategy borrow vs flash loan

Mechanismflash_loanStrategy borrow (multiply, migrate_from_blend)
Pool callpool.flash_loan → receiver callbackpool.create_strategy
FeeConfigured bps0 for migration; configured for multiply
Debt outcomeRepaid in same callbackPermanent account debt
Solvency checkAt callback endAt strategy_finalize

Reentry and trust

Strategy swaps and Blend submit calls reuse the flash-loan reentrancy guard (FlashLoanOngoing). A compromised or misconfigured aggregator cannot call back into mutating controller entrypoints during the swap. The controller authorizes exactly one token pull (total_in of the input token) for the aggregator and verifies its own balance deltas afterward.

Failure modes

ErrorCauseFix
#500 ConvertStepsRequiredmultiply initial payment is a third token but convert_swap is NoneProvide a convert_swap route for payment → collateral.
#27 AggregatorNotSetNo aggregator configuredGovernance must schedule set_aggregator.
#42 BlendPoolNotApprovedblend_pool not on the allow-listGovernance must schedule approve_blend_pool.
#102 HealthFactorTooLowPost-strategy health factor below 1Reduce leverage or improve the quote (total_min_out).
Aggregator slippage revertOutput below total_min_outRe-quote; the floor is inside the swap bytes.

Next

Blend migration

Atomic Blend V2 to XOXNO position migration.

Flash loans

The single-asset flash loan receiver pattern.

Stellar Aggregator

The quote service that produces the swap bytes.