From 2954f97ad4ec66e5cdb5fa2a7294948100792dda Mon Sep 17 00:00:00 2001 From: Zach Yang Date: Wed, 18 Dec 2024 13:01:45 -0800 Subject: [PATCH] example multifiller executor (#272) * multifiller executor * add reactorChanged * make reactor public * add deploy script * forge fmt * fix names * add addr encoding comment --- script/DeployMultiFillerExecutor.s.sol | 30 ++++ .../MultiFillerSwapRouter02Executor.sol | 138 ++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 script/DeployMultiFillerExecutor.s.sol create mode 100644 src/sample-executors/MultiFillerSwapRouter02Executor.sol diff --git a/script/DeployMultiFillerExecutor.s.sol b/script/DeployMultiFillerExecutor.s.sol new file mode 100644 index 00000000..466fb274 --- /dev/null +++ b/script/DeployMultiFillerExecutor.s.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.13; + +import "forge-std/console2.sol"; +import "forge-std/Script.sol"; +import {MultiFillerSwapRouter02Executor} from "../src/sample-executors/MultiFillerSwapRouter02Executor.sol"; +import {ISwapRouter02} from "../src/external/ISwapRouter02.sol"; +import {IReactor} from "../src/interfaces/IReactor.sol"; + +contract DeployMultiFillerExecutor is Script { + function setUp() public {} + + function run() public returns (MultiFillerSwapRouter02Executor executor) { + uint256 privateKey = vm.envUint("FOUNDRY_PRIVATE_KEY"); + IReactor reactor = IReactor(vm.envAddress("FOUNDRY_SWAPROUTER02EXECUTOR_DEPLOY_REACTOR")); + // can encode with cast abi-encode "foo(address[])" "[addr1, addr2, ...]" + bytes memory encodedAddresses = vm.envBytes("FOUNDRY_MULTIFILLER_ADDRESSES_ENCODED"); + address owner = vm.envAddress("FOUNDRY_SWAPROUTER02EXECUTOR_DEPLOY_OWNER"); + ISwapRouter02 swapRouter02 = ISwapRouter02(vm.envAddress("FOUNDRY_SWAPROUTER02EXECUTOR_DEPLOY_SWAPROUTER02")); + + address[] memory decodedAddresses = abi.decode(encodedAddresses, (address[])); + + vm.startBroadcast(privateKey); + executor = new MultiFillerSwapRouter02Executor{salt: 0x00}(decodedAddresses, reactor, owner, swapRouter02); + vm.stopBroadcast(); + + console2.log("SwapRouter02Executor", address(executor)); + console2.log("owner", executor.owner()); + } +} diff --git a/src/sample-executors/MultiFillerSwapRouter02Executor.sol b/src/sample-executors/MultiFillerSwapRouter02Executor.sol new file mode 100644 index 00000000..5fa0b284 --- /dev/null +++ b/src/sample-executors/MultiFillerSwapRouter02Executor.sol @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +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 "../interfaces/IReactorCallback.sol"; +import {IReactor} from "../interfaces/IReactor.sol"; +import {CurrencyLibrary} from "../lib/CurrencyLibrary.sol"; +import {ResolvedOrder, SignedOrder} from "../base/ReactorStructs.sol"; +import {ISwapRouter02} from "../external/ISwapRouter02.sol"; + +/// @notice A fill contract that uses SwapRouter02 to execute trades +contract MultiFillerSwapRouter02Executor is IReactorCallback, Owned { + using SafeTransferLib for ERC20; + using CurrencyLibrary for address; + + event ReactorChanged(address newReactor, address oldReactor); + + /// @notice thrown if reactorCallback is called with a non-whitelisted filler + error CallerNotWhitelisted(); + /// @notice thrown if reactorCallback is called by an address other than the reactor + error MsgSenderNotReactor(); + + ISwapRouter02 private immutable swapRouter02; + mapping(address => bool) whitelistedCallers; + IReactor public reactor; + WETH private immutable weth; + + modifier onlyWhitelistedCaller() { + if (whitelistedCallers[msg.sender] == false) { + revert CallerNotWhitelisted(); + } + _; + } + + modifier onlyReactor() { + if (msg.sender != address(reactor)) { + revert MsgSenderNotReactor(); + } + _; + } + + constructor(address[] memory _whitelistedCallers, IReactor _reactor, address _owner, ISwapRouter02 _swapRouter02) + Owned(_owner) + { + for (uint256 i = 0; i < _whitelistedCallers.length; i++) { + whitelistedCallers[_whitelistedCallers[i]] = true; + } + reactor = _reactor; + swapRouter02 = _swapRouter02; + weth = WETH(payable(_swapRouter02.WETH9())); + } + + /// @notice assume that we already have all output tokens + function execute(SignedOrder calldata order, bytes calldata callbackData) external onlyWhitelistedCaller { + reactor.executeWithCallback(order, callbackData); + } + + /// @notice assume that we already have all output tokens + function executeBatch(SignedOrder[] calldata orders, bytes calldata callbackData) external onlyWhitelistedCaller { + reactor.executeBatchWithCallback(orders, callbackData); + } + + /// @notice fill UniswapX orders using SwapRouter02 + /// @param callbackData It has the below encoded: + /// address[] memory tokensToApproveForSwapRouter02: Max approve these tokens to swapRouter02 + /// address[] memory tokensToApproveForReactor: Max approve these tokens to reactor + /// bytes[] memory multicallData: Pass into swapRouter02.multicall() + function reactorCallback(ResolvedOrder[] calldata, bytes calldata callbackData) external onlyReactor { + ( + address[] memory tokensToApproveForSwapRouter02, + address[] memory tokensToApproveForReactor, + bytes[] memory multicallData + ) = abi.decode(callbackData, (address[], address[], bytes[])); + + unchecked { + for (uint256 i = 0; i < tokensToApproveForSwapRouter02.length; i++) { + ERC20(tokensToApproveForSwapRouter02[i]).safeApprove(address(swapRouter02), type(uint256).max); + } + + for (uint256 i = 0; i < tokensToApproveForReactor.length; i++) { + ERC20(tokensToApproveForReactor[i]).safeApprove(address(reactor), type(uint256).max); + } + } + + swapRouter02.multicall(type(uint256).max, multicallData); + + // transfer any native balance to the reactor + // it will refund any excess + if (address(this).balance > 0) { + CurrencyLibrary.transferNative(address(reactor), address(this).balance); + } + } + + /// @notice This function can be used to convert ERC20s to ETH that remains in this contract + /// @param tokensToApprove Max approve these tokens to swapRouter02 + /// @param multicallData Pass into swapRouter02.multicall() + function multicall(ERC20[] calldata tokensToApprove, bytes[] calldata multicallData) external onlyOwner { + for (uint256 i = 0; i < tokensToApprove.length; i++) { + tokensToApprove[i].safeApprove(address(swapRouter02), type(uint256).max); + } + swapRouter02.multicall(type(uint256).max, multicallData); + } + + /// @notice Unwraps the contract's WETH9 balance and sends it to the recipient as ETH. Can only be called by owner. + /// @param recipient The address receiving ETH + function unwrapWETH(address recipient) external onlyOwner { + uint256 balanceWETH = weth.balanceOf(address(this)); + + weth.withdraw(balanceWETH); + SafeTransferLib.safeTransferETH(recipient, address(this).balance); + } + + /// @notice Transfer all ETH in this contract to the recipient. Can only be called by owner. + /// @param recipient The recipient of the ETH + function withdrawETH(address recipient) external onlyOwner { + SafeTransferLib.safeTransferETH(recipient, address(this).balance); + } + + /// @notice Transfer the entire balance of an ERC20 token in this contract to a recipient. Can only be called by owner. + /// @param token The ERC20 token to withdraw + /// @param to The recipient of the tokens + function withdrawERC20(ERC20 token, address to) external onlyOwner { + token.safeTransfer(to, token.balanceOf(address(this))); + } + + /// @notice Update the reactor contract address. Can only be called by owner. + /// @param _reactor The new reactor contract address + function updateReactor(IReactor _reactor) external onlyOwner { + emit ReactorChanged(address(_reactor), address(reactor)); + reactor = _reactor; + } + + /// @notice Necessary for this contract to receive ETH when calling unwrapWETH() + receive() external payable {} +}