// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import {IERC3156FlashBorrower} from "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol"; import {IERC3156FlashLender} from "@openzeppelin/contracts/interfaces/IERC3156FlashLender.sol"; import "./IRWAToken.sol"; /** * @title LiIndexFlashVault * @notice IERC3156 flash lender for M00 Li* index tokens held in OMNL institutional vault. * @dev Flash borrow permitted only when utilization LTV is below GRU Monetary Policy ceiling * (default 80% = 20% operational buffer). Enforces index freshness and MPAP parity band. */ contract LiIndexFlashVault is IERC3156FlashLender, AccessControl, ReentrancyGuard { using SafeERC20 for IERC20; bytes32 public constant VAULT_MANAGER_ROLE = keccak256("VAULT_MANAGER_ROLE"); bytes32 private constant _FLASH_RETURN = keccak256("ERC3156FlashBorrower.onFlashLoan"); /// @notice Max utilization LTV before flash disabled (8000 = 80%, retains 20% buffer). uint256 public maxUtilizationLtvBps; /// @notice Max single flash as share of vault balance (5000 = 50%). uint256 public maxFlashShareOfVaultBps; /// @notice Flash fee in basis points (≤ 9 bps per policy). uint256 public flashFeeBps; /// @notice MPAP parity band — block flash if index drift vs XAU anchor exceeds this. uint256 public mpapParityBandBps; /// @notice Max indexValue age (seconds) for flash eligibility. uint256 public maxIndexAgeSeconds; /// @notice XAU/USD anchor for parity drift check (8 decimals, e.g. 2400_00000000). uint256 public xauUsdAnchor8; mapping(address => bool) public supportedToken; mapping(address => uint256) public vaultBalance; mapping(address => uint256) public outstandingFlash; event TokenSupported(address indexed token, bool supported); event Deposited(address indexed token, address indexed from, uint256 amount); event Withdrawn(address indexed token, address indexed to, uint256 amount); event RiskParamsUpdated( uint256 maxUtilizationLtvBps, uint256 maxFlashShareOfVaultBps, uint256 flashFeeBps, uint256 mpapParityBandBps ); constructor( address admin, uint256 maxUtilizationLtvBps_, uint256 maxFlashShareOfVaultBps_, uint256 flashFeeBps_, uint256 mpapParityBandBps_, uint256 maxIndexAgeSeconds_, uint256 xauUsdAnchor8_ ) { require(admin != address(0), "LiIndexFlashVault: zero admin"); require(maxUtilizationLtvBps_ <= 10_000, "LiIndexFlashVault: ltv"); require(maxFlashShareOfVaultBps_ <= 10_000, "LiIndexFlashVault: flash share"); require(flashFeeBps_ <= 1000, "LiIndexFlashVault: fee"); _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(VAULT_MANAGER_ROLE, admin); maxUtilizationLtvBps = maxUtilizationLtvBps_; maxFlashShareOfVaultBps = maxFlashShareOfVaultBps_; flashFeeBps = flashFeeBps_; mpapParityBandBps = mpapParityBandBps_; maxIndexAgeSeconds = maxIndexAgeSeconds_; xauUsdAnchor8 = xauUsdAnchor8_; } function setSupportedToken(address token, bool supported) external onlyRole(VAULT_MANAGER_ROLE) { supportedToken[token] = supported; emit TokenSupported(token, supported); } function setRiskParams( uint256 maxUtilizationLtvBps_, uint256 maxFlashShareOfVaultBps_, uint256 flashFeeBps_, uint256 mpapParityBandBps_ ) external onlyRole(DEFAULT_ADMIN_ROLE) { require(maxUtilizationLtvBps_ <= 10_000, "LiIndexFlashVault: ltv"); require(maxFlashShareOfVaultBps_ <= 10_000, "LiIndexFlashVault: flash share"); require(flashFeeBps_ <= 1000, "LiIndexFlashVault: fee"); maxUtilizationLtvBps = maxUtilizationLtvBps_; maxFlashShareOfVaultBps = maxFlashShareOfVaultBps_; flashFeeBps = flashFeeBps_; mpapParityBandBps = mpapParityBandBps_; emit RiskParamsUpdated(maxUtilizationLtvBps_, maxFlashShareOfVaultBps_, flashFeeBps_, mpapParityBandBps_); } function deposit(address token, uint256 amount) external nonReentrant { require(supportedToken[token], "LiIndexFlashVault: unsupported"); IERC20(token).safeTransferFrom(msg.sender, address(this), amount); vaultBalance[token] += amount; emit Deposited(token, msg.sender, amount); } function withdraw(address token, uint256 amount, address to) external nonReentrant onlyRole(VAULT_MANAGER_ROLE) { require(to != address(0), "LiIndexFlashVault: zero to"); vaultBalance[token] -= amount; IERC20(token).safeTransfer(to, amount); emit Withdrawn(token, to, amount); } function currentUtilizationBps(address token) public view returns (uint256) { uint256 bal = vaultBalance[token]; if (bal == 0) return 0; return (outstandingFlash[token] * 10_000) / bal; } function flashBorrowEligible(address token) public view returns (bool) { if (!supportedToken[token]) return false; if (currentUtilizationBps(token) >= maxUtilizationLtvBps) return false; IRWAToken idx = IRWAToken(token); if (!idx.isRwaIndex()) return false; uint256 updatedAt = idx.indexUpdatedAt(); if (updatedAt == 0 || block.timestamp - updatedAt > maxIndexAgeSeconds) return false; uint256 indexLevel = idx.indexValue(); if (indexLevel == 0) return false; if (xauUsdAnchor8 > 0 && mpapParityBandBps > 0) { uint256 anchorLevel6 = (xauUsdAnchor8 / 1e2); if (anchorLevel6 > 0) { uint256 driftBps = indexLevel > anchorLevel6 ? ((indexLevel - anchorLevel6) * 10_000) / indexLevel : ((anchorLevel6 - indexLevel) * 10_000) / anchorLevel6; if (driftBps >= mpapParityBandBps) return false; } } return true; } function maxFlashLoan(address token) external view returns (uint256) { if (!flashBorrowEligible(token)) return 0; uint256 bal = vaultBalance[token]; uint256 outstanding = outstandingFlash[token]; uint256 lendable = (bal * maxUtilizationLtvBps) / 10_000; if (outstanding >= lendable) return 0; uint256 headroom = lendable - outstanding; uint256 cap = (bal * maxFlashShareOfVaultBps) / 10_000; return headroom < cap ? headroom : cap; } function flashFee(address token, uint256 amount) external view returns (uint256) { token; amount; return (amount * flashFeeBps) / 10_000; } function flashLoan( IERC3156FlashBorrower receiver, address token, uint256 amount, bytes calldata data ) external nonReentrant returns (bool) { require(flashBorrowEligible(token), "LiIndexFlashVault: not eligible"); require(amount <= this.maxFlashLoan(token), "LiIndexFlashVault: exceeds max"); uint256 fee = (amount * flashFeeBps) / 10_000; outstandingFlash[token] += amount; IERC20(token).safeTransfer(address(receiver), amount); require( receiver.onFlashLoan(msg.sender, token, amount, fee, data) == _FLASH_RETURN, "LiIndexFlashVault: callback failed" ); IERC20(token).safeTransferFrom(address(receiver), address(this), amount + fee); outstandingFlash[token] -= amount; if (fee > 0) { vaultBalance[token] += fee; } return true; } }