// 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 {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; /// @notice Engine X XAUt-backed USDC borrowing vault. /// @dev This vault lends only pre-funded USDC. cWUSDC can be referenced as proof /// provenance, but debt is always repaid in official USDC. contract DBISEngineXXautUsdcBorrowVault is Ownable, ReentrancyGuard { using SafeERC20 for IERC20; uint256 public constant BPS = 10_000; uint256 public constant MAX_LTV_BPS = 9_500; uint256 public constant MAX_LIQUIDATION_THRESHOLD_BPS = 9_800; uint256 public constant MAX_LIQUIDATION_BONUS_BPS = 2_000; IERC20 public immutable xaut; IERC20 public immutable usdc; IERC20 public immutable cWUSDC; uint256 public xautUsdPrice6; uint256 public lastPriceUpdate; bytes32 public lastPriceSourceHash; uint256 public ltvBps; uint256 public liquidationThresholdBps; uint256 public minHealthFactorBps; uint256 public liquidationBonusBps; uint256 public maxBorrowUsdc; bool public paused; uint256 public lenderUsdcAvailable; uint256 public totalCollateralXaut; uint256 public totalDebtUsdc; uint256 public totalCwusdcProofRepayUsdc; struct Position { uint256 collateralXaut; uint256 debtUsdc; } mapping(address => Position) public positions; event PriceUpdated(uint256 price6, bytes32 indexed sourceHash); event RiskParamsUpdated( uint256 ltvBps, uint256 liquidationThresholdBps, uint256 minHealthFactorBps, uint256 liquidationBonusBps, uint256 maxBorrowUsdc ); event Paused(address indexed operator); event Unpaused(address indexed operator); event LenderFunded(address indexed funder, uint256 amount); event LenderUsdcWithdrawn(address indexed to, uint256 amount); event CollateralSupplied(address indexed account, uint256 amount); event CollateralWithdrawn(address indexed account, address indexed to, uint256 amount); event UsdcBorrowed(address indexed account, address indexed to, uint256 amount, uint256 debtAfter); event UsdcRepaid(address indexed account, address indexed payer, uint256 amount, uint256 debtAfter); event CwusdcSourcedRepay( address indexed account, address indexed payer, uint256 amount, bytes32 indexed publicSwapTxHash, bytes32 iso20022DocumentHash, bytes32 auditEnvelopeHash, bytes32 pegProofHash ); event Liquidated( address indexed account, address indexed liquidator, uint256 repayUsdc, uint256 seizedXaut, uint256 debtAfter ); event UnaccountedTokenWithdrawn(address indexed token, address indexed to, uint256 amount); modifier whenNotPaused() { require(!paused, "paused"); _; } constructor( address xaut_, address usdc_, address cWUSDC_, address owner_, uint256 xautUsdPrice6_, uint256 ltvBps_, uint256 liquidationThresholdBps_, uint256 minHealthFactorBps_, uint256 liquidationBonusBps_, uint256 maxBorrowUsdc_, bytes32 priceSourceHash_ ) Ownable(owner_) { require(xaut_ != address(0) && usdc_ != address(0) && cWUSDC_ != address(0), "zero token"); require(owner_ != address(0), "zero owner"); xaut = IERC20(xaut_); usdc = IERC20(usdc_); cWUSDC = IERC20(cWUSDC_); _setPrice(xautUsdPrice6_, priceSourceHash_); _setRiskParams( ltvBps_, liquidationThresholdBps_, minHealthFactorBps_, liquidationBonusBps_, maxBorrowUsdc_ ); } function pause() external onlyOwner { paused = true; emit Paused(msg.sender); } function unpause() external onlyOwner { paused = false; emit Unpaused(msg.sender); } function setXautUsdPrice6(uint256 price6, bytes32 sourceHash) external onlyOwner { _setPrice(price6, sourceHash); } function setRiskParams( uint256 ltvBps_, uint256 liquidationThresholdBps_, uint256 minHealthFactorBps_, uint256 liquidationBonusBps_, uint256 maxBorrowUsdc_ ) external onlyOwner { _setRiskParams( ltvBps_, liquidationThresholdBps_, minHealthFactorBps_, liquidationBonusBps_, maxBorrowUsdc_ ); } function fundLender(uint256 amount) external nonReentrant whenNotPaused { require(amount > 0, "zero fund"); usdc.safeTransferFrom(msg.sender, address(this), amount); lenderUsdcAvailable += amount; emit LenderFunded(msg.sender, amount); } function withdrawLenderUsdc(address to, uint256 amount) external onlyOwner nonReentrant { require(to != address(0), "zero to"); require(amount > 0, "zero withdraw"); require(amount <= lenderUsdcAvailable, "insufficient lender usdc"); lenderUsdcAvailable -= amount; usdc.safeTransfer(to, amount); emit LenderUsdcWithdrawn(to, amount); } function supplyCollateral(uint256 amount) external nonReentrant whenNotPaused { require(amount > 0, "zero collateral"); xaut.safeTransferFrom(msg.sender, address(this), amount); positions[msg.sender].collateralXaut += amount; totalCollateralXaut += amount; emit CollateralSupplied(msg.sender, amount); } function withdrawCollateral(uint256 amount, address to) external nonReentrant whenNotPaused { require(to != address(0), "zero to"); require(amount > 0, "zero collateral"); Position storage position = positions[msg.sender]; require(amount <= position.collateralXaut, "insufficient collateral"); position.collateralXaut -= amount; totalCollateralXaut -= amount; _requireHealthy(msg.sender); xaut.safeTransfer(to, amount); emit CollateralWithdrawn(msg.sender, to, amount); } function borrowUsdc(uint256 amount, address to) external nonReentrant whenNotPaused { require(to != address(0), "zero to"); require(amount > 0, "zero borrow"); require(amount <= lenderUsdcAvailable, "insufficient lender usdc"); if (maxBorrowUsdc != 0) { require(totalDebtUsdc + amount <= maxBorrowUsdc, "max borrow exceeded"); } Position storage position = positions[msg.sender]; position.debtUsdc += amount; totalDebtUsdc += amount; lenderUsdcAvailable -= amount; _requireHealthy(msg.sender); usdc.safeTransfer(to, amount); emit UsdcBorrowed(msg.sender, to, amount, position.debtUsdc); } function repayUsdc(uint256 amount) external nonReentrant whenNotPaused returns (uint256 repaid) { repaid = _repay(msg.sender, msg.sender, amount); } function repayUsdcFor(address account, uint256 amount) external nonReentrant whenNotPaused returns (uint256 repaid) { require(account != address(0), "zero account"); repaid = _repay(account, msg.sender, amount); } function repayUsdcFromCwusdcProof( uint256 amount, bytes32 publicSwapTxHash, bytes32 iso20022DocumentHash, bytes32 auditEnvelopeHash, bytes32 pegProofHash ) external nonReentrant whenNotPaused returns (uint256 repaid) { require(publicSwapTxHash != bytes32(0), "zero swap hash"); require(iso20022DocumentHash != bytes32(0), "zero iso hash"); require(auditEnvelopeHash != bytes32(0), "zero audit hash"); require(pegProofHash != bytes32(0), "zero peg hash"); repaid = _repay(msg.sender, msg.sender, amount); totalCwusdcProofRepayUsdc += repaid; emit CwusdcSourcedRepay( msg.sender, msg.sender, repaid, publicSwapTxHash, iso20022DocumentHash, auditEnvelopeHash, pegProofHash ); } function liquidate(address account, uint256 repayAmount) external nonReentrant whenNotPaused returns (uint256 seized) { require(account != address(0), "zero account"); require(repayAmount > 0, "zero repay"); require(healthFactorBps(account) < minHealthFactorBps, "position healthy"); Position storage position = positions[account]; uint256 repaid = repayAmount > position.debtUsdc ? position.debtUsdc : repayAmount; require(repaid > 0, "zero debt"); seized = _xautForUsdcWithBonus(repaid); require(seized <= position.collateralXaut, "insufficient collateral"); usdc.safeTransferFrom(msg.sender, address(this), repaid); position.debtUsdc -= repaid; totalDebtUsdc -= repaid; lenderUsdcAvailable += repaid; position.collateralXaut -= seized; totalCollateralXaut -= seized; xaut.safeTransfer(msg.sender, seized); emit Liquidated(account, msg.sender, repaid, seized, position.debtUsdc); } function rescueUnaccountedToken(address token, address to, uint256 amount) external onlyOwner nonReentrant { require(to != address(0), "zero to"); require(amount > 0, "zero withdraw"); IERC20(token).safeTransfer(to, amount); _requireAccountingCollateralized(); emit UnaccountedTokenWithdrawn(token, to, amount); } function collateralValueUsd6(address account) public view returns (uint256) { return collateralValueUsd6ForRaw(positions[account].collateralXaut); } function collateralValueUsd6ForRaw(uint256 xautRaw) public view returns (uint256) { return (xautRaw * xautUsdPrice6) / 1e6; } function maxDebtForCollateral(uint256 xautRaw) public view returns (uint256) { uint256 collateralUsd6 = collateralValueUsd6ForRaw(xautRaw); uint256 byLtv = (collateralUsd6 * ltvBps) / BPS; uint256 byHealth = (collateralUsd6 * liquidationThresholdBps) / minHealthFactorBps; return byLtv < byHealth ? byLtv : byHealth; } function maxAdditionalBorrow(address account) public view returns (uint256) { uint256 maxDebt = maxDebtForCollateral(positions[account].collateralXaut); if (positions[account].debtUsdc >= maxDebt) { return 0; } return maxDebt - positions[account].debtUsdc; } function healthFactorBps(address account) public view returns (uint256) { Position memory position = positions[account]; if (position.debtUsdc == 0) { return type(uint256).max; } uint256 collateralUsd6 = collateralValueUsd6ForRaw(position.collateralXaut); return (collateralUsd6 * liquidationThresholdBps) / position.debtUsdc; } function _repay(address account, address payer, uint256 amount) internal returns (uint256 repaid) { require(amount > 0, "zero repay"); Position storage position = positions[account]; require(position.debtUsdc > 0, "zero debt"); repaid = amount > position.debtUsdc ? position.debtUsdc : amount; usdc.safeTransferFrom(payer, address(this), repaid); position.debtUsdc -= repaid; totalDebtUsdc -= repaid; lenderUsdcAvailable += repaid; emit UsdcRepaid(account, payer, repaid, position.debtUsdc); } function _requireHealthy(address account) internal view { Position memory position = positions[account]; if (position.debtUsdc == 0) { return; } require(position.debtUsdc <= maxDebtForCollateral(position.collateralXaut), "exceeds collateral"); require(healthFactorBps(account) >= minHealthFactorBps, "health too low"); } function _xautForUsdcWithBonus(uint256 usdcAmount) internal view returns (uint256) { uint256 numerator = usdcAmount * 1e6 * (BPS + liquidationBonusBps); uint256 denominator = xautUsdPrice6 * BPS; return (numerator + denominator - 1) / denominator; } function _setPrice(uint256 price6, bytes32 sourceHash) internal { require(price6 > 0, "zero price"); require(sourceHash != bytes32(0), "zero source"); xautUsdPrice6 = price6; lastPriceUpdate = block.timestamp; lastPriceSourceHash = sourceHash; emit PriceUpdated(price6, sourceHash); } function _setRiskParams( uint256 ltvBps_, uint256 liquidationThresholdBps_, uint256 minHealthFactorBps_, uint256 liquidationBonusBps_, uint256 maxBorrowUsdc_ ) internal { require(ltvBps_ > 0 && ltvBps_ <= MAX_LTV_BPS, "bad ltv"); require( liquidationThresholdBps_ >= ltvBps_ && liquidationThresholdBps_ <= MAX_LIQUIDATION_THRESHOLD_BPS, "bad threshold" ); require(minHealthFactorBps_ >= BPS, "bad health"); require(liquidationBonusBps_ <= MAX_LIQUIDATION_BONUS_BPS, "bad bonus"); ltvBps = ltvBps_; liquidationThresholdBps = liquidationThresholdBps_; minHealthFactorBps = minHealthFactorBps_; liquidationBonusBps = liquidationBonusBps_; maxBorrowUsdc = maxBorrowUsdc_; emit RiskParamsUpdated( ltvBps_, liquidationThresholdBps_, minHealthFactorBps_, liquidationBonusBps_, maxBorrowUsdc_ ); } function _requireAccountingCollateralized() internal view { require(xaut.balanceOf(address(this)) >= totalCollateralXaut, "xaut undercollateralized"); require(usdc.balanceOf(address(this)) >= lenderUsdcAvailable, "usdc undercollateralized"); } }