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.
// Controller entrypoint
flash_loan(
    caller: Address,
    asset: Address,
    amount: i128,
    receiver: Address,
    data: Bytes,
)
The controller forwards to the pool’s single settlement call:
// Pool — one atomic call, owner-only (controller)
flash_loan(
    asset: Address,
    initiator: Address,
    receiver: Address,
    amount: i128,
    fee: i128,
    data: Bytes,
) -> MarketStateSnapshot

Preconditions

  • asset is a listed, Active market with is_flashloanable = true (else #401 FlashloanNotEnabled).
  • 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

caller
Address
required
Initiator. Authorizes the flash loan. Need not be the receiver.
asset
Address
required
The flashloanable asset to borrow.
amount
i128
required
Borrowed amount in asset-native units. Must be positive.
receiver
Address
required
The contract that receives amount, runs execute_flash_loan, and approves repayment.
data
Bytes
required
Opaque payload forwarded verbatim to the receiver’s callback. Encode whatever your receiver needs.

Fee

The fee is flashloan_fee_bps * amount from the asset’s config, with a floor of 1 unit whenever the configured rate is nonzero. Configured values come from the launched market configuration; non-flashloanable assets reject flash loans. The fee is retained by the pool as protocol revenue, later claimable through the controller.

Receiver contract

Your receiver is a separate contract that must export:
execute_flash_loan(
    initiator: Address,
    asset: Address,
    amount: i128,
    fee: i128,
    pool: Address,
    data: Bytes,
)
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.
#![no_std]
use soroban_sdk::{
    contract, contractimpl, token, Address, Bytes, Env,
    auth::{ContractContext, InvokerContractAuthEntry, SubContractInvocation},
    symbol_short, IntoVal, Vec,
};

#[contract]
pub struct Receiver;

#[contractimpl]
impl Receiver {
    pub fn execute_flash_loan(
        env: Env,
        _initiator: Address,
        asset: Address,
        amount: i128,
        fee: i128,
        pool: Address,
        _data: Bytes,
    ) {
        // ... do work with `amount` of `asset` (must end holding amount + fee) ...

        let total = amount + fee;
        let me = env.current_contract_address();
        let expiry = env.ledger().sequence() + 1;

        // Authorize the pool's transfer_from by pre-approving as this contract.
        let approve = InvokerContractAuthEntry::Contract(SubContractInvocation {
            context: ContractContext {
                contract: asset.clone(),
                fn_name: symbol_short!("approve"),
                args: (me.clone(), pool.clone(), total, expiry).into_val(&env),
            },
            sub_invocations: Vec::new(&env),
        });
        let mut entries = Vec::new(&env);
        entries.push_back(approve);
        env.authorize_as_current_contract(entries);

        token::Client::new(&env, &asset).approve(&me, &pool, &total, &expiry);
    }
}
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

# Flash-borrow 10,000 USDC (7 decimals) to your receiver contract.
stellar contract invoke \
  --id <CONTROLLER> \
  --source-account caller \
  --network testnet \
  -- flash_loan \
  --caller C... \
  --asset CUSDC... \
  --amount 100000000000 \
  --receiver CRECEIVER... \
  --data <xdr-bytes>
import { Contract, nativeToScVal, Address, xdr } from "@stellar/stellar-sdk";

const controller = new Contract("C...");

const op = controller.call(
  "flash_loan",
  new Address(callerPublicKey).toScVal(),
  new Address("CUSDC...").toScVal(),
  nativeToScVal(100_000_000_000n, { type: "i128" }),
  new Address("CRECEIVER...").toScVal(),
  xdr.ScVal.scvBytes(payload), // opaque data for your receiver
);
// build, prepareTransaction (simulate — auto-discovers the receiver's auth), sign with caller, send

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 { asset, receiver, caller, amount, fee } — and UpdateMarketStateBatchEvent (["market", "batch_state_update"]) reflecting the fee booked as revenue. Confirm the fee landed with the pool view protocol_revenue(asset) (up by fee), and check your receiver’s token balance returned to its pre-loan level (it paid back amount + fee).

Failure modes

ErrorCauseFix
#401 FlashloanNotEnabledAsset config has is_flashloanable = falseUse a flashloanable asset (for example USDC, XLM, EURC, BTC, ETH on mainnet).
#412 InvalidFlashloanReceiverreceiver is not a deployed Wasm contractPass a deployed contract address that exports execute_flash_loan.
#402 InvalidFlashloanRepayReceiver approved less than amount + fee, or changed the pool balanceApprove exactly amount + fee to the pool before the callback returns.
#400 FlashLoanOngoingReentry into a mutating controller entrypoint during the callbackDo not call back into the controller; do your work, then repay.
#14 AmountMustBePositiveamount is zero or negativeBorrow a positive amount.

Strategy borrow vs flash loan

Strategy flows (multiply, migrate_from_blend) borrow through pool.create_strategy, not the flash-loan receiver pattern:
Mechanismflash_loanStrategy borrow
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
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.