---
title: "Flash Loans"
description: "ERC-3156-style flash loans on the pool-keyed Flash singleton: fees, liquidity floor, and borrower callback"
audience: tech
type: reference
status: live
lang: en
updated: "2026-09-03"
publish: true
---
# Flash Loans

## 1. Overview

ERC-3156-style flash loans, `postFlashLoan` variant: borrow any flash-enabled asset of any pool, uncollateralized, for the duration of one transaction. Lending is served by a single `Flash` singleton keyed by pool, so every call carries the target pool as its first argument and repayment is made to the pool rather than to the singleton. This page covers the loan flow, the fee split, the liquidity floor that bounds a loan, and the borrower callback contract.

> **Dormant on Arc.** No leg is flash-enabled. Live `RiskConfig.flags` is `0x06` (`SWAP_ENABLED_BIT | LIABILITY_SWAP_ENABLED_BIT`) on all 37 legs; `FLASH_ENABLED_BIT` (bit 4, `PoolConstantsLib.sol`) is set nowhere, so `Flash.flashLoan` reverts `FeatureDisabled(Err.Resource.FLASH)` fleet-wide at its flags check. The contract is deployed and the code below is the shipped behaviour; arming it on any leg is an `UPDATE_RISK` write (§5.1), and whether the feature ships at all is an open decision. Note the interaction with §6.2: `initAsset` writes `minLiquidity = 0` and no deploy script raises it, so enabling the bit today would make the whole liquid reserve loanable.

## 2. Architecture

### 2.1. Implementation

The `Flash` singleton (`Flash.sol`) is a UUPS implementation behind an ERC-1967 proxy; `Pool` bakes the proxy address in as an immutable. It serves every pool, so each public function takes `address pool` as its first arg. Flash calls `Pool`'s restricted entry points (`flashSend` for the token push, `flashAccount` for the protocol-fee ledger update) via standard external calls, which `Pool` answers directly; the bodies run in the linked `PoolLiquidity` library. No runtime module trust: the DELEGATECALL target is compile-time fixed, not a pluggable module.

### 2.2. Key Features

- **ERC-3156-style**: `postFlashLoan` variant of the standard borrower callback (magic value `keccak256("ERC3156FlashBorrower.postFlashLoan")`), pool-keyed singleton
- **Multi-Asset Support**: any flash-enabled asset
- **Configurable Fees**: pool-wide fee in pbps (`flashFeePbps / 1_000_000`)
- **Protocol Revenue**: fees split between LPs and protocol treasury
- **Liquidity Constraints**: `minLiquidity` floor enforced

## 3. Technical Details

### 3.1. Flash Loan Flow

1. **Validation**
   - Reject the native sentinel: the pool holds wnative as the asset, so borrow wnative directly
   - Check asset is flash-enabled (`FLASH_ENABLED_BIT`); an unlisted token reads flags 0 and fails here
   - Check asset is not halted (`HALT_MASK` = `HALT_RISK_BIT` | `HALT_GUARDIAN_BIT` = 0x0041)
   - `amount != 0`

2. **Fee quote**
   - `fee = amount * flashFeePbps / PBPS`, split into `protoFee` / LP share by `protoSharePct`

3. **Prepare and send**
   - `flashPrepare` recalls from the asset hook to $R_{\mathrm{liq}} \ge \mathrm{amount} + \mathrm{minLiquidity}$
   - Capture the pool's token balance, then `flashSend` pushes `amount` and re-checks the liquidity floor at the push

4. **Callback**
   - Call the borrower's `postFlashLoan()` (IERC3156-style) and require the magic value

5. **Repayment Verification**
   - Post-callback pool balance must be at least `balanceBefore + fee` (the capture predates the debit, so the net delta is `+fee`)
   - `flashAccount` credits the LP share to reserves and `protoFee` to the protocol ledger

There are **no** pool hooks `preFlashLoan` / `postFlashLoan`, only the borrower's IERC3156 callback. The step-3 recall is `flashPrepare` CALLing the asset hook's `preOutflow` (dual ledger). `maxFlashLoan` $= R_{\mathrm{liq}} - \mathrm{minLiquidity}$, an advisory ERC-3156 quote: the floor itself is enforced pool-side at both `flashPrepare` and `flashSend`. See [Hooks](/docs/5-1-3-hooks).

### 3.2. Fee Structure

```solidity
fee = (amount * flashFeePbps) / 1_000_000
protocolFee = (fee * protoSharePct) / 100
lpFee = fee - protocolFee
```

- **flashFeePbps**: Pool-wide parameter, PBPS, ceiling `MAX_FLASH_FEE_PBPS` = 10,000 (1%). Live values: [2. Deployments](/docs/2-overview)
- **protoSharePct**: Percentage to protocol (e.g., 20 = 20%)
- LP fee increases reserves, protocol fee collected separately

### 3.3. Security Features

- **Reentrancy Protection**: `nonReentrant` modifier on `flashLoan()`
- **Balance Verification**: Strict check on repayment amount
- **Callback Validation**: ERC-3156 magic value must be returned
- **Atomic Execution**: All operations within single transaction

## 4. Usage Examples

### 4.1. Basic Flash Loan

The lender is the **`Flash` singleton**, not the pool, every call takes the target `pool` as first arg. Repayment is by **raising the pool's token balance** (a plain `transfer` to the pool): `Flash` verifies `pool balance >= balanceBefore + fee` after the callback. Repaying via `deposit`/`donate`/`swap` is blocked during the callback (flash-in-flight guard in `PoolIOLib`).

```solidity
contract Arbitrageur is IERC3156FlashBorrower {
    bytes32 constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.postFlashLoan");

    function executeArbitrage(address flash, address pool, address token, uint256 amount) external {
        bytes memory data = abi.encode(msg.sender, pool);
        IFlash(flash).flashLoan(pool, this, token, amount, data);
    }

    function postFlashLoan(
        address initiator,
        address token,
        uint256 amount,
        uint256 fee,
        bytes calldata data
    ) external returns (bytes32) {
        (, address pool) = abi.decode(data, (address, address));
        // Perform arbitrage logic
        // ...

        // Repay by transferring amount + fee back to the POOL (not the Flash singleton)
        IERC20(token).transfer(pool, amount + fee);

        return CALLBACK_SUCCESS;
    }
}
```

### 4.2. Query Available Liquidity

```solidity
// Check maximum flash loan available (pool-keyed on the Flash singleton)
uint256 maxLoan = flash.maxFlashLoan(pool, USDC);

// Calculate fee for specific amount
uint256 fee = flash.flashFee(pool, USDC, 1_000_000e6); // 1M USDC
```

## 5. Configuration

### 5.1. Enabling Flash Loans

Pool owner must enable flash loans per asset via risk configuration. No leg is enabled today (§1):

```solidity
// Queue the flash-loan enable (LOW tier, 1 hour under PROD_DELAYS)
admin.requestOp(
    pool,
    uint8(IPool.OpType.UPDATE_RISK),
    bytes32(uint256(uint160(token))),
    abi.encode(riskConfig)
);

// After the delay matures
admin.executeUpdateRiskConfig(pool, token);
```

**RiskConfig flags**:
```solidity
riskConfig.flags |= FLASH_ENABLED_BIT;  // Enable flash loans
```

### 5.2. Fee Parameters

Flash loan fees are pool-wide and subject to the LOW-tier timelock, 1 hour (`Admin._tier` maps `UPDATE_FEES` to `DELAY_LOW`; `Constants.sol` sets LOW to `1 hours` in both the PROD and the TESTNET tables):

```solidity
// UPDATE_FEES is pool-wide, so its subject is ignored.
admin.requestOp(
    pool,
    uint8(IPool.OpType.UPDATE_FEES),
    bytes32(0),
    abi.encode(IPool.FeeParams({
        protoSharePct: 20,    // 20% to protocol
        flashFeePbps: 100     // 0.01%, the shipped value
    }))
);

// Execute after the delay matures
admin.executeUpdateFeeParams(pool);
```

## 6. Emergency Controls

### 6.1. Asset Halt

One lever disables flash loans instantly (no timelock). `src` names the `HALT_MASK` bit being set: `HALT_RISK_BIT` (bit 0, owner risk halt) or `HALT_GUARDIAN_BIT` (bit 6, guardian emergency halt). Halting is guardian-or-owner; `unhaltAsset` is owner-only and clears only the bit it names, so a leg is loanable again once every source that halted it has been lifted.

```solidity
admin.haltAsset(pool, token, HALT_RISK_BIT);        // owner risk halt
admin.haltAsset(pool, token, HALT_GUARDIAN_BIT);    // guardian emergency halt
admin.unhaltAsset(pool, token, HALT_RISK_BIT);      // owner only
```

### 6.2. Minimum Liquidity

Reserves below `minLiquidity` cannot be flash loaned, protecting against liquidity drain attacks.

## 7. Cost shape

The loan path does one balance read before the callback and one after, and touches the asset hook only when liquid reserves are short of `amount + minLiquidity`. The LP share and the protocol share are booked in the same `flashAccount` write.

## 8. Events

```solidity
event FlashLoanExecuted(
    address indexed pool,
    address indexed initiator,
    address indexed receiver,
    address token,
    uint256 amount,
    uint256 fee
);
```

## 9. Error Handling

| Error | Trigger |
|-------|---------|
| `FeatureDisabled(Resource.FLASH)` | Flash loans not enabled for the asset, the singleton is paused, or the native sentinel was passed |
| `FeatureDisabled(Resource.ASSET)` | Any `HALT_MASK` bit set on the asset |
| `ZeroValue()` | Requested amount is 0 |
| `InsufficientAmount(available, required)` | Liquid reserves short of `amount + minLiquidity` after recall |
| `OperationFailed()` | Callback did not return the magic value, or the pool balance did not grow by at least `fee` |

## 10. Integration Notes

### 10.1. For Borrowers

1. Implement `IERC3156FlashBorrower` interface
2. Return correct magic value from `postFlashLoan()`
3. Repay by transferring `amount + fee` to the pool before returning; no approval is involved
4. Account for fees in arbitrage/liquidation logic

### 10.2. For Pool Operators

1. Enable flash loans via risk config (requires timelock)
2. Set fee parameters (§5.2)
3. Monitor flash loan activity via events
4. Set `minLiquidity` deliberately: `initAsset` writes 0, so the floor is inert until a deploy config raises it
