// 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"; /// @notice Mainnet proof vault for Engine X with automatic cWUSDC surplus neutralization. /// @dev Surplus cWUSDC is removed from pool reserves and transferred to a neutral receiver. contract DBISEngineXMaintainedProofVault is Ownable { using SafeERC20 for IERC20; IERC20 public immutable cWUSDC; IERC20 public immutable usdc; IERC20 public immutable xaut; uint256 public poolCwusdcReserve; uint256 public poolUsdcReserve; uint256 public lenderUsdcAvailable; uint256 public totalNeutralizedCwusdc; uint256 public immutable xautUsdPrice6; uint256 public immutable ltvBps; uint256 public immutable maxRoundTripLossBps; uint256 public immutable maxSurplusDust; address public surplusReceiver; event PoolSeeded(uint256 cwusdcAmount, uint256 usdcAmount); event LenderFunded(uint256 usdcAmount); event SurplusReceiverUpdated(address indexed receiver); event SurplusNeutralized(bytes32 indexed proofId, uint256 indexed step, address indexed receiver, uint256 amount); event ProofStep( bytes32 indexed proofId, uint256 indexed step, uint256 debtUsdc, uint256 collateralXaut, uint256 cwusdcIn, uint256 cwusdcOut, uint256 cwusdcLoss, uint256 neutralizedCwusdc, uint256 poolCwusdcReserveAfter, uint256 poolUsdcReserveAfter ); event ProofClosed( bytes32 indexed proofId, uint256 loops, uint256 totalDebtUsdc, uint256 totalCwusdcIn, uint256 totalNeutralizedCwusdcAmount ); event OwnerWithdraw(address indexed token, address indexed to, uint256 amount); constructor( address cWUSDC_, address usdc_, address xaut_, address owner_, address surplusReceiver_, uint256 xautUsdPrice6_, uint256 ltvBps_, uint256 maxRoundTripLossBps_, uint256 maxSurplusDust_ ) Ownable(owner_) { require(cWUSDC_ != address(0) && usdc_ != address(0) && xaut_ != address(0), "zero token"); require(owner_ != address(0), "zero owner"); require(surplusReceiver_ != address(0), "zero receiver"); require(xautUsdPrice6_ > 0, "zero price"); require(ltvBps_ > 0 && ltvBps_ < 10_000, "bad ltv"); require(maxRoundTripLossBps_ > 0 && maxRoundTripLossBps_ < 10_000, "bad loss"); cWUSDC = IERC20(cWUSDC_); usdc = IERC20(usdc_); xaut = IERC20(xaut_); surplusReceiver = surplusReceiver_; xautUsdPrice6 = xautUsdPrice6_; ltvBps = ltvBps_; maxRoundTripLossBps = maxRoundTripLossBps_; maxSurplusDust = maxSurplusDust_; } function seedPool(uint256 cwusdcAmount, uint256 usdcAmount) external onlyOwner { require(cwusdcAmount > 0 && usdcAmount > 0, "zero seed"); cWUSDC.safeTransferFrom(msg.sender, address(this), cwusdcAmount); usdc.safeTransferFrom(msg.sender, address(this), usdcAmount); poolCwusdcReserve += cwusdcAmount; poolUsdcReserve += usdcAmount; emit PoolSeeded(cwusdcAmount, usdcAmount); } function fundLender(uint256 usdcAmount) external onlyOwner { require(usdcAmount > 0, "zero fund"); usdc.safeTransferFrom(msg.sender, address(this), usdcAmount); lenderUsdcAvailable += usdcAmount; emit LenderFunded(usdcAmount); } function setSurplusReceiver(address receiver) external onlyOwner { require(receiver != address(0), "zero receiver"); surplusReceiver = receiver; emit SurplusReceiverUpdated(receiver); } function previewCwusdcInForExactUsdc(uint256 usdcOut) public view returns (uint256) { return _getAmountIn(usdcOut, poolCwusdcReserve, poolUsdcReserve); } function previewCwusdcOutForExactUsdcIn(uint256 usdcIn) public view returns (uint256) { return _getAmountOut(usdcIn, poolUsdcReserve, poolCwusdcReserve); } function currentSurplusCwusdc() public view returns (uint256) { return poolCwusdcReserve > poolUsdcReserve ? poolCwusdcReserve - poolUsdcReserve : 0; } function minimumXautCollateral(uint256 debtUsdc) public view returns (uint256) { uint256 numerator = debtUsdc * 1e6 * 10_000; uint256 denominator = xautUsdPrice6 * ltvBps; return (numerator + denominator - 1) / denominator; } function runProof(bytes32 proofId, uint256 debtUsdcPerLoop, uint256 loops) external { _runProofFor(msg.sender, proofId, debtUsdcPerLoop, loops); } function runProofBatch(bytes32[] calldata proofIds, uint256[] calldata debtsUsdcPerLoop, uint256[] calldata loops_) external { require(proofIds.length == debtsUsdcPerLoop.length && proofIds.length == loops_.length, "length mismatch"); for (uint256 i = 0; i < proofIds.length; i++) { _runProofFor(msg.sender, proofIds[i], debtsUsdcPerLoop[i], loops_[i]); } } function _runProofFor(address account, bytes32 proofId, uint256 debtUsdcPerLoop, uint256 loops) internal { require(proofId != bytes32(0), "zero proof"); require(debtUsdcPerLoop > 0, "zero debt"); require(loops > 0 && loops <= 32, "bad loops"); uint256 collateralPerLoop = minimumXautCollateral(debtUsdcPerLoop); uint256 totalCollateral = collateralPerLoop * loops; uint256 totalDebt; uint256 totalCwusdcIn; uint256 neutralizedInProof; xaut.safeTransferFrom(account, address(this), totalCollateral); for (uint256 i = 0; i < loops; i++) { require(lenderUsdcAvailable >= debtUsdcPerLoop, "insufficient lender usdc"); uint256 poolUsdcBefore = poolUsdcReserve; lenderUsdcAvailable -= debtUsdcPerLoop; uint256 cwusdcIn = _getAmountIn(debtUsdcPerLoop, poolCwusdcReserve, poolUsdcReserve); cWUSDC.safeTransferFrom(account, address(this), cwusdcIn); poolCwusdcReserve += cwusdcIn; poolUsdcReserve -= debtUsdcPerLoop; lenderUsdcAvailable += debtUsdcPerLoop; uint256 cwusdcOut = _getAmountOut(debtUsdcPerLoop, poolUsdcReserve, poolCwusdcReserve); poolUsdcReserve += debtUsdcPerLoop; poolCwusdcReserve -= cwusdcOut; cWUSDC.safeTransfer(account, cwusdcOut); uint256 cwusdcLoss = cwusdcIn - cwusdcOut; require(cwusdcLoss * 10_000 <= cwusdcIn * maxRoundTripLossBps, "roundtrip loss too high"); require(poolUsdcReserve == poolUsdcBefore, "usdc reserve drift"); uint256 neutralized = _neutralizeSurplus(proofId, i + 1); neutralizedInProof += neutralized; totalDebt += debtUsdcPerLoop; totalCwusdcIn += cwusdcIn; emit ProofStep( proofId, i + 1, debtUsdcPerLoop, collateralPerLoop, cwusdcIn, cwusdcOut, cwusdcLoss, neutralized, poolCwusdcReserve, poolUsdcReserve ); } xaut.safeTransfer(account, totalCollateral); emit ProofClosed(proofId, loops, totalDebt, totalCwusdcIn, neutralizedInProof); } function _neutralizeSurplus(bytes32 proofId, uint256 step) internal returns (uint256 neutralized) { uint256 surplus = currentSurplusCwusdc(); if (surplus <= maxSurplusDust) { return 0; } neutralized = surplus - maxSurplusDust; poolCwusdcReserve -= neutralized; totalNeutralizedCwusdc += neutralized; cWUSDC.safeTransfer(surplusReceiver, neutralized); emit SurplusNeutralized(proofId, step, surplusReceiver, neutralized); } function withdraw(address token, address to, uint256 amount) external onlyOwner { require(to != address(0), "zero to"); IERC20(token).safeTransfer(to, amount); emit OwnerWithdraw(token, to, amount); } function _getAmountIn(uint256 amountOut, uint256 reserveIn, uint256 reserveOut) internal pure returns (uint256) { require(amountOut > 0, "insufficient output"); require(reserveIn > 0 && reserveOut > amountOut, "insufficient liquidity"); uint256 numerator = reserveIn * amountOut * 1000; uint256 denominator = (reserveOut - amountOut) * 997; return (numerator / denominator) + 1; } function _getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut) internal pure returns (uint256) { require(amountIn > 0, "insufficient input"); require(reserveIn > 0 && reserveOut > 0, "insufficient liquidity"); uint256 amountInWithFee = amountIn * 997; uint256 numerator = amountInWithFee * reserveOut; uint256 denominator = reserveIn * 1000 + amountInWithFee; return numerator / denominator; } }