Files
smom-dbis-138/contracts/rwa/LiIndexFlashVault.sol

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;
}
}