Skip to main content
Borrow one pool asset for the span of a single transaction, do arbitrary work in your receiver contract, and repay amount + fee before the call returns. Read first: Strategies, Security model. Call the controller; it issues one pool flash_loan. The pool sends amount to your receiver, runs the callback, then pulls amount + fee back in the same call. The controller emits FlashLoanEvent.

Goal

Obtain amount of a flashloanable asset, run your logic in a receiver contract, and have the pool reclaim amount + fee in the same transaction.
The controller forwards to the pool’s single settlement call, which computes the fee itself and returns it:

Preconditions

  • asset names a listed market whose is_flashloanable is true (else #401 FlashloanNotEnabled), and whose hub is active.
  • receiver is a deployed Wasm contract (else #412 InvalidFlashloanReceiver) that exports execute_flash_loan.
  • The pool holds at least amount of free liquidity.
  • amount > 0 (else #14 AmountMustBePositive).
  • No flash loan is already in progress for this transaction (the FlashLoanOngoing reentry guard, else #400).

Inputs

Address
required
Initiator. Authorizes the flash loan. Need not be the receiver.
HubAssetKey
required
The market to borrow from: { hub_id, asset }. The same token under two hubs is two independent markets with independent flash-loan settings.
i128
required
Borrowed amount in asset-native units. Must be positive.
Address
required
The contract that receives amount, runs execute_flash_loan, and approves repayment.
Bytes
required
Opaque payload forwarded verbatim to the receiver’s callback. Encode whatever your receiver needs.

Fee

The fee is flashloan_fee bps of amount from the market’s MarketParamsRaw, rounded half-up and raised to 1 unit whenever a positive fee would otherwise round to zero. It is capped at MAX_FLASHLOAN_FEE_BPS = 500 (5%) at configuration time. The fee is retained by the pool as protocol revenue, later claimable through the controller’s claim_revenue.

Receiver contract

Your receiver is a separate contract that must export:
The pool calls it after transferring amount to the receiver. Before the callback returns, the receiver must approve the pool to pull amount + fee of asset — repayment is a transfer_from the pool performs, not an ERC-20-style allowance you set ahead of time. Authorize that approval as the current contract.
If the receiver does not approve enough, the pool’s transfer_from reclaims less than amount + fee and the call reverts with #402 InvalidFlashloanRepay. The pool also asserts the receiver did not otherwise change the pool balance during the callback.

Build the call

Authorize

caller.require_auth() runs inside the controller’s flash_loan. The cross-contract authorization that matters is the receiver authorizing the pool’s transfer_from during the callback — handled inside execute_flash_loan with authorize_as_current_contract, not by the top-level signer. The payout (pool → receiver) is pool-internal. Simulating the transaction discovers and attaches the auth tree; sign the outer call with the caller key. Reentry. While the callback runs, the controller holds the FlashLoanOngoing guard. Any attempt to call back into a mutating controller entrypoint — supply, borrow, withdraw, repay, liquidate, a strategy, or another flash_loan — reverts with #400 FlashLoanOngoing.

Verify

On success the controller emits FlashLoanEvent — topics ["position", "flash_loan"], data { hub_id, asset, receiver, caller, amount, fee } — and the pool emits PoolMarketStateBatchEvent (["market", "batch_state_update"]) reflecting the fee booked as revenue. Confirm the fee landed with the pool view get_revenue(hub_asset) (up by fee), and check your receiver’s token balance returned to its pre-loan level (it paid back amount + fee).

Failure modes

flash_position — the collateralized cousin

flash_position is a distinct entrypoint with different semantics. It mints strategy debt onto an account with no flash fee, forwards the measured tokens to receiver, invokes its execute_flash_position callback, and deposits measured controller-balance increases of the declared collaterals. It does not repay. The debt stands at its full minted amount.
receiver must be a deployed Wasm contract, and must be neither the controller nor the pool. The position must remain open: the account must end with live debt in debt and at least one supply position, else #505 FlashPositionClosed.

The two declaration lists

Soroban has no way to list every token an address holds. A contract can only ask a named token contract for a balance. So the controller cannot discover what your receiver sent back. It can only check the assets you told it to check. These two lists are how you tell it. Each list does two jobs:
  • It bounds the work. Every entry costs two cross-contract balance() calls, so both lists are capped at max_supply_positions.
  • It bounds the trust. Every entry is checked against the spoke’s listing before your callback runs, so the controller never calls an arbitrary address you picked.
The controller takes two balance snapshots inside the flash guard, right before calling you. Everything after that is measured as a difference from those snapshots.

collaterals — the amount is a minimum

The i128 is a slippage floor, not a cap and not an exact expectation. The measured delta is what gets deposited, never the declared figure. All of this is checked before your callback runs:
  • The list is not empty.
  • It holds at most max_supply_positions entries.
  • Every minimum is zero or positive.
  • No duplicate HubAssetKey, and no duplicate underlying token address either — the same token can be listed under two hubs.
  • Every asset is listed on the account’s spoke, is collateralizable, and is not halted.
  • At least one minimum is above zero, else #503 CollateralRequired.
  • The usual supply gates pass: caps and position-count limits.

refund_assets — delta only, paid to the caller

The refund leg transfers balance − baseline, and only when that difference is positive. It is never the controller’s gross balance. The recipient is caller, not the receiver and not the account owner. Why must a refund asset be listed? Because the refund step hands an address you chose to a token contract after the flash guard has closed. Requiring it to be listed keeps that call on a contract governance already approved.
Returning the debt token does not repay. flash_position has no repay leg. Debt tokens handed back by the receiver are refunded to the caller and the debt stands at its full minted amount. The debt asset may appear in refund_assets, and the overlap check is only against collaterals.Refunds are silent, and there is no dry run. No view or return value tells you in advance whether a refund will occur; no refund event exists, and FlashPositionEvent carries no refund field. The only on-chain trace is the token contract’s own transfer event, controller to caller.Unlisted leftovers are stranded. An asset that is neither a declared collateral nor in refund_assets stays in the controller permanently, and no later caller can sweep it — a subsequent user’s baseline already contains it. There is no sweep or rescue entrypoint, deliberately: it would be exactly the primitive the measured-delta discipline exists to eliminate.

Receiver author checklist

  1. Set each minimum to a real slippage floor — it is the only post-callback protection against a bad route.
  2. At least one minimum must be positive.
  3. List in refund_assets every asset the callback might return, including the debt asset if it might not be fully spent.
  4. refund_assets may not overlap collaterals, may not duplicate, and every entry must be listed in the account’s spoke.
  5. Do not expect a returned debt token to repay anything.
  6. End the callback with the position still open: live debt in debt and at least one supply position.
Observer note: the strategy-debt mint is tagged FlashPos (16) in the position batch; collateral deposited from the callback is tagged Supply, identically to an ordinary deposit. FlashPositionEvent.fee is always 0.

Strategy borrow vs flash loan

Strategy flows (multiply, migrate_from_blend) borrow through pool.create_strategy, not the flash-loan receiver pattern: See Strategies for the full contrast.

Next

Strategies

Leverage and swap flows that build on the flash loan plus the swap aggregator.

Controller ABI

Every controller entrypoint, grouped by access control.