// 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 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 Ownable, ReentrancyGuard { 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 totalVirtualLoops; uint256 public totalVirtualDebtUsdc; uint256 public totalVirtualCwusdcIn; mapping(bytes32 => bool) public usedProofIds; 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 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_ ) 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 { 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 { 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 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 { _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 { 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 { 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 _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 withdraw(address token, address to, uint256 amount) external onlyOwner nonReentrant { 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; } }