- Expand token-aggregation API (report routes), canonical tokens, pools - Add flash vault contracts + tests (indexed, DODO cwUSDC, XAUT borrow) - PMM pools JSON, deploy/export scripts, metamask verified list Co-authored-by: Cursor <cursoragent@cursor.com>
345 lines
13 KiB
Solidity
345 lines
13 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 {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");
|
|
}
|
|
}
|