185 lines
7.7 KiB
Solidity
185 lines
7.7 KiB
Solidity
// 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;
|
|
}
|
|
}
|