diff --git a/foundry/src/executors/ExecutorTransferMethods.sol b/foundry/src/executors/ExecutorTransferMethods.sol new file mode 100644 index 0000000..45fc1d2 --- /dev/null +++ b/foundry/src/executors/ExecutorTransferMethods.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.26; + +import "@interfaces/IExecutor.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@permit2/src/interfaces/IAllowanceTransfer.sol"; +import "@permit2/src/interfaces/IAllowanceTransfer.sol"; + +error ExecutorTransferMethods__InvalidPermit2(); + +contract ExecutorTransferMethods { + using SafeERC20 for IERC20; + + IAllowanceTransfer public immutable permit2; + + enum TransferMethod { + TRANSFER, + TRANSFERFROM, + TRANSFERPERMIT2, + NONE + } + + constructor(address _permit2) { + if (_permit2 == address(0)) { + revert ExecutorTransferMethods__InvalidPermit2(); + } + permit2 = IAllowanceTransfer(_permit2); + } + + function _transfer( + IERC20 tokenIn, + address receiver, + uint256 amount, + TransferMethod method + ) internal { + if (method == TransferMethod.TRANSFER) { + tokenIn.safeTransfer(receiver, amount); + } else if (method == TransferMethod.TRANSFERFROM) { + tokenIn.safeTransferFrom(msg.sender, receiver, amount); + } else if (method == TransferMethod.TRANSFERPERMIT2) { + // Permit2.permit is called from the TychoRouter + permit2.transferFrom( + msg.sender, + receiver, // Does this work if receiver is not address(this)? + uint160(amount), + address(tokenIn) + ); + } else { + // Funds are likely already in pool. Do nothing. + } + } +} diff --git a/foundry/src/executors/UniswapV2Executor.sol b/foundry/src/executors/UniswapV2Executor.sol index 50fb056..29544ab 100644 --- a/foundry/src/executors/UniswapV2Executor.sol +++ b/foundry/src/executors/UniswapV2Executor.sol @@ -4,20 +4,23 @@ pragma solidity ^0.8.26; import "@interfaces/IExecutor.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@uniswap-v2/contracts/interfaces/IUniswapV2Pair.sol"; +import "./ExecutorTransferMethods.sol"; error UniswapV2Executor__InvalidDataLength(); error UniswapV2Executor__InvalidTarget(); error UniswapV2Executor__InvalidFactory(); error UniswapV2Executor__InvalidInitCode(); -contract UniswapV2Executor is IExecutor { +contract UniswapV2Executor is IExecutor, ExecutorTransferMethods { using SafeERC20 for IERC20; address public immutable factory; bytes32 public immutable initCode; address private immutable self; - constructor(address _factory, bytes32 _initCode) { + constructor(address _factory, bytes32 _initCode, address _permit2) + ExecutorTransferMethods(_permit2) + { if (_factory == address(0)) { revert UniswapV2Executor__InvalidFactory(); } @@ -35,17 +38,18 @@ contract UniswapV2Executor is IExecutor { payable returns (uint256 calculatedAmount) { + IERC20 tokenIn; address target; address receiver; bool zeroForOne; - IERC20 tokenIn; + TransferMethod method; - (tokenIn, target, receiver, zeroForOne) = _decodeData(data); + (tokenIn, target, receiver, zeroForOne, method) = _decodeData(data); _verifyPairAddress(target); calculatedAmount = _getAmountOut(target, givenAmount, zeroForOne); - tokenIn.safeTransfer(target, givenAmount); + _transfer(tokenIn, target, givenAmount, method); IUniswapV2Pair pool = IUniswapV2Pair(target); if (zeroForOne) { @@ -62,7 +66,8 @@ contract UniswapV2Executor is IExecutor { IERC20 inToken, address target, address receiver, - bool zeroForOne + bool zeroForOne, + TransferMethod method ) { if (data.length != 61) { @@ -72,6 +77,8 @@ contract UniswapV2Executor is IExecutor { target = address(bytes20(data[20:40])); receiver = address(bytes20(data[40:60])); zeroForOne = uint8(data[60]) > 0; + // TODO properly decode, assume encoded using just 1 byte. + method = TransferMethod.TRANSFER; } function _getAmountOut(address target, uint256 amountIn, bool zeroForOne) diff --git a/foundry/test/TychoRouterTestSetup.sol b/foundry/test/TychoRouterTestSetup.sol index 1690d3c..89ea394 100644 --- a/foundry/test/TychoRouterTestSetup.sol +++ b/foundry/test/TychoRouterTestSetup.sol @@ -97,7 +97,7 @@ contract TychoRouterTestSetup is Constants { address ekuboCore = 0xe0e0e08A6A4b9Dc7bD67BCB7aadE5cF48157d444; IPoolManager poolManager = IPoolManager(poolManagerAddress); - usv2Executor = new UniswapV2Executor(factoryV2, initCodeV2); + usv2Executor = new UniswapV2Executor(factoryV2, initCodeV2, PERMIT2_ADDRESS); usv3Executor = new UniswapV3Executor(factoryV3, initCodeV3); usv4Executor = new UniswapV4Executor(poolManager); pancakev3Executor = diff --git a/foundry/test/executors/UniswapV2Executor.t.sol b/foundry/test/executors/UniswapV2Executor.t.sol index a26a9b7..3f303fd 100644 --- a/foundry/test/executors/UniswapV2Executor.t.sol +++ b/foundry/test/executors/UniswapV2Executor.t.sol @@ -2,12 +2,13 @@ pragma solidity ^0.8.26; import "@src/executors/UniswapV2Executor.sol"; +import "@src/executors/ExecutorTransferMethods.sol"; import {Test} from "../../lib/forge-std/src/Test.sol"; import {Constants} from "../Constants.sol"; contract UniswapV2ExecutorExposed is UniswapV2Executor { - constructor(address _factory, bytes32 _initCode) - UniswapV2Executor(_factory, _initCode) + constructor(address _factory, bytes32 _initCode, address _permit2) + UniswapV2Executor(_factory, _initCode, _permit2) {} function decodeParams(bytes calldata data) @@ -17,7 +18,8 @@ contract UniswapV2ExecutorExposed is UniswapV2Executor { IERC20 inToken, address target, address receiver, - bool zeroForOne + bool zeroForOne, + TransferMethod method ) { return _decodeData(data); @@ -59,13 +61,13 @@ contract UniswapV2ExecutorTest is Test, Constants { uint256 forkBlock = 17323404; vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); uniswapV2Exposed = new UniswapV2ExecutorExposed( - USV2_FACTORY_ETHEREUM, USV2_POOL_CODE_INIT_HASH + USV2_FACTORY_ETHEREUM, USV2_POOL_CODE_INIT_HASH, PERMIT2_ADDRESS ); sushiswapV2Exposed = new UniswapV2ExecutorExposed( - SUSHISWAPV2_FACTORY_ETHEREUM, SUSHIV2_POOL_CODE_INIT_HASH + SUSHISWAPV2_FACTORY_ETHEREUM, SUSHIV2_POOL_CODE_INIT_HASH, PERMIT2_ADDRESS ); pancakeswapV2Exposed = new UniswapV2ExecutorExposed( - PANCAKESWAPV2_FACTORY_ETHEREUM, PANCAKEV2_POOL_CODE_INIT_HASH + PANCAKESWAPV2_FACTORY_ETHEREUM, PANCAKEV2_POOL_CODE_INIT_HASH, PERMIT2_ADDRESS ); } @@ -73,13 +75,19 @@ contract UniswapV2ExecutorTest is Test, Constants { bytes memory params = abi.encodePacked(WETH_ADDR, address(2), address(3), false); - (IERC20 tokenIn, address target, address receiver, bool zeroForOne) = - uniswapV2Exposed.decodeParams(params); + ( + IERC20 tokenIn, + address target, + address receiver, + bool zeroForOne, + ExecutorTransferMethods.TransferMethod method + ) = uniswapV2Exposed.decodeParams(params); assertEq(address(tokenIn), WETH_ADDR); assertEq(target, address(2)); assertEq(receiver, address(3)); assertEq(zeroForOne, false); + assertEq(0, uint8(method)); } function testDecodeParamsInvalidDataLength() public { @@ -145,13 +153,20 @@ contract UniswapV2ExecutorTest is Test, Constants { bytes memory protocolData = hex"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc288e6a0c2ddd26feeb64f039a2c41296fcb3f5640000000000000000000000000000000000000000100"; - (IERC20 tokenIn, address target, address receiver, bool zeroForOne) = - uniswapV2Exposed.decodeParams(protocolData); + ( + IERC20 tokenIn, + address target, + address receiver, + bool zeroForOne, + ExecutorTransferMethods.TransferMethod method + ) = uniswapV2Exposed.decodeParams(protocolData); assertEq(address(tokenIn), WETH_ADDR); assertEq(target, 0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640); assertEq(receiver, 0x0000000000000000000000000000000000000001); assertEq(zeroForOne, false); + // TRANSFER = 0 + assertEq(0, uint8(method)); } function testSwapIntegration() public {