diff --git a/contracts/flash/DBISEngineXVirtualBatchVault.sol b/contracts/flash/DBISEngineXVirtualBatchVault.sol new file mode 100644 index 0000000..d9d2e20 --- /dev/null +++ b/contracts/flash/DBISEngineXVirtualBatchVault.sol @@ -0,0 +1,297 @@ +// 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; + } +} diff --git a/test/flash/DBISEngineXVirtualBatchVault.t.sol b/test/flash/DBISEngineXVirtualBatchVault.t.sol new file mode 100644 index 0000000..9079a3d --- /dev/null +++ b/test/flash/DBISEngineXVirtualBatchVault.t.sol @@ -0,0 +1,246 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {DBISEngineXVirtualBatchVault} from "../../contracts/flash/DBISEngineXVirtualBatchVault.sol"; +import {MockMintableToken} from "../dbis/MockMintableToken.sol"; + +contract DBISEngineXVirtualBatchVaultTest is Test { + MockMintableToken internal cwusdc; + MockMintableToken internal usdc; + MockMintableToken internal xaut; + DBISEngineXVirtualBatchVault internal vault; + + address internal constant USER = address(0xBEEF); + address internal constant SURPLUS_RECEIVER = address(0xCAFE); + address internal constant OUTPUT_RECIPIENT = address(0xD00D); + address internal constant ROUNDING_RECEIVER = address(0xA11CE); + + uint256 internal constant LIVE_POOL_RESERVE = 85_763_529; + uint256 internal constant LENDER_USDC = 5_000_000; + uint256 internal constant XAUT_USD_PRICE6 = 3_226_640_000; + uint256 internal constant LTV_BPS = 8_000; + uint256 internal constant MAX_ROUND_TRIP_LOSS_BPS = 100; + bytes32 internal constant ISO_HASH = bytes32(uint256(0x1001)); + bytes32 internal constant AUDIT_HASH = bytes32(uint256(0x1002)); + bytes32 internal constant PEG_HASH = bytes32(uint256(0x1003)); + + event VirtualProofAuditEvidence( + bytes32 indexed proofId, + address indexed operator, + address indexed outputRecipient, + uint256 exactOutputAmount, + uint256 outputRoundingAmount, + address roundingReceiver, + bytes32 iso20022DocumentHash, + bytes32 auditEnvelopeHash, + bytes32 pegProofHash + ); + + function setUp() public { + cwusdc = new MockMintableToken("Wrapped cWUSDC", "cWUSDC", 6, address(this)); + usdc = new MockMintableToken("USD Coin", "USDC", 6, address(this)); + xaut = new MockMintableToken("Tether Gold", "XAUt", 6, address(this)); + + vault = new DBISEngineXVirtualBatchVault( + address(cwusdc), + address(usdc), + address(xaut), + address(this), + SURPLUS_RECEIVER, + XAUT_USD_PRICE6, + LTV_BPS, + MAX_ROUND_TRIP_LOSS_BPS + ); + + cwusdc.mint(address(this), LIVE_POOL_RESERVE); + usdc.mint(address(this), LIVE_POOL_RESERVE + LENDER_USDC); + cwusdc.approve(address(vault), type(uint256).max); + usdc.approve(address(vault), type(uint256).max); + vault.seedPool(LIVE_POOL_RESERVE, LIVE_POOL_RESERVE); + vault.fundLender(LENDER_USDC); + + cwusdc.mint(USER, 10_000_000_000_000_000); + xaut.mint(USER, 10_000_000); + vm.startPrank(USER); + cwusdc.approve(address(vault), type(uint256).max); + xaut.approve(address(vault), type(uint256).max); + vm.stopPrank(); + } + + function testPreviewMatchesLiveMaintainedLoopMath() public view { + ( + uint256 collateralXaut, + uint256 cwusdcInPerLoop, + uint256 cwusdcOutPerLoop, + uint256 cwusdcLossPerLoop, + uint256 totalCwusdcInAmount, + uint256 totalCwusdcOutAmount, + uint256 totalNeutralizedCwusdcAmount + ) = vault.previewVirtualProof(LENDER_USDC, 3); + + assertEq(collateralXaut, 1_937, "collateral should match live 5 USDC floor"); + assertEq(cwusdcInPerLoop, 5_325_523, "cwusdc in should match live proof"); + assertEq(cwusdcOutPerLoop, 5_295_471, "cwusdc out should match live proof"); + assertEq(cwusdcLossPerLoop, 30_052, "neutralized loss should match live proof"); + assertEq(totalCwusdcInAmount, 15_976_569, "three-loop cWUSDC in should match live proof"); + assertEq(totalCwusdcOutAmount, 15_886_413, "three-loop cWUSDC out should match live proof"); + assertEq(totalNeutralizedCwusdcAmount, 90_156, "three-loop neutralization should match live proof"); + } + + function testVirtualProofNetSettlesAndKeepsPoolMaintained() public { + uint256 userCwusdcBefore = cwusdc.balanceOf(USER); + uint256 userXautBefore = xaut.balanceOf(USER); + + vm.prank(USER); + vault.runVirtualProof(bytes32("proof-1"), LENDER_USDC, 3); + + assertEq(vault.poolCwusdcReserve(), LIVE_POOL_RESERVE, "pool cWUSDC reserve should not drift"); + assertEq(vault.poolUsdcReserve(), LIVE_POOL_RESERVE, "pool USDC reserve should not drift"); + assertEq(vault.currentSurplusCwusdc(), 0, "pool surplus should stay neutralized"); + assertEq(vault.lenderUsdcAvailable(), LENDER_USDC, "lender bucket should be reusable"); + + assertEq(cwusdc.balanceOf(USER), userCwusdcBefore - 90_156, "user only loses neutralized amount"); + assertEq(cwusdc.balanceOf(SURPLUS_RECEIVER), 90_156, "receiver gets neutralized cWUSDC"); + assertEq(xaut.balanceOf(USER), userXautBefore, "XAUt collateral should be returned"); + + assertEq(vault.totalVirtualLoops(), 3, "virtual loop counter"); + assertEq(vault.totalVirtualDebtUsdc(), 15_000_000, "virtual debt counter"); + assertEq(vault.totalVirtualCwusdcIn(), 15_976_569, "virtual cWUSDC in counter"); + assertEq(vault.totalNeutralizedCwusdc(), 90_156, "neutralized counter"); + assertTrue(vault.usedProofIds(bytes32("proof-1")), "proof id should be consumed"); + } + + function testVirtualProofCanSendOutputToRecipient() public { + uint256 userCwusdcBefore = cwusdc.balanceOf(USER); + uint256 userXautBefore = xaut.balanceOf(USER); + + vm.prank(USER); + vault.runVirtualProofTo(bytes32("proof-to"), LENDER_USDC, 3, OUTPUT_RECIPIENT); + + assertEq(vault.poolCwusdcReserve(), LIVE_POOL_RESERVE, "pool cWUSDC reserve should not drift"); + assertEq(vault.poolUsdcReserve(), LIVE_POOL_RESERVE, "pool USDC reserve should not drift"); + assertEq(cwusdc.balanceOf(USER), userCwusdcBefore - 15_976_569, "sender supplies gross cWUSDC"); + assertEq(cwusdc.balanceOf(OUTPUT_RECIPIENT), 15_886_413, "recipient gets cWUSDC output"); + assertEq(cwusdc.balanceOf(SURPLUS_RECEIVER), 90_156, "receiver gets neutralized cWUSDC"); + assertEq(xaut.balanceOf(USER), userXautBefore, "XAUt collateral should be returned"); + } + + function testVirtualProofExactOutToAnchorsAuditProofsAndSendsRounding() public { + uint256 userCwusdcBefore = cwusdc.balanceOf(USER); + uint256 userXautBefore = xaut.balanceOf(USER); + uint256 exactOutput = 15_886_000; + bytes32 proofId = bytes32("proof-audit"); + + vm.expectEmit(true, true, true, true, address(vault)); + emit VirtualProofAuditEvidence( + proofId, USER, OUTPUT_RECIPIENT, exactOutput, 413, ROUNDING_RECEIVER, ISO_HASH, AUDIT_HASH, PEG_HASH + ); + vm.prank(USER); + vault.runVirtualProofExactOutTo( + proofId, + LENDER_USDC, + 3, + OUTPUT_RECIPIENT, + exactOutput, + ROUNDING_RECEIVER, + ISO_HASH, + AUDIT_HASH, + PEG_HASH + ); + + assertEq(vault.poolCwusdcReserve(), LIVE_POOL_RESERVE, "pool cWUSDC reserve should not drift"); + assertEq(vault.poolUsdcReserve(), LIVE_POOL_RESERVE, "pool USDC reserve should not drift"); + assertEq(cwusdc.balanceOf(USER), userCwusdcBefore - 15_976_569, "sender supplies gross cWUSDC"); + assertEq(cwusdc.balanceOf(OUTPUT_RECIPIENT), exactOutput, "recipient gets exact cWUSDC"); + assertEq(cwusdc.balanceOf(ROUNDING_RECEIVER), 413, "rounding receiver gets output dust"); + assertEq(cwusdc.balanceOf(SURPLUS_RECEIVER), 90_156, "receiver gets neutralized cWUSDC"); + assertEq(xaut.balanceOf(USER), userXautBefore, "XAUt collateral should be returned"); + } + + function testVirtualLoopCountScalesTotalsButNotSequentialCollateral() public view { + uint256 loops = 938_874_924; + ( + uint256 collateralXaut, + uint256 cwusdcInPerLoop,, + uint256 cwusdcLossPerLoop, + uint256 totalCwusdcInAmount,, + uint256 totalNeutralizedCwusdcAmount + ) = vault.previewVirtualProof(LENDER_USDC, loops); + + assertEq(collateralXaut, 1_937, "sequential virtual batch reuses one loop of XAUt collateral"); + assertEq(cwusdcInPerLoop, 5_325_523, "per-loop input stays constant"); + assertEq(cwusdcLossPerLoop, 30_052, "per-loop neutralization stays constant"); + assertEq(totalCwusdcInAmount, 5_000_000_001_885_252, "virtual batch covers 5B cWUSDC gross"); + assertEq(totalNeutralizedCwusdcAmount, 28_215_069_216_048, "5B batch neutralized amount"); + } + + function testPreviewRevertsWhenPoolIsNotMaintained() public { + DBISEngineXVirtualBatchVault unevenVault = new DBISEngineXVirtualBatchVault( + address(cwusdc), + address(usdc), + address(xaut), + address(this), + SURPLUS_RECEIVER, + XAUT_USD_PRICE6, + LTV_BPS, + MAX_ROUND_TRIP_LOSS_BPS + ); + cwusdc.mint(address(this), LIVE_POOL_RESERVE + 1); + usdc.mint(address(this), LIVE_POOL_RESERVE + LENDER_USDC); + cwusdc.approve(address(unevenVault), type(uint256).max); + usdc.approve(address(unevenVault), type(uint256).max); + unevenVault.seedPool(LIVE_POOL_RESERVE + 1, LIVE_POOL_RESERVE); + unevenVault.fundLender(LENDER_USDC); + + vm.expectRevert(bytes("pool not maintained")); + unevenVault.previewVirtualProof(LENDER_USDC, 1); + } + + function testRunVirtualProofRejectsDuplicateProofIds() public { + vm.prank(USER); + vault.runVirtualProof(bytes32("proof-1"), LENDER_USDC, 1); + + vm.expectRevert(bytes("proof used")); + vm.prank(USER); + vault.runVirtualProof(bytes32("proof-1"), LENDER_USDC, 1); + } + + function testRunVirtualProofToRejectsZeroRecipient() public { + vm.expectRevert(bytes("zero output")); + vm.prank(USER); + vault.runVirtualProofTo(bytes32("proof-zero"), LENDER_USDC, 1, address(0)); + } + + function testRunVirtualProofExactOutToRejectsMissingAuditHashes() public { + vm.expectRevert(bytes("zero iso hash")); + vm.prank(USER); + vault.runVirtualProofExactOutTo( + bytes32("proof-no-iso"), + LENDER_USDC, + 1, + OUTPUT_RECIPIENT, + 1, + ROUNDING_RECEIVER, + bytes32(0), + AUDIT_HASH, + PEG_HASH + ); + } + + function testRunVirtualProofExactOutToRejectsOutputAbovePreview() public { + vm.expectRevert(bytes("exact output too high")); + vm.prank(USER); + vault.runVirtualProofExactOutTo( + bytes32("proof-too-high"), + LENDER_USDC, + 1, + OUTPUT_RECIPIENT, + 5_295_472, + ROUNDING_RECEIVER, + ISO_HASH, + AUDIT_HASH, + PEG_HASH + ); + } +}