// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {IERC3156FlashBorrower} from "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; /// @dev Matches `DODOPMMIntegration.swapExactIn` surface (any registered pool). interface IDODOQuotePushSwapExactIn { function swapExactIn(address pool, address tokenIn, uint256 amountIn, uint256 minAmountOut) external returns (uint256 amountOut); } /// @dev Minimal external unwind interface for converting PMM base back into flash-borrowed quote. interface IExternalUnwinder { function unwind(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, bytes calldata data) external returns (uint256 amountOut); } /** * @title QuotePushFlashWorkflowBorrower * @notice ERC-3156 borrower for a quote-push loop: * flash `quoteToken` -> buy `baseToken` from a DODO-style PMM -> unwind externally back into `quoteToken` * -> repay `amount + fee`, leaving any quote surplus on this contract. * @dev `data` must be `abi.encode(QuotePushParams)`. The caller is responsible for choosing trusted integrations, * setting conservative minimums, and sweeping any retained surplus from this contract after execution. */ contract QuotePushFlashWorkflowBorrower is IERC3156FlashBorrower { using SafeERC20 for IERC20; bytes32 private constant _RETURN_VALUE = keccak256("ERC3156FlashBorrower.onFlashLoan"); address public immutable trustedLender; struct QuotePushParams { address integration; address pool; address baseToken; address externalUnwinder; uint256 minOutPmm; uint256 minOutUnwind; bytes unwindData; } error UntrustedLender(); error BadParams(); error InsufficientToRepay(); event QuotePushExecuted( address indexed quoteToken, address indexed baseToken, uint256 borrowedAmount, uint256 fee, uint256 baseOut, uint256 unwindOut, uint256 surplus ); constructor(address trustedLender_) { trustedLender = trustedLender_; } function onFlashLoan( address, address quoteToken, uint256 amount, uint256 fee, bytes calldata data ) external override returns (bytes32) { if (msg.sender != trustedLender) revert UntrustedLender(); QuotePushParams memory p = abi.decode(data, (QuotePushParams)); if ( p.integration == address(0) || p.pool == address(0) || p.baseToken == address(0) || p.externalUnwinder == address(0) ) revert BadParams(); if (p.baseToken == quoteToken) revert BadParams(); IERC20 borrowed = IERC20(quoteToken); IERC20 base = IERC20(p.baseToken); borrowed.forceApprove(p.integration, amount); uint256 baseOut = IDODOQuotePushSwapExactIn(p.integration).swapExactIn(p.pool, quoteToken, amount, p.minOutPmm); uint256 baseBal = base.balanceOf(address(this)); base.forceApprove(p.externalUnwinder, baseBal); uint256 unwindOut = IExternalUnwinder(p.externalUnwinder).unwind(p.baseToken, quoteToken, baseBal, p.minOutUnwind, p.unwindData); uint256 need = amount + fee; uint256 quoteBal = borrowed.balanceOf(address(this)); if (quoteBal < need) revert InsufficientToRepay(); uint256 surplus = quoteBal - need; borrowed.safeTransfer(msg.sender, need); emit QuotePushExecuted(quoteToken, p.baseToken, amount, fee, baseOut, unwindOut, surplus); return _RETURN_VALUE; } }