You're using the beta version on testnet.Learn more
Contract addresses, network settings and the flash-loan reference. Every function that moves a market forward is permissionless; ABIs ship in @confluere/sdk. Want to try the interface with sample data? Open the demo.
Same core address on Monad Testnet. ABIs ship in @confluere/sdk.
The core contract is an ERC-3156 flash lender. Borrow any token it holds, use it inside one transaction, and return it plus a fee before the call ends. No collateral, no allowlist, no oracle; the only check is the token balance after your callback.
maxFlashLoan(token) = the core's whole balance of that token.
flashLoan(receiver, token, amount, data). It reverts with InvalidParams if amount is 0 or above maxFlashLoan(token).fee = amount × 5 / 10,000 and transfers amount to receiver.receiver.onFlashLoan(msg.sender, token, amount, fee, data); the first argument is the account that started the loan. The callback must return keccak256("ERC3156FlashBorrower.onFlashLoan"), otherwise FlashFailed.amount + fee from receiver with transferFrom, so approve the core for that sum inside the callback.FlashFailed otherwise), credits the fee to the treasury's withdrawable balance, adds it to flashFees(token) and emits FlashLoan.interface IERC3156FlashLender {
function maxFlashLoan(address token) external view returns (uint256);
function flashFee(address token, uint256 amount) external view returns (uint256);
function flashLoan(IERC3156FlashBorrower receiver, address token, uint256 amount, bytes calldata data)
external returns (bool);
}
interface IERC3156FlashBorrower {
function onFlashLoan(address initiator, address token, uint256 amount, uint256 fee, bytes calldata data)
external returns (bytes32); // must return keccak256("ERC3156FlashBorrower.onFlashLoan")
}
// Confluere extras
uint256 public constant FLASH_FEE_BPS = 5; // 0.05%
function flashFees(address token) external view returns (uint256); // cumulative fees collected
event FlashLoan(address indexed receiver, address indexed token, uint256 amount, uint256 fee);
error InvalidParams(); // amount == 0 or amount > maxFlashLoan(token)
error FlashFailed(); // wrong callback return value, or balance short after repayment// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IERC3156FlashBorrower} from "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol";
import {IERC3156FlashLender} from "@openzeppelin/contracts/interfaces/IERC3156FlashLender.sol";
contract Borrower is IERC3156FlashBorrower {
IERC3156FlashLender public constant CORE = IERC3156FlashLender(0xf969CAb062278362b3E705C7c5b88bDEE2610De7);
bytes32 private constant OK = keccak256("ERC3156FlashBorrower.onFlashLoan");
error UntrustedLender();
error UntrustedInitiator();
function run(address token, uint256 amount, bytes calldata params) external {
CORE.flashLoan(this, token, amount, params);
}
function onFlashLoan(address initiator, address token, uint256 amount, uint256 fee, bytes calldata params)
external
returns (bytes32)
{
if (msg.sender != address(CORE)) revert UntrustedLender();
if (initiator != address(this)) revert UntrustedInitiator();
// ... use `amount` of `token` here (arbitrage, collateral swap, self-liquidation elsewhere) ...
params;
// end the call holding amount + fee; the core pulls it with transferFrom
IERC20(token).approve(address(CORE), amount + fee);
return OK;
}
}msg.sender == core in onFlashLoan; anyone can call your callback directly.initiator. Anyone can start a loan that names your contract as receiver; if your callback acts on stored approvals or balances, accept only loans you started.amount + fee and hold that much when the callback returns, or the pull reverts.InvalidParams(): amount is 0 or above the available balance.FlashFailed(): the callback returned something else, or the balance is short after repayment.SafeERC20FailedOperation(token): the pull failed, usually a missing approval or balance.ReentrancyGuardReentrantCall(): the callback called back into the core.