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 Rliqamount+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 =Rliq-minLiquidity, an advisory ERC-3156 quote: the floor itself is enforced pool-side at both flashPrepare and flashSend. See Hooks.

3.2. Fee Structure

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
  • 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).

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

// 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):

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

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

// 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.

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

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

9. Error Handling

ErrorTrigger
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