> ## Documentation Index
> Fetch the complete documentation index at: https://xoxno.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Stellar lending flash loans

> Flash loans on XOXNO's Stellar lending: the controller entrypoint, receiver callback contract, fee accounting, reentry guard, and repayment verification for uncollateralized same-transaction borrowing.

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](/stellar-lending/dev/strategies), [Security model](/stellar-lending/dev/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.

```rust theme={"system"}
// Controller entrypoint
flash_loan(
    caller: Address,
    asset: Address,
    amount: i128,
    receiver: Address,
    data: Bytes,
)
```

The controller forwards to the pool's single settlement call:

```rust theme={"system"}
// 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

<ParamField path="caller" type="Address" required>
  Initiator. Authorizes the flash loan. Need not be the receiver.
</ParamField>

<ParamField path="asset" type="Address" required>
  The flashloanable asset to borrow.
</ParamField>

<ParamField path="amount" type="i128" required>
  Borrowed amount in asset-native units. Must be positive.
</ParamField>

<ParamField path="receiver" type="Address" required>
  The contract that receives `amount`, runs `execute_flash_loan`, and approves repayment.
</ParamField>

<ParamField path="data" type="Bytes" required>
  Opaque payload forwarded verbatim to the receiver's callback. Encode whatever your receiver needs.
</ParamField>

## 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:

```rust theme={"system"}
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.

```rust theme={"system"}
#![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

<CodeGroup>
  ```bash Stellar CLI theme={"system"}
  # 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>
  ```

  ```typescript TypeScript theme={"system"}
  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
  ```
</CodeGroup>

## 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

| Error                           | Cause                                                                   | Fix                                                                           |
| ------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| `#401 FlashloanNotEnabled`      | Asset config has `is_flashloanable = false`                             | Use a flashloanable asset (for example USDC, XLM, EURC, BTC, ETH on mainnet). |
| `#412 InvalidFlashloanReceiver` | `receiver` is not a deployed Wasm contract                              | Pass a deployed contract address that exports `execute_flash_loan`.           |
| `#402 InvalidFlashloanRepay`    | Receiver approved less than `amount + fee`, or changed the pool balance | Approve exactly `amount + fee` to the pool before the callback returns.       |
| `#400 FlashLoanOngoing`         | Reentry into a mutating controller entrypoint during the callback       | Do not call back into the controller; do your work, then repay.               |
| `#14 AmountMustBePositive`      | `amount` is zero or negative                                            | Borrow 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:

| Mechanism      | `flash_loan`                          | Strategy borrow                            |
| -------------- | ------------------------------------- | ------------------------------------------ |
| Pool call      | `pool.flash_loan` → receiver callback | `pool.create_strategy`                     |
| Fee            | Configured bps                        | `0` for migration; configured for multiply |
| Debt outcome   | Repaid in same callback               | Permanent account debt                     |
| Solvency check | At callback end                       | At `strategy_finalize`                     |

See [Strategies](/stellar-lending/dev/strategies#strategy-borrow-vs-flash-loan) for the full contrast.

## Next

<CardGroup cols={2}>
  <Card title="Strategies" icon="layer-group" href="/stellar-lending/dev/strategies">
    Leverage and swap flows that build on the flash loan plus the swap aggregator.
  </Card>

  <Card title="Controller ABI" icon="code" href="/stellar-lending/dev/controller-abi">
    Every controller entrypoint, grouped by access control.
  </Card>
</CardGroup>
