Files
smom-dbis-138/contracts/flash/QuotePushFlashWorkflowBorrower.sol
defiQUG 76aa419320 feat: bridges, PMM, flash workflow, token-aggregation, and deployment docs
- CCIP/trustless bridge contracts, GRU tokens, DEX/PMM tests, reserve vault.
- Token-aggregation service routes, planner, chain config, relay env templates.
- Config snapshots and multi-chain deployment markdown updates.
- gitignore services/btc-intake/dist/ (tsc output); do not track dist.

Run forge build && forge test before deploy (large solc graph).

Made-with: Cursor
2026-04-07 23:40:52 -07:00

102 lines
3.7 KiB
Solidity

// 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;
}
}