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
Obtainamount of a flashloanable asset, run your logic in a receiver contract,
and have the pool reclaim amount + fee in the same transaction.
Preconditions
assetnames a listed market whoseis_flashloanableis true (else#401 FlashloanNotEnabled), and whose hub is active.receiveris a deployed Wasm contract (else#412 InvalidFlashloanReceiver) that exportsexecute_flash_loan.- The pool holds at least
amountof free liquidity. amount > 0(else#14 AmountMustBePositive).- No flash loan is already in progress for this transaction (the
FlashLoanOngoingreentry 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 isflashloan_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: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.
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 emitsFlashLoanEvent — 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 atmax_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.
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_positionsentries. - 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.
Receiver author checklist
- Set each minimum to a real slippage floor — it is the only post-callback protection against a bad route.
- At least one minimum must be positive.
- List in
refund_assetsevery asset the callback might return, including the debt asset if it might not be fully spent. refund_assetsmay not overlapcollaterals, may not duplicate, and every entry must be listed in the account’s spoke.- Do not expect a returned debt token to repay anything.
- End the callback with the position still open: live debt in
debtand at least one supply position.
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.

