// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC3156FlashBorrower} from "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol"; import {SimpleERC3156FlashVault} from "../../contracts/flash/SimpleERC3156FlashVault.sol"; import { QuotePushFlashWorkflowBorrower, IExternalUnwinder } from "../../contracts/flash/QuotePushFlashWorkflowBorrower.sol"; contract ForkMockExternalUnwinder is IExternalUnwinder { IERC20 public immutable base; IERC20 public immutable quote; uint256 public immutable numerator; uint256 public immutable denominator; constructor(IERC20 base_, IERC20 quote_, uint256 numerator_, uint256 denominator_) { base = base_; quote = quote_; numerator = numerator_; denominator = denominator_; } function unwind(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, bytes calldata) external override returns (uint256 amountOut) { require(tokenIn == address(base), "base only"); require(tokenOut == address(quote), "quote only"); IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn); amountOut = amountIn * numerator / denominator; require(amountOut >= minAmountOut, "min unwind"); IERC20(address(quote)).transfer(msg.sender, amountOut); } } contract QuotePushFlashWorkflowBorrowerMainnetForkTest is Test { address constant DODO_PMM_INTEGRATION_MAINNET = 0xa9F284eD010f4F7d7F8F201742b49b9f58e29b84; address constant POOL_CWUSDC_USDC = 0x69776fc607e9edA8042e320e7e43f54d06c68f0E; address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; address constant CWUSDC = 0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a; bool public forkAvailable; SimpleERC3156FlashVault internal vault; QuotePushFlashWorkflowBorrower internal borrower; ForkMockExternalUnwinder internal unwinder; modifier skipIfNoFork() { if (!forkAvailable) { return; } _; } function setUp() public { string memory rpcUrl = vm.envOr("ETHEREUM_MAINNET_RPC", string("")); if (bytes(rpcUrl).length == 0) { forkAvailable = false; return; } try vm.createSelectFork(rpcUrl) { forkAvailable = true; } catch { forkAvailable = false; return; } vault = new SimpleERC3156FlashVault(address(this), 5); vault.setTokenSupported(USDC, true); borrower = new QuotePushFlashWorkflowBorrower(address(vault)); unwinder = new ForkMockExternalUnwinder(IERC20(CWUSDC), IERC20(USDC), 112, 100); // Seed the local lender and unwind venue with enough quote on the fork. deal(USDC, address(vault), 50_000_000_000); deal(USDC, address(unwinder), 50_000_000_000); } function testFork_quotePush_usesLiveMainnetPmmLegAndRepays() public skipIfNoFork { uint256 amount = 2_964_298; // live safe tranche from 120/120 under 500 bps cap uint256 fee = vault.flashFee(USDC, amount); QuotePushFlashWorkflowBorrower.QuotePushParams memory p = QuotePushFlashWorkflowBorrower.QuotePushParams({ integration: DODO_PMM_INTEGRATION_MAINNET, pool: POOL_CWUSDC_USDC, baseToken: CWUSDC, externalUnwinder: address(unwinder), minOutPmm: 2_800_000, minOutUnwind: amount + fee, unwindData: bytes("") }); uint256 vaultBefore = IERC20(USDC).balanceOf(address(vault)); uint256 poolBaseBefore = IERC20(CWUSDC).balanceOf(POOL_CWUSDC_USDC); uint256 poolQuoteBefore = IERC20(USDC).balanceOf(POOL_CWUSDC_USDC); vault.flashLoan(IERC3156FlashBorrower(address(borrower)), USDC, amount, abi.encode(p)); uint256 vaultAfter = IERC20(USDC).balanceOf(address(vault)); uint256 borrowerSurplus = IERC20(USDC).balanceOf(address(borrower)); uint256 poolBaseAfter = IERC20(CWUSDC).balanceOf(POOL_CWUSDC_USDC); uint256 poolQuoteAfter = IERC20(USDC).balanceOf(POOL_CWUSDC_USDC); assertEq(vaultAfter, vaultBefore + fee, "vault repaid plus fee"); assertGt(borrowerSurplus, 0, "borrower retains quote surplus"); assertEq(IERC20(CWUSDC).balanceOf(address(borrower)), 0, "all base unwound"); assertLt(poolBaseAfter, poolBaseBefore, "pool base decreased via quote push"); assertGt(poolQuoteAfter, poolQuoteBefore, "pool quote increased via quote push"); } }