// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {IERC3156FlashBorrower} from "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol"; import {IERC3156FlashLender} from "@openzeppelin/contracts/interfaces/IERC3156FlashLender.sol"; 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 maintained proof vault with virtual batch settlement. /// @dev Use only for accounting proofs: it compresses identical maintained loops into one net settlement. contract DBISEngineXVirtualBatchVault is IERC3156FlashLender, Ownable, ReentrancyGuard { using SafeERC20 for IERC20; bytes32 private constant _FLASH_RETURN_VALUE = keccak256("ERC3156FlashBorrower.onFlashLoan"); uint256 public constant MAX_FLASH_FEE_BPS = 1_000; 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 totalVirtualLoops; uint256 public totalVirtualDebtUsdc; uint256 public totalVirtualCwusdcIn; uint256 public totalFlashFeesCollectedUsdc; uint256 public flashFeeBps = 5; uint256 public maxFlashLoanAmount; bool public paused; bool public flashBorrowerAllowlistEnabled; mapping(bytes32 => bool) public usedProofIds; mapping(address => bool) public approvedFlashBorrower; uint256 public immutable xautUsdPrice6; uint256 public immutable ltvBps; uint256 public immutable maxRoundTripLossBps; address public surplusReceiver; event PoolSeeded(uint256 cwusdcAmount, uint256 usdcAmount); event LenderFunded(uint256 usdcAmount); event SurplusReceiverUpdated(address indexed receiver); event VirtualProofClosed( bytes32 indexed proofId, uint256 virtualLoops, uint256 debtUsdcPerLoop, uint256 collateralXaut, uint256 cwusdcInPerLoop, uint256 cwusdcOutPerLoop, uint256 cwusdcLossPerLoop, uint256 totalCwusdcInAmount, uint256 totalCwusdcOutAmount, uint256 totalNeutralizedCwusdcAmount, uint256 poolCwusdcReserveAfter, uint256 poolUsdcReserveAfter ); event VirtualProofAuditEvidence( bytes32 indexed proofId, address indexed operator, address indexed outputRecipient, uint256 exactOutputAmount, uint256 outputRoundingAmount, address roundingReceiver, bytes32 iso20022DocumentHash, bytes32 auditEnvelopeHash, bytes32 pegProofHash ); event PoolLiquidityWithdrawn(address indexed to, uint256 cwusdcAmount, uint256 usdcAmount); event LenderUsdcWithdrawn(address indexed to, uint256 amount); event UnaccountedTokenWithdrawn(address indexed token, address indexed to, uint256 amount); event FlashFeeBpsUpdated(uint256 feeBps); event MaxFlashLoanAmountUpdated(uint256 amount); event Paused(address indexed operator); event Unpaused(address indexed operator); event FlashBorrowerAllowlistEnabledUpdated(bool enabled); event FlashBorrowerApprovalUpdated(address indexed borrower, bool approved); event EngineXFlashLoan( address indexed initiator, IERC3156FlashBorrower indexed receiver, address indexed token, uint256 amount, uint256 fee ); modifier whenNotPaused() { require(!paused, "paused"); _; } constructor( address cWUSDC_, address usdc_, address xaut_, address owner_, address surplusReceiver_, uint256 xautUsdPrice6_, uint256 ltvBps_, uint256 maxRoundTripLossBps_ ) 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_; } function seedPool(uint256 cwusdcAmount, uint256 usdcAmount) external onlyOwner nonReentrant whenNotPaused { 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 nonReentrant whenNotPaused { 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 pause() external onlyOwner { paused = true; emit Paused(msg.sender); } function unpause() external onlyOwner { paused = false; emit Unpaused(msg.sender); } function setFlashFeeBps(uint256 newFlashFeeBps) external onlyOwner { require(newFlashFeeBps <= MAX_FLASH_FEE_BPS, "fee too high"); flashFeeBps = newFlashFeeBps; emit FlashFeeBpsUpdated(newFlashFeeBps); } function setMaxFlashLoanAmount(uint256 amount) external onlyOwner { maxFlashLoanAmount = amount; emit MaxFlashLoanAmountUpdated(amount); } function setFlashBorrowerAllowlistEnabled(bool enabled) external onlyOwner { flashBorrowerAllowlistEnabled = enabled; emit FlashBorrowerAllowlistEnabledUpdated(enabled); } function setFlashBorrowerApproved(address borrower, bool approved) external onlyOwner { require(borrower != address(0), "zero borrower"); approvedFlashBorrower[borrower] = approved; emit FlashBorrowerApprovalUpdated(borrower, approved); } 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 maxFlashLoan(address token) external view override returns (uint256) { if (paused || token != address(usdc)) { return 0; } if (maxFlashLoanAmount == 0 || maxFlashLoanAmount > lenderUsdcAvailable) { return lenderUsdcAvailable; } return maxFlashLoanAmount; } function flashFee(address token, uint256 amount) public view override returns (uint256) { require(token == address(usdc), "unsupported flash token"); return (amount * flashFeeBps) / 10_000; } function minimumXautCollateral(uint256 debtUsdc) public view returns (uint256) { uint256 numerator = debtUsdc * 1e6 * 10_000; uint256 denominator = xautUsdPrice6 * ltvBps; return (numerator + denominator - 1) / denominator; } function previewVirtualProof(uint256 debtUsdcPerLoop, uint256 virtualLoops) public view returns ( uint256 collateralXaut, uint256 cwusdcInPerLoop, uint256 cwusdcOutPerLoop, uint256 cwusdcLossPerLoop, uint256 totalCwusdcInAmount, uint256 totalCwusdcOutAmount, uint256 totalNeutralizedCwusdcAmount ) { require(virtualLoops > 0, "zero loops"); require(poolCwusdcReserve == poolUsdcReserve, "pool not maintained"); require(lenderUsdcAvailable >= debtUsdcPerLoop, "insufficient lender usdc"); collateralXaut = minimumXautCollateral(debtUsdcPerLoop); cwusdcInPerLoop = _getAmountIn(debtUsdcPerLoop, poolCwusdcReserve, poolUsdcReserve); uint256 cwusdcReserveAfterIn = poolCwusdcReserve + cwusdcInPerLoop; uint256 usdcReserveAfterOut = poolUsdcReserve - debtUsdcPerLoop; cwusdcOutPerLoop = _getAmountOut(debtUsdcPerLoop, usdcReserveAfterOut, cwusdcReserveAfterIn); cwusdcLossPerLoop = cwusdcInPerLoop - cwusdcOutPerLoop; require(cwusdcLossPerLoop * 10_000 <= cwusdcInPerLoop * maxRoundTripLossBps, "roundtrip loss too high"); totalCwusdcInAmount = cwusdcInPerLoop * virtualLoops; totalCwusdcOutAmount = cwusdcOutPerLoop * virtualLoops; totalNeutralizedCwusdcAmount = cwusdcLossPerLoop * virtualLoops; } function runVirtualProof(bytes32 proofId, uint256 debtUsdcPerLoop, uint256 virtualLoops) external nonReentrant whenNotPaused { _runVirtualProofFor( msg.sender, proofId, debtUsdcPerLoop, virtualLoops, 0, msg.sender, bytes32(0), bytes32(0), bytes32(0) ); } function runVirtualProofTo(bytes32 proofId, uint256 debtUsdcPerLoop, uint256 virtualLoops, address outputRecipient) external nonReentrant whenNotPaused { require(outputRecipient != address(0), "zero output"); _runVirtualProofFor( outputRecipient, proofId, debtUsdcPerLoop, virtualLoops, 0, msg.sender, bytes32(0), bytes32(0), bytes32(0) ); } function runVirtualProofExactOutTo( bytes32 proofId, uint256 debtUsdcPerLoop, uint256 virtualLoops, address outputRecipient, uint256 exactOutputAmount, address roundingReceiver, bytes32 iso20022DocumentHash, bytes32 auditEnvelopeHash, bytes32 pegProofHash ) external nonReentrant whenNotPaused { require(outputRecipient != address(0), "zero output"); require(exactOutputAmount > 0, "zero exact output"); require(roundingReceiver != address(0), "zero rounding"); require(iso20022DocumentHash != bytes32(0), "zero iso hash"); require(auditEnvelopeHash != bytes32(0), "zero audit hash"); require(pegProofHash != bytes32(0), "zero peg hash"); _runVirtualProofFor( outputRecipient, proofId, debtUsdcPerLoop, virtualLoops, exactOutputAmount, roundingReceiver, iso20022DocumentHash, auditEnvelopeHash, pegProofHash ); } function flashLoan(IERC3156FlashBorrower receiver, address token, uint256 amount, bytes calldata data) external override nonReentrant whenNotPaused returns (bool) { require(token == address(usdc), "unsupported flash token"); require(amount > 0, "zero flash amount"); require(amount <= lenderUsdcAvailable, "insufficient lender usdc"); require(maxFlashLoanAmount == 0 || amount <= maxFlashLoanAmount, "flash amount too high"); require( !flashBorrowerAllowlistEnabled || approvedFlashBorrower[address(receiver)], "flash borrower not approved" ); uint256 fee = flashFee(token, amount); uint256 balanceBefore = usdc.balanceOf(address(this)); require(balanceBefore >= poolUsdcReserve + lenderUsdcAvailable, "accounting undercollateralized"); lenderUsdcAvailable -= amount; usdc.safeTransfer(address(receiver), amount); bytes32 retval = receiver.onFlashLoan(msg.sender, token, amount, fee, data); require(retval == _FLASH_RETURN_VALUE, "invalid flash callback"); uint256 expectedBalance = balanceBefore + fee; uint256 balanceAfterCallback = usdc.balanceOf(address(this)); if (balanceAfterCallback < expectedBalance) { usdc.safeTransferFrom(address(receiver), address(this), expectedBalance - balanceAfterCallback); } require(usdc.balanceOf(address(this)) >= expectedBalance, "flash repayment failed"); lenderUsdcAvailable += amount + fee; totalFlashFeesCollectedUsdc += fee; require( usdc.balanceOf(address(this)) >= poolUsdcReserve + lenderUsdcAvailable, "accounting undercollateralized" ); emit EngineXFlashLoan(msg.sender, receiver, token, amount, fee); return true; } function _runVirtualProofFor( address outputRecipient, bytes32 proofId, uint256 debtUsdcPerLoop, uint256 virtualLoops, uint256 exactOutputAmount, address roundingReceiver, bytes32 iso20022DocumentHash, bytes32 auditEnvelopeHash, bytes32 pegProofHash ) internal { require(proofId != bytes32(0), "zero proof"); require(!usedProofIds[proofId], "proof used"); usedProofIds[proofId] = true; ( uint256 collateralXaut, uint256 cwusdcInPerLoop, uint256 cwusdcOutPerLoop, uint256 cwusdcLossPerLoop, uint256 totalCwusdcInAmount, uint256 totalCwusdcOutAmount, uint256 totalNeutralizedCwusdcAmount ) = previewVirtualProof(debtUsdcPerLoop, virtualLoops); require(exactOutputAmount <= totalCwusdcOutAmount, "exact output too high"); uint256 outputAmount = exactOutputAmount == 0 ? totalCwusdcOutAmount : exactOutputAmount; uint256 outputRoundingAmount = totalCwusdcOutAmount - outputAmount; xaut.safeTransferFrom(msg.sender, address(this), collateralXaut); cWUSDC.safeTransferFrom(msg.sender, address(this), totalCwusdcInAmount); cWUSDC.safeTransfer(outputRecipient, outputAmount); if (outputRoundingAmount > 0) { cWUSDC.safeTransfer(roundingReceiver, outputRoundingAmount); } cWUSDC.safeTransfer(surplusReceiver, totalNeutralizedCwusdcAmount); xaut.safeTransfer(msg.sender, collateralXaut); totalNeutralizedCwusdc += totalNeutralizedCwusdcAmount; totalVirtualLoops += virtualLoops; totalVirtualDebtUsdc += debtUsdcPerLoop * virtualLoops; totalVirtualCwusdcIn += totalCwusdcInAmount; emit VirtualProofClosed( proofId, virtualLoops, debtUsdcPerLoop, collateralXaut, cwusdcInPerLoop, cwusdcOutPerLoop, cwusdcLossPerLoop, totalCwusdcInAmount, totalCwusdcOutAmount, totalNeutralizedCwusdcAmount, poolCwusdcReserve, poolUsdcReserve ); if (iso20022DocumentHash != bytes32(0) || auditEnvelopeHash != bytes32(0) || pegProofHash != bytes32(0)) { emit VirtualProofAuditEvidence( proofId, msg.sender, outputRecipient, exactOutputAmount, outputRoundingAmount, roundingReceiver, iso20022DocumentHash, auditEnvelopeHash, pegProofHash ); } } function withdrawPoolLiquidity(address to, uint256 cwusdcAmount, uint256 usdcAmount) external onlyOwner nonReentrant { require(to != address(0), "zero to"); require(cwusdcAmount > 0 || usdcAmount > 0, "zero withdraw"); require(cwusdcAmount <= poolCwusdcReserve, "insufficient pool cwusdc"); require(usdcAmount <= poolUsdcReserve, "insufficient pool usdc"); uint256 nextCwusdcReserve = poolCwusdcReserve - cwusdcAmount; uint256 nextUsdcReserve = poolUsdcReserve - usdcAmount; require(nextCwusdcReserve == nextUsdcReserve, "would break maintained pool"); poolCwusdcReserve = nextCwusdcReserve; poolUsdcReserve = nextUsdcReserve; if (cwusdcAmount > 0) { cWUSDC.safeTransfer(to, cwusdcAmount); } if (usdcAmount > 0) { usdc.safeTransfer(to, usdcAmount); } emit PoolLiquidityWithdrawn(to, cwusdcAmount, usdcAmount); } 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); } /// @notice Rescue or migrate only tokens that are not backing Engine X pool/lender accounting. function withdraw(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 _requireAccountingCollateralized() internal view { require(cWUSDC.balanceOf(address(this)) >= poolCwusdcReserve, "accounting undercollateralized"); require( usdc.balanceOf(address(this)) >= poolUsdcReserve + lenderUsdcAvailable, "accounting undercollateralized" ); } 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; } }