Skip to main content
migrate_from_blend moves a Blend V2 position to XOXNO in one transaction. The controller repays Blend debt, withdraws collateral and non-collateral supply, deposits the withdrawn assets as XOXNO collateral, and runs a final health check before persisting state.

Entrypoint

migrate_from_blend(
    caller: Address,
    account_id: u64,                    // 0 creates a new account
    spoke_id: u32,
    hub_id: u32,
    blend_pool: Address,                // must be governance-approved
    collateral_assets: Vec<Address>,    // WithdrawCollateral, sweep-all
    supply_assets: Vec<Address>,        // Withdraw (non-collateral supply), sweep-all
    debt_caps: Vec<(Address, i128)>,    // (debt_asset, max_repay_cap)
) -> u64
ParameterSemantics
callerMust authorize the call. Owns the Blend position and the resulting XOXNO account.
account_id0 creates a fresh account; non-zero merges into an existing account owned by caller.
blend_poolTarget Blend pool. Must pass is_blend_pool_approved or the call reverts with #42 BlendPoolNotApproved.
collateral_assetsBlend collateral tokens to withdraw. Each uses WithdrawCollateral with i128::MAX internally.
supply_assetsBlend non-collateral supply to withdraw. Each uses Withdraw with i128::MAX. Deposited as XOXNO collateral.
debt_capsPer debt asset, the maximum the controller may borrow to repay Blend. Must be ≥ actual Blend debt.
At least one of {collateral_assets, supply_assets, debt_caps} must be non-empty.

Atomic flow

The entire flow runs inside one controller call. Any revert rolls back all Blend and XOXNO state changes.

Debt sourcing

Migration does not use the flash-loan receiver pattern. It uses an internal zero-fee strategy borrow:
Mechanismflash_loanMigration borrow
Entrypointcontroller.flash_loanopen_migration_borrow inside migrate_from_blend
Pool callpool.flash_loan → receiver callbackpool.create_strategy
FeeConfigured bps0
Debt outcomeRepaid in same callbackPermanent debt on the migrated account
Receiver contractRequiredNone
Solvency checkAt callback endAt strategy_finalize

Refund reconciliation

debt_caps are upper bounds, not exact debt amounts. When Blend repays less than the cap, Blend refunds the surplus to the controller. reconcile_debt_refunds detects the refund via balance delta and calls repay_debt_from_controller. The migrated XOXNO debt equals the actual Blend debt, not the cap.

Looped positions

When the same token is both collateral and debt (for example USDC collateral plus USDC debt), the controller uses a two-phase submit:
  1. Phase 1 — repay debt with an isolated balance snapshot so refunds do not alias with withdrawals.
  2. Phase 2 — withdraw collateral and supply with a fresh snapshot.
The same asset may appear in both collateral_assets and debt_caps. Duplicate debt entries in debt_caps revert with #AssetsAreTheSame.

Blend request types

RequestConstantAmountPurpose
RepayREQ_REPAY = 5cap from debt_capsClear Blend debt
WithdrawCollateralREQ_WITHDRAW_COLLATERAL = 3i128::MAXSweep all collateral
WithdrawREQ_WITHDRAW = 1i128::MAXSweep non-collateral supply
Non-collateral Blend supply becomes collateral on XOXNO. XOXNO has no non-collateral supply concept.

Authorization

LegWho authorizes
migrate_from_blendcaller.require_auth()
Blend submit(from=caller)Caller in the transaction auth tree (per phase)
transfer(controller → blend_pool, cap)Controller authorize_as_current_contract before repay submit

Reentry guard

guarded_submit sets FlashLoanOngoing during Blend calls. Mutating controller entrypoints reject reentry while the guard is set (#400 FlashLoanOngoing).

Governance

Blend pools are allow-listed through governance:
# Schedule via governance (production):
propose_approve_blend_pool(proposer, blend_pool_address, salt)
# → execute after timelock
Approved pools are listed in configs/blend_pools.json. Query approval with is_blend_pool_approved(blend_pool).

Events

BlendMigrationEvent {
    topics: ["strategy", "blend_migration"],
    account_id,
    blend_pool,
    collateral_count,
    supply_count,
    debt_count,
}

Failure modes

ConditionError
Empty collateral_assets, supply_assets, and debt_caps#InvalidPayments
Unapproved blend_pool#42 BlendPoolNotApproved
Duplicate debt asset in debt_caps#AssetsAreTheSame
debt_cap below actual Blend debtBlend health check revert (atomic rollback)
End state fails health / LTV / min collateral#InsufficientCollateral at finalize
Caller does not own target accountOwnership mismatch

Example invoke

stellar contract invoke --id <controller> --network <network> \
  --source <user> \
  -- migrate_from_blend \
  --caller <user> \
  --account-id 0 \
  --spoke-id 1 \
  --hub-id 1 \
  --blend-pool <approved_blend_pool> \
  --collateral-assets '[<XLM>]' \
  --supply-assets '[]' \
  --debt-caps '[["<USDC>", "1000000000"]]'
Set each debt_cap to at least the actual Blend debt for that asset. Use get_account_positions and Blend views to size caps before invoking.

Next

Strategies

Multiply, swap debt, swap collateral, and repay-with-collateral flows.

Flash loans

The flash-loan receiver pattern — distinct from migration’s zero-fee borrow.

Accounts and risk

Account model, health factor, and spoke constraints after migration.

Controller ABI

Full migrate_from_blend signature and related admin entrypoints.