From 12d3ea53b36fb8acf1c453ddd178fa4758b77ad5 Mon Sep 17 00:00:00 2001 From: pblivin0x Date: Sat, 8 Feb 2025 01:26:06 -0500 Subject: [PATCH 1/3] forge install: index-coop-smart-contracts v0.36.0 --- .gitmodules | 3 +++ lib/index-coop-smart-contracts | 1 + 2 files changed, 4 insertions(+) create mode 160000 lib/index-coop-smart-contracts diff --git a/.gitmodules b/.gitmodules index 5963803..b3aa01c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "lib/uniswapx"] path = lib/uniswapx url = https://github.com/uniswap/uniswapx +[submodule "lib/index-coop-smart-contracts"] + path = lib/index-coop-smart-contracts + url = https://github.com/indexcoop/index-coop-smart-contracts diff --git a/lib/index-coop-smart-contracts b/lib/index-coop-smart-contracts new file mode 160000 index 0000000..31b483a --- /dev/null +++ b/lib/index-coop-smart-contracts @@ -0,0 +1 @@ +Subproject commit 31b483a02831d6732599921c1dcf5f063bf06078 From e2dc36fb0899c28429c3ee86a2df2d617dc22448 Mon Sep 17 00:00:00 2001 From: pblivin0x Date: Sat, 8 Feb 2025 03:21:16 -0500 Subject: [PATCH 2/3] add FlashMintExecutor --- src/interfaces/IFlashMintDexV5.sol | 107 ++++++++++++ src/uniswapx/FlashMintExecutor.sol | 223 ++++++++++++++++++++++++++ test/uniswapx/FlashMintExecutor.t.sol | 121 ++++++++++++++ 3 files changed, 451 insertions(+) create mode 100644 src/interfaces/IFlashMintDexV5.sol create mode 100644 src/uniswapx/FlashMintExecutor.sol create mode 100644 test/uniswapx/FlashMintExecutor.t.sol diff --git a/src/interfaces/IFlashMintDexV5.sol b/src/interfaces/IFlashMintDexV5.sol new file mode 100644 index 0000000..0b2ad82 --- /dev/null +++ b/src/interfaces/IFlashMintDexV5.sol @@ -0,0 +1,107 @@ +pragma solidity ^0.8.0; + +import { ISetToken } from "./ISetToken.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +interface IFlashMintDexV5 { + /* ============ Structs ============ */ + + struct LeveragedTokenData { + address collateralAToken; + address collateralToken; + uint256 collateralAmount; + address debtToken; + uint256 debtAmount; + } + + struct SwapData { + address[] path; + uint24[] fees; + int24[] tickSpacing; + address pool; // For Curve swaps + bytes32[] poolIds; // For Balancer V2 multihop swaps + uint8 exchange; + } + + /* ============ Events ============ */ + + event FlashMint( + address indexed _recipient, // The recipient address of the issued SetTokens + ISetToken indexed _setToken, // The issued SetToken + address indexed _inputToken, // The address of the input asset(ERC20/ETH) used to issue the SetTokens + uint256 _amountInputToken, // The amount of input tokens used for issuance + uint256 _amountSetIssued // The amount of SetTokens received by the recipient + ); + + event FlashRedeem( + address indexed _recipient, // The recipient address which redeemed the SetTokens + ISetToken indexed _setToken, // The redeemed SetToken + address indexed _outputToken, // The address of output asset(ERC20/ETH) received by the recipient + uint256 _amountSetRedeemed, // The amount of SetTokens redeemed for output tokens + uint256 _amountOutputToken // The amount of output tokens received by the recipient + ); + + /* ============ Functions ============ */ + + function getLeveragedTokenData( + ISetToken _setToken, + uint256 _setAmount, + bool _isIssuance + ) external view returns (LeveragedTokenData memory); + + function approveToken(IERC20 _token) external; + + function getIssueExactSet( + address _setToken, + uint256 _setAmount, + uint256 _maxAmountInputToken, + SwapData memory _swapDataDebtForCollateral, + SwapData memory _swapDataInputToken + ) external returns (uint256); + + function getRedeemExactSet( + ISetToken _setToken, + uint256 _setAmount, + SwapData memory _swapDataCollateralForDebt, + SwapData memory _swapDataOutputToken + ) external returns (uint256); + + function redeemExactSetForETH( + ISetToken _setToken, + uint256 _setAmount, + uint256 _minAmountOutputToken, + SwapData memory _swapDataCollateralForDebt, + SwapData memory _swapDataOutputToken + ) external; + + function redeemExactSetForERC20( + ISetToken _setToken, + uint256 _setAmount, + address _outputToken, + uint256 _minAmountOutputToken, + SwapData memory _swapDataCollateralForDebt, + SwapData memory _swapDataOutputToken + ) external; + + function issueExactSetFromERC20( + ISetToken _setToken, + uint256 _setAmount, + address _inputToken, + uint256 _maxAmountInputToken, + SwapData memory _swapDataDebtForCollateral, + SwapData memory _swapDataInputToken + ) external; + + function issueExactSetFromETH( + ISetToken _setToken, + uint256 _setAmount, + SwapData memory _swapDataDebtForCollateral, + SwapData memory _swapDataInputToken + ) external payable; + + function onMorphoFlashLoan(uint256 assets, bytes calldata data) external; + + function approveTokens(IERC20[] memory _tokens) external; + + function approveSetToken(ISetToken _setToken) external; +} \ No newline at end of file diff --git a/src/uniswapx/FlashMintExecutor.sol b/src/uniswapx/FlashMintExecutor.sol new file mode 100644 index 0000000..8a61ea9 --- /dev/null +++ b/src/uniswapx/FlashMintExecutor.sol @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import {Owned} from "solmate/src/auth/Owned.sol"; +import {SafeTransferLib} from "solmate/src/utils/SafeTransferLib.sol"; +import {ERC20} from "solmate/src/tokens/ERC20.sol"; +import {WETH} from "solmate/src/tokens/WETH.sol"; + +import {IReactorCallback} from "uniswapx/src/interfaces/IReactorCallback.sol"; +import {IReactor} from "uniswapx/src/interfaces/IReactor.sol"; +import {ResolvedOrder, OutputToken, SignedOrder} from "uniswapx/src/base/ReactorStructs.sol"; + +import {IFlashMintDexV5} from "../interfaces/IFlashMintDexV5.sol"; +import {ISetToken} from "../interfaces/ISetToken.sol"; + +/// @title FlashMintExecutor +/// @notice A UniswapX executor that routes fills to a FlashMint contract. The owner can +/// register enabled flash–mint tokens (typically SetTokens) and for each its corresponding +/// FlashMint contract. Then in the reactorCallback, if issuance is requested, the contract +/// checks that the set token is enabled and uses its flash–mint contract; similarly for redemption. +contract FlashMintExecutor is IReactorCallback, Owned { + using SafeTransferLib for ERC20; + + /// @notice Constant used to represent native ETH. + address constant ETH = address(0xEeeeeEeeeeeEeeeeeeeEEEeeeeEeeeeeeeEEeE); + + event FlashMintTokenAdded(address indexed token, address indexed flashMintContract); + event FlashMintTokenRemoved(address indexed token); + + /// @notice Error when caller of reactorCallback is not the expected reactor. + error MsgSenderNotReactor(); + /// @notice Error when a native ETH transfer fails. + error NativeTransferFailed(); + + IReactor public immutable reactor; + WETH public immutable weth; + + // Mapping of enabled flash–mint tokens (typically SetTokens) to a boolean flag. + mapping(address => bool) public flashMintEnabled; + // Mapping from an enabled flash–mint token (SetToken) to its flash–mint contract. + mapping(address => IFlashMintDexV5) public flashMintForToken; + + modifier onlyReactor() { + if (msg.sender != address(reactor)) { + revert MsgSenderNotReactor(); + } + _; + } + + constructor( + IReactor _reactor, + address _weth, + address _owner + ) Owned(_owner) { + require(_weth != address(0), "Invalid WETH address"); + reactor = _reactor; + weth = WETH(payable(_weth)); + } + + // ───────────────────────────────────────────────────────────────────────────── + // Owner Functions: Flash–Mint Token Management + // ───────────────────────────────────────────────────────────────────────────── + + /// @notice Enables a flash–mint token and registers its flash–mint contract. + /// @param token The flash–mint token (e.g. a SetToken) to enable. + /// @param flashMintContract The FlashMint contract to use for this token. + function addFlashMintToken(address token, IFlashMintDexV5 flashMintContract) external onlyOwner { + require(token != address(0), "Invalid token"); + require(address(flashMintContract) != address(0), "Invalid flashMint contract"); + flashMintEnabled[token] = true; + flashMintForToken[token] = flashMintContract; + emit FlashMintTokenAdded(token, address(flashMintContract)); + } + + /// @notice Removes a flash–mint token from the enabled list. + /// @param token The flash–mint token to remove. + function removeFlashMintToken(address token) external onlyOwner { + flashMintEnabled[token] = false; + delete flashMintForToken[token]; + emit FlashMintTokenRemoved(token); + } + + // ───────────────────────────────────────────────────────────────────────────── + // Executor Functions: Forwarding orders to the reactor + // ───────────────────────────────────────────────────────────────────────────── + + /// @notice Forwards a single order to the reactor with callback. + function execute(SignedOrder calldata order, bytes calldata callbackData) external { + reactor.executeWithCallback(order, callbackData); + } + + /// @notice Forwards a batch of orders to the reactor with callback. + function executeBatch(SignedOrder[] calldata orders, bytes calldata callbackData) external { + reactor.executeBatchWithCallback(orders, callbackData); + } + + // ───────────────────────────────────────────────────────────────────────────── + // Reactor Callback Implementation + // ───────────────────────────────────────────────────────────────────────────── + + /** + * @notice UniswapX reactor callback that fills orders via the appropriate FlashMint contract. + * + * The callbackData is ABI–decoded as follows: + * - address[] tokensToApproveForFlashMint: tokens to approve for the flash–mint contract. + * - address[] tokensToApproveForReactor: tokens to approve for the reactor. + * - ISetToken setToken: the SetToken to be issued or redeemed. + * - uint256 setAmount: the SetToken amount to issue/redeem. + * - address inputOutputToken: the ERC20 token (or ETH) used as input for issuance, or desired output for redemption. + * - uint256 inputOutputTokenAmount: the max input (for issuance) or min output (for redemption) amount. + * - IFlashMintDexV5.SwapData swapDataCollateral: swap data for collateral. + * - IFlashMintDexV5.SwapData swapDataInputOutputToken: swap data for the input/output token. + * - bool isIssuance: true for issuance, false for redemption. + */ + function reactorCallback( + ResolvedOrder[] calldata, + bytes calldata callbackData + ) external override onlyReactor { + ( + address[] memory tokensToApproveForFlashMint, + address[] memory tokensToApproveForReactor, + ISetToken setToken, + uint256 setAmount, + address inputOutputToken, + uint256 inputOutputTokenAmount, + IFlashMintDexV5.SwapData memory swapDataCollateral, + IFlashMintDexV5.SwapData memory swapDataInputOutputToken, + bool isIssuance + ) = abi.decode( + callbackData, + ( + address[], + address[], + ISetToken, + uint256, + address, + uint256, + IFlashMintDexV5.SwapData, + IFlashMintDexV5.SwapData, + bool + ) + ); + + // Approve tokens to the FlashMint contract and the reactor. + IFlashMintDexV5 flashMintContract = getFlashMintContract(setToken); + unchecked { + for (uint256 i = 0; i < tokensToApproveForFlashMint.length; i++) { + ERC20(tokensToApproveForFlashMint[i]).safeApprove(address(flashMintContract), type(uint256).max); + } + for (uint256 i = 0; i < tokensToApproveForReactor.length; i++) { + ERC20(tokensToApproveForReactor[i]).safeApprove(address(reactor), type(uint256).max); + } + } + + if (isIssuance) { + // For issuance, the flash–mint token to issue is setToken. + require(flashMintEnabled[address(setToken)], "FlashMint not enabled for issuance"); + if (inputOutputToken == ETH) { + flashMintContract.issueExactSetFromETH{value: inputOutputTokenAmount}( + setToken, + setAmount, + swapDataCollateral, + swapDataInputOutputToken + ); + } else { + flashMintContract.issueExactSetFromERC20( + setToken, + setAmount, + inputOutputToken, + inputOutputTokenAmount, + swapDataCollateral, + swapDataInputOutputToken + ); + } + } else { + // For redemption, the flash–mint token being redeemed is setToken. + require(flashMintEnabled[address(setToken)], "FlashMint not enabled for redemption"); + if (inputOutputToken == ETH) { + flashMintContract.redeemExactSetForETH( + setToken, + setAmount, + inputOutputTokenAmount, + swapDataCollateral, + swapDataInputOutputToken + ); + } else { + flashMintContract.redeemExactSetForERC20( + setToken, + setAmount, + inputOutputToken, + inputOutputTokenAmount, + swapDataCollateral, + swapDataInputOutputToken + ); + } + } + + // Refund any excess native ETH to the reactor. + if (address(this).balance > 0) { + transferNative(address(reactor), address(this).balance); + } + } + + /// @dev Returns the flash–mint contract associated with a given set token. + function getFlashMintContract(ISetToken setToken) internal view returns (IFlashMintDexV5) { + IFlashMintDexV5 flashMintContract = flashMintForToken[address(setToken)]; + require(address(flashMintContract) != address(0), "No flashMint contract for this token"); + return flashMintContract; + } + + /** + * @notice Transfers native ETH to the specified recipient. + * @param recipient The address to receive the ETH. + * @param amount The amount of ETH to transfer. + */ + function transferNative(address recipient, uint256 amount) internal { + (bool success, ) = recipient.call{value: amount}(""); + if (!success) revert NativeTransferFailed(); + } + + /// @notice Receive function to allow contract to accept ETH. + receive() external payable {} +} diff --git a/test/uniswapx/FlashMintExecutor.t.sol b/test/uniswapx/FlashMintExecutor.t.sol new file mode 100644 index 0000000..8d222aa --- /dev/null +++ b/test/uniswapx/FlashMintExecutor.t.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import "forge-std/Vm.sol"; +import "../../src/uniswapx/FlashMintExecutor.sol"; +import {IReactor} from "uniswapx/src/interfaces/IReactor.sol"; +import "../../src/interfaces/IFlashMintDexV5.sol"; + +contract FlashMintExecutorTest is Test { + FlashMintExecutor public flashMintExecutor; + + address public owner; + address public weth; + address public flashMint; + + address public mockReactor; + address public mockSetToken; + address public mockFlashMint; + address public nonOwner; + event FlashMintTokenAdded(address indexed token, address indexed flashMintContract); + event FlashMintTokenRemoved(address indexed token); + + function setUp() public { + owner = msg.sender; + weth = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + mockReactor = address(0x1); + mockSetToken = address(0x2); + mockFlashMint = address(0x3); + nonOwner = address(0x4); + flashMintExecutor = new FlashMintExecutor( + IReactor(mockReactor), + weth, + owner + ); + } + + function testConstructor() public { + assertEq(address(flashMintExecutor.reactor()), mockReactor, "Incorrect reactor address"); + assertEq(address(flashMintExecutor.weth()), weth, "Incorrect WETH address"); + assertEq(flashMintExecutor.owner(), owner, "Incorrect owner address"); + } + + function testAddFlashMintToken() public { + vm.expectEmit(true, false, false, true); + emit FlashMintTokenAdded(mockSetToken, mockFlashMint); + + vm.prank(owner); + flashMintExecutor.addFlashMintToken(mockSetToken, IFlashMintDexV5(mockFlashMint)); + + assertTrue(flashMintExecutor.flashMintEnabled(mockSetToken), "Token should be enabled"); + assertEq( + address(flashMintExecutor.flashMintForToken(mockSetToken)), + mockFlashMint, + "Incorrect flash mint contract" + ); + } + + function testCannotAddFlashMintTokenWithZeroAddresses() public { + vm.expectRevert("Invalid token"); + vm.prank(owner); + flashMintExecutor.addFlashMintToken(address(0), IFlashMintDexV5(mockFlashMint)); + + vm.expectRevert("Invalid flashMint contract"); + vm.prank(owner); + flashMintExecutor.addFlashMintToken(mockSetToken, IFlashMintDexV5(address(0))); + } + + function testCannotAddFlashMintTokenIfNotOwner() public { + vm.prank(nonOwner); + vm.expectRevert("UNAUTHORIZED"); + flashMintExecutor.addFlashMintToken(mockSetToken, IFlashMintDexV5(mockFlashMint)); + } + + function testRemoveFlashMintToken() public { + vm.prank(owner); + flashMintExecutor.addFlashMintToken(mockSetToken, IFlashMintDexV5(mockFlashMint)); + + vm.expectEmit(true, false, false, true); + emit FlashMintTokenRemoved(mockSetToken); + + vm.prank(owner); + flashMintExecutor.removeFlashMintToken(mockSetToken); + + assertFalse(flashMintExecutor.flashMintEnabled(mockSetToken), "Token should be disabled"); + assertEq( + address(flashMintExecutor.flashMintForToken(mockSetToken)), + address(0), + "Flash mint contract should be removed" + ); + } + + function testCannotRemoveFlashMintTokenIfNotOwner() public { + vm.prank(owner); + flashMintExecutor.addFlashMintToken(mockSetToken, IFlashMintDexV5(mockFlashMint)); + + vm.prank(nonOwner); + vm.expectRevert("UNAUTHORIZED"); + flashMintExecutor.removeFlashMintToken(mockSetToken); + } + + function testUpdateFlashMintToken() public { + address newMockFlashMint = address(0x99); + + vm.prank(owner); + flashMintExecutor.addFlashMintToken(mockSetToken, IFlashMintDexV5(mockFlashMint)); + + vm.expectEmit(true, true, false, false); + emit FlashMintTokenAdded(mockSetToken, newMockFlashMint); + + vm.prank(owner); + flashMintExecutor.addFlashMintToken(mockSetToken, IFlashMintDexV5(newMockFlashMint)); + + assertTrue(flashMintExecutor.flashMintEnabled(mockSetToken), "Token should still be enabled"); + assertEq( + address(flashMintExecutor.flashMintForToken(mockSetToken)), + newMockFlashMint, + "Flash mint contract should be updated" + ); + } +} From 5ad1375c5ff77d3e6037cee813c1c145ee1f7a5b Mon Sep 17 00:00:00 2001 From: pblivin0x Date: Tue, 11 Feb 2025 23:21:37 -0500 Subject: [PATCH 3/3] documentation cleanups, setup tests --- src/uniswapx/FlashMintExecutor.sol | 158 ++++++++++---------------- test/uniswapx/FlashMintExecutor.t.sol | 13 +-- 2 files changed, 66 insertions(+), 105 deletions(-) diff --git a/src/uniswapx/FlashMintExecutor.sol b/src/uniswapx/FlashMintExecutor.sol index 8a61ea9..78ea454 100644 --- a/src/uniswapx/FlashMintExecutor.sol +++ b/src/uniswapx/FlashMintExecutor.sol @@ -4,40 +4,53 @@ pragma solidity ^0.8.0; import {Owned} from "solmate/src/auth/Owned.sol"; import {SafeTransferLib} from "solmate/src/utils/SafeTransferLib.sol"; import {ERC20} from "solmate/src/tokens/ERC20.sol"; -import {WETH} from "solmate/src/tokens/WETH.sol"; + +import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import {IReactorCallback} from "uniswapx/src/interfaces/IReactorCallback.sol"; import {IReactor} from "uniswapx/src/interfaces/IReactor.sol"; -import {ResolvedOrder, OutputToken, SignedOrder} from "uniswapx/src/base/ReactorStructs.sol"; +import {ResolvedOrder, SignedOrder} from "uniswapx/src/base/ReactorStructs.sol"; import {IFlashMintDexV5} from "../interfaces/IFlashMintDexV5.sol"; import {ISetToken} from "../interfaces/ISetToken.sol"; -/// @title FlashMintExecutor -/// @notice A UniswapX executor that routes fills to a FlashMint contract. The owner can -/// register enabled flash–mint tokens (typically SetTokens) and for each its corresponding -/// FlashMint contract. Then in the reactorCallback, if issuance is requested, the contract -/// checks that the set token is enabled and uses its flash–mint contract; similarly for redemption. +/** + * @title FlashMintExecutor + * @notice A UniswapX executor that routes fills to a FlashMint contract using ERC20 tokens only. + * + * The owner can register enabled FlashMint tokens (typically SetTokens) and for each its + * corresponding FlashMint contract. In the reactorCallback, if issuance (or redemption) + * is requested, the contract verifies that the SetToken is enabled and then calls the appropriate + * FlashMint function. + * + * The callbackData passed into reactorCallback must be ABI-encoded as: + * + * abi.encode( + * address[] tokensToApproveForFlashMint, // Tokens to approve for the FlashMint contract + * address[] tokensToApproveForReactor, // Tokens to approve for the reactor + * ISetToken setToken, // The SetToken to issue or redeem + * uint256 setAmount, // The SetToken amount to issue/redeem + * address inputOutputToken, // The ERC20 token used as input (issuance) or desired output (redemption) + * uint256 inputOutputTokenAmount, // The max input (issuance) or min output (redemption) amount + * IFlashMintDexV5.SwapData swapDataCollateral, // Swap data for collateral + * IFlashMintDexV5.SwapData swapDataInputOutputToken, // Swap data for the input/output token + * bool isIssuance // True for issuance, false for redemption + * ) + */ contract FlashMintExecutor is IReactorCallback, Owned { using SafeTransferLib for ERC20; - /// @notice Constant used to represent native ETH. - address constant ETH = address(0xEeeeeEeeeeeEeeeeeeeEEEeeeeEeeeeeeeEEeE); - event FlashMintTokenAdded(address indexed token, address indexed flashMintContract); event FlashMintTokenRemoved(address indexed token); - /// @notice Error when caller of reactorCallback is not the expected reactor. + /// @notice Reverts if reactorCallback is called by an address other than the expected reactor. error MsgSenderNotReactor(); - /// @notice Error when a native ETH transfer fails. - error NativeTransferFailed(); IReactor public immutable reactor; - WETH public immutable weth; - // Mapping of enabled flash–mint tokens (typically SetTokens) to a boolean flag. + // Mapping of enabled FlashMint tokens (typically SetTokens) to a boolean flag. mapping(address => bool) public flashMintEnabled; - // Mapping from an enabled flash–mint token (SetToken) to its flash–mint contract. + // Mapping from an enabled FlashMint token (SetToken) to its FlashMint contract. mapping(address => IFlashMintDexV5) public flashMintForToken; modifier onlyReactor() { @@ -49,40 +62,36 @@ contract FlashMintExecutor is IReactorCallback, Owned { constructor( IReactor _reactor, - address _weth, address _owner ) Owned(_owner) { - require(_weth != address(0), "Invalid WETH address"); reactor = _reactor; - weth = WETH(payable(_weth)); } - // ───────────────────────────────────────────────────────────────────────────── - // Owner Functions: Flash–Mint Token Management - // ───────────────────────────────────────────────────────────────────────────── - - /// @notice Enables a flash–mint token and registers its flash–mint contract. - /// @param token The flash–mint token (e.g. a SetToken) to enable. + /// @notice Enables a token and registers its FlashMint contract. + /// @param token The token (e.g. a SetToken) to enable. /// @param flashMintContract The FlashMint contract to use for this token. function addFlashMintToken(address token, IFlashMintDexV5 flashMintContract) external onlyOwner { require(token != address(0), "Invalid token"); - require(address(flashMintContract) != address(0), "Invalid flashMint contract"); + require(address(flashMintContract) != address(0), "Invalid FlashMint contract"); flashMintEnabled[token] = true; flashMintForToken[token] = flashMintContract; emit FlashMintTokenAdded(token, address(flashMintContract)); } - /// @notice Removes a flash–mint token from the enabled list. - /// @param token The flash–mint token to remove. + /// @notice Removes a token from the enabled list. + /// @param token The token to remove. function removeFlashMintToken(address token) external onlyOwner { flashMintEnabled[token] = false; delete flashMintForToken[token]; emit FlashMintTokenRemoved(token); } - // ───────────────────────────────────────────────────────────────────────────── - // Executor Functions: Forwarding orders to the reactor - // ───────────────────────────────────────────────────────────────────────────── + /// @notice Withdraws a token from the contract. + /// @param _token The token to withdraw. + /// @param _amount The amount to withdraw. + function withdrawToken(IERC20 _token, uint256 _amount) external onlyOwner { + _token.transfer(msg.sender, _amount); + } /// @notice Forwards a single order to the reactor with callback. function execute(SignedOrder calldata order, bytes calldata callbackData) external { @@ -94,20 +103,16 @@ contract FlashMintExecutor is IReactorCallback, Owned { reactor.executeBatchWithCallback(orders, callbackData); } - // ───────────────────────────────────────────────────────────────────────────── - // Reactor Callback Implementation - // ───────────────────────────────────────────────────────────────────────────── - /** * @notice UniswapX reactor callback that fills orders via the appropriate FlashMint contract. * - * The callbackData is ABI–decoded as follows: - * - address[] tokensToApproveForFlashMint: tokens to approve for the flash–mint contract. + * The callbackData is ABI-decoded as follows: + * - address[] tokensToApproveForFlashMint: tokens to approve for the FlashMint contract. * - address[] tokensToApproveForReactor: tokens to approve for the reactor. - * - ISetToken setToken: the SetToken to be issued or redeemed. + * - ISetToken setToken: the SetToken to issue or redeem. * - uint256 setAmount: the SetToken amount to issue/redeem. - * - address inputOutputToken: the ERC20 token (or ETH) used as input for issuance, or desired output for redemption. - * - uint256 inputOutputTokenAmount: the max input (for issuance) or min output (for redemption) amount. + * - address inputOutputToken: the ERC20 token used as input (issuance) or desired output (redemption). + * - uint256 inputOutputTokenAmount: the max input (issuance) or min output (redemption) amount. * - IFlashMintDexV5.SwapData swapDataCollateral: swap data for collateral. * - IFlashMintDexV5.SwapData swapDataInputOutputToken: swap data for the input/output token. * - bool isIssuance: true for issuance, false for redemption. @@ -141,7 +146,6 @@ contract FlashMintExecutor is IReactorCallback, Owned { ) ); - // Approve tokens to the FlashMint contract and the reactor. IFlashMintDexV5 flashMintContract = getFlashMintContract(setToken); unchecked { for (uint256 i = 0; i < tokensToApproveForFlashMint.length; i++) { @@ -153,71 +157,31 @@ contract FlashMintExecutor is IReactorCallback, Owned { } if (isIssuance) { - // For issuance, the flash–mint token to issue is setToken. require(flashMintEnabled[address(setToken)], "FlashMint not enabled for issuance"); - if (inputOutputToken == ETH) { - flashMintContract.issueExactSetFromETH{value: inputOutputTokenAmount}( - setToken, - setAmount, - swapDataCollateral, - swapDataInputOutputToken - ); - } else { - flashMintContract.issueExactSetFromERC20( - setToken, - setAmount, - inputOutputToken, - inputOutputTokenAmount, - swapDataCollateral, - swapDataInputOutputToken - ); - } + flashMintContract.issueExactSetFromERC20( + setToken, + setAmount, + inputOutputToken, + inputOutputTokenAmount, + swapDataCollateral, + swapDataInputOutputToken + ); } else { - // For redemption, the flash–mint token being redeemed is setToken. require(flashMintEnabled[address(setToken)], "FlashMint not enabled for redemption"); - if (inputOutputToken == ETH) { - flashMintContract.redeemExactSetForETH( - setToken, - setAmount, - inputOutputTokenAmount, - swapDataCollateral, - swapDataInputOutputToken - ); - } else { - flashMintContract.redeemExactSetForERC20( - setToken, - setAmount, - inputOutputToken, - inputOutputTokenAmount, - swapDataCollateral, - swapDataInputOutputToken - ); - } - } - - // Refund any excess native ETH to the reactor. - if (address(this).balance > 0) { - transferNative(address(reactor), address(this).balance); + flashMintContract.redeemExactSetForERC20( + setToken, + setAmount, + inputOutputToken, + inputOutputTokenAmount, + swapDataCollateral, + swapDataInputOutputToken + ); } } - /// @dev Returns the flash–mint contract associated with a given set token. function getFlashMintContract(ISetToken setToken) internal view returns (IFlashMintDexV5) { IFlashMintDexV5 flashMintContract = flashMintForToken[address(setToken)]; require(address(flashMintContract) != address(0), "No flashMint contract for this token"); return flashMintContract; } - - /** - * @notice Transfers native ETH to the specified recipient. - * @param recipient The address to receive the ETH. - * @param amount The amount of ETH to transfer. - */ - function transferNative(address recipient, uint256 amount) internal { - (bool success, ) = recipient.call{value: amount}(""); - if (!success) revert NativeTransferFailed(); - } - - /// @notice Receive function to allow contract to accept ETH. - receive() external payable {} } diff --git a/test/uniswapx/FlashMintExecutor.t.sol b/test/uniswapx/FlashMintExecutor.t.sol index 8d222aa..2d20d5c 100644 --- a/test/uniswapx/FlashMintExecutor.t.sol +++ b/test/uniswapx/FlashMintExecutor.t.sol @@ -8,36 +8,33 @@ import {IReactor} from "uniswapx/src/interfaces/IReactor.sol"; import "../../src/interfaces/IFlashMintDexV5.sol"; contract FlashMintExecutorTest is Test { - FlashMintExecutor public flashMintExecutor; + FlashMintExecutor flashMintExecutor; address public owner; - address public weth; - address public flashMint; - address public mockReactor; address public mockSetToken; address public mockFlashMint; address public nonOwner; + event FlashMintTokenAdded(address indexed token, address indexed flashMintContract); event FlashMintTokenRemoved(address indexed token); function setUp() public { owner = msg.sender; - weth = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + mockReactor = address(0x1); mockSetToken = address(0x2); mockFlashMint = address(0x3); nonOwner = address(0x4); + flashMintExecutor = new FlashMintExecutor( IReactor(mockReactor), - weth, owner ); } function testConstructor() public { assertEq(address(flashMintExecutor.reactor()), mockReactor, "Incorrect reactor address"); - assertEq(address(flashMintExecutor.weth()), weth, "Incorrect WETH address"); assertEq(flashMintExecutor.owner(), owner, "Incorrect owner address"); } @@ -61,7 +58,7 @@ contract FlashMintExecutorTest is Test { vm.prank(owner); flashMintExecutor.addFlashMintToken(address(0), IFlashMintDexV5(mockFlashMint)); - vm.expectRevert("Invalid flashMint contract"); + vm.expectRevert("Invalid FlashMint contract"); vm.prank(owner); flashMintExecutor.addFlashMintToken(mockSetToken, IFlashMintDexV5(address(0))); }