// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {IERC3156FlashLender} from "@openzeppelin/contracts/interfaces/IERC3156FlashLender.sol"; import {IERC3156FlashBorrower} from "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; /** * @title SimpleERC3156FlashVault * @notice Minimal ERC-3156 flash lender: whitelist per token, flat bps fee, same-block repayment invariant. * @dev Intended for Chain 138 / internal atomic workflows when no external flash source exists. */ contract SimpleERC3156FlashVault is IERC3156FlashLender, Ownable, ReentrancyGuard { using SafeERC20 for IERC20; bytes32 private constant _RETURN_VALUE = keccak256("ERC3156FlashBorrower.onFlashLoan"); /// @notice Maximum owner-configurable fee (10%). Prevents griefing at 100% fee. uint256 public constant MAX_FEE_BPS = 1000; /// @notice Fee on borrowed amount, basis points (10_000 = 100%). uint256 public feeBps; mapping(address token => bool) public supportedToken; /// @notice When true, only `approvedBorrower[receiver]` may be used as the flash callback contract. bool public borrowerAllowlistEnabled; mapping(address borrower => bool) public approvedBorrower; /// @notice Cumulative fees retained by the vault per token (for ops / accounting). mapping(address token => uint256) public totalFeesCollected; /// @notice initiator = flashLoan caller; receiver = IERC3156FlashBorrower callback target. event FlashLoan(address indexed initiator, IERC3156FlashBorrower indexed receiver, address indexed token, uint256 amount, uint256 fee); event FeeBpsUpdated(uint256 feeBps); event TokenSupportUpdated(address indexed token, bool supported); event TokensRescued(address indexed token, address indexed to, uint256 amount); event BorrowerAllowlistEnabledUpdated(bool enabled); event BorrowerApprovalUpdated(address indexed borrower, bool approved); error UnsupportedToken(); error ZeroAmount(); error FeeTooHigh(); error InvalidCallback(); error RepaymentFailed(); error ZeroRescue(); error ZeroRecipient(); error BorrowerNotApproved(); constructor(address initialOwner, uint256 initialFeeBps) Ownable(initialOwner) { _setFeeBps(initialFeeBps); } function setFeeBps(uint256 newFeeBps) external onlyOwner { _setFeeBps(newFeeBps); } function setTokenSupported(address token, bool supported) external onlyOwner { supportedToken[token] = supported; emit TokenSupportUpdated(token, supported); } function setBorrowerAllowlistEnabled(bool enabled) external onlyOwner { borrowerAllowlistEnabled = enabled; emit BorrowerAllowlistEnabledUpdated(enabled); } function setBorrowerApproved(address borrower, bool approved) external onlyOwner { approvedBorrower[borrower] = approved; emit BorrowerApprovalUpdated(borrower, approved); } /// @notice Operator alias for `flashFee` (same revert rules). function previewFlashFee(address token, uint256 amount) external view returns (uint256) { return flashFee(token, amount); } /// @notice Owner-only recovery (mis-seeded asset, deprecated token, migration). Does not bypass flash invariants on active loans in the same tx. function rescueTokens(address token, uint256 amount, address to) external onlyOwner { if (amount == 0) revert ZeroRescue(); if (to == address(0)) revert ZeroRecipient(); IERC20(token).safeTransfer(to, amount); emit TokensRescued(token, to, amount); } function maxFlashLoan(address token) external view override returns (uint256) { if (!supportedToken[token]) { return 0; } return IERC20(token).balanceOf(address(this)); } function flashFee(address token, uint256 amount) public view override returns (uint256) { if (!supportedToken[token]) { revert UnsupportedToken(); } return (amount * feeBps) / 10_000; } function flashLoan( IERC3156FlashBorrower receiver, address token, uint256 amount, bytes calldata data ) external override nonReentrant returns (bool) { if (!supportedToken[token]) revert UnsupportedToken(); if (amount == 0) revert ZeroAmount(); if (borrowerAllowlistEnabled && !approvedBorrower[address(receiver)]) { revert BorrowerNotApproved(); } uint256 fee = flashFee(token, amount); uint256 balanceBefore = IERC20(token).balanceOf(address(this)); if (balanceBefore < amount) revert RepaymentFailed(); IERC20(token).safeTransfer(address(receiver), amount); bytes32 retval = receiver.onFlashLoan(msg.sender, token, amount, fee, data); if (retval != _RETURN_VALUE) revert InvalidCallback(); if (IERC20(token).balanceOf(address(this)) < balanceBefore + fee) { revert RepaymentFailed(); } totalFeesCollected[token] += fee; emit FlashLoan(msg.sender, receiver, token, amount, fee); return true; } function _setFeeBps(uint256 newFeeBps) internal { if (newFeeBps > MAX_FEE_BPS) revert FeeTooHigh(); feeBps = newFeeBps; emit FeeBpsUpdated(newFeeBps); } }