From 5627a1902b74ace7eccce9888b4505f77b827d43 Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Thu, 23 Jan 2025 17:04:08 +0000 Subject: [PATCH] feat: UniswapV2 SwapExecutor --- don't change below this line --- ENG-4033 Took 52 minutes Took 3 minutes Took 5 minutes Took 36 seconds Took 2 minutes Took 30 seconds --- .gitmodules | 3 + README.md | 2 +- foundry/README.md | 14 +-- foundry/lib/v2-core | 1 + foundry/remappings.txt | 3 +- .../src/executors/Uniswapv2SwapExecutor.sol | 73 ++++++++++++ foundry/src/interfaces/ISwapExecutor.sol | 37 +++++++ foundry/test/Constants.sol | 1 + .../executors/UniswapV2SwapExecutor.t.sol | 104 ++++++++++++++++++ 9 files changed, 226 insertions(+), 12 deletions(-) create mode 160000 foundry/lib/v2-core create mode 100644 foundry/src/executors/Uniswapv2SwapExecutor.sol create mode 100644 foundry/src/interfaces/ISwapExecutor.sol create mode 100644 foundry/test/executors/UniswapV2SwapExecutor.t.sol diff --git a/.gitmodules b/.gitmodules index b61da93..b573165 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "foundry/lib/permit2"] path = foundry/lib/permit2 url = https://github.com/Uniswap/permit2 +[submodule "foundry/lib/v2-core"] + path = foundry/lib/v2-core + url = https://github.com/uniswap/v2-core diff --git a/README.md b/README.md index db429f9..49b15fe 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ To run locally, simply install Slither in your conda env and run it inside the f conda create --name tycho-execution python=3.10 conda activate tycho-execution -python3 -m pip install slither-analyzer` +pip install slither-analyzer cd foundry slither . ``` \ No newline at end of file diff --git a/foundry/README.md b/foundry/README.md index 9265b45..ffbfb13 100644 --- a/foundry/README.md +++ b/foundry/README.md @@ -4,10 +4,10 @@ Foundry consists of: -- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). -- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. -- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. -- **Chisel**: Fast, utilitarian, and verbose solidity REPL. +- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). +- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. +- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. +- **Chisel**: Fast, utilitarian, and verbose solidity REPL. ## Documentation @@ -45,12 +45,6 @@ $ forge snapshot $ anvil ``` -### Deploy - -```shell -$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key -``` - ### Cast ```shell diff --git a/foundry/lib/v2-core b/foundry/lib/v2-core new file mode 160000 index 0000000..4dd5906 --- /dev/null +++ b/foundry/lib/v2-core @@ -0,0 +1 @@ +Subproject commit 4dd59067c76dea4a0e8e4bfdda41877a6b16dedc diff --git a/foundry/remappings.txt b/foundry/remappings.txt index 0ab4175..f4b9a08 100644 --- a/foundry/remappings.txt +++ b/foundry/remappings.txt @@ -1,4 +1,5 @@ @openzeppelin/=lib/openzeppelin-contracts/ @interfaces/=interfaces/ @permit2/=lib/permit2/ -@src/=src/ \ No newline at end of file +@src/=src/ +@uniswap-v2/=lib/v2-core/ \ No newline at end of file diff --git a/foundry/src/executors/Uniswapv2SwapExecutor.sol b/foundry/src/executors/Uniswapv2SwapExecutor.sol new file mode 100644 index 0000000..c194c80 --- /dev/null +++ b/foundry/src/executors/Uniswapv2SwapExecutor.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +import "@uniswap-v2/contracts/interfaces/IUniswapV2Pair.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {ISwapExecutor} from "../interfaces/ISwapExecutor.sol"; + +contract UniswapV2SwapExecutor is ISwapExecutor { + using SafeERC20 for IERC20; + + function swap(uint256 givenAmount, bytes calldata data) + external + returns (uint256 calculatedAmount) + { + address target; + address receiver; + bool zeroForOne; + bool exactOut; + IERC20 tokenIn; + + (tokenIn, target, receiver, zeroForOne, exactOut) = _decodeData(data); + calculatedAmount = _getAmountOut(target, givenAmount, zeroForOne); + tokenIn.safeTransfer(target, givenAmount); + + IUniswapV2Pair pool = IUniswapV2Pair(target); + if (zeroForOne) { + pool.swap(0, calculatedAmount, receiver, ""); + } else { + pool.swap(calculatedAmount, 0, receiver, ""); + } + } + + function _decodeData(bytes calldata data) + internal + pure + returns ( + IERC20 inToken, + address target, + address receiver, + bool zeroForOne, + bool exactOut + ) + { + inToken = IERC20(address(bytes20(data[0:20]))); + target = address(bytes20(data[20:40])); + receiver = address(bytes20(data[40:60])); + zeroForOne = uint8(data[60]) > 0; + exactOut = uint8(data[61]) > 0; + } + + function _getAmountOut(address target, uint256 amountIn, bool zeroForOne) + internal + view + returns (uint256 amount) + { + IUniswapV2Pair pair = IUniswapV2Pair(target); + uint112 reserveIn; + uint112 reserveOut; + if (zeroForOne) { + // slither-disable-next-line unused-return + (reserveIn, reserveOut,) = pair.getReserves(); + } else { + // slither-disable-next-line unused-return + (reserveOut, reserveIn,) = pair.getReserves(); + } + + require(reserveIn > 0 && reserveOut > 0, "L"); + uint256 amountInWithFee = amountIn * 997; + uint256 numerator = amountInWithFee * uint256(reserveOut); + uint256 denominator = (uint256(reserveIn) * 1000) + amountInWithFee; + amount = numerator / denominator; + } +} diff --git a/foundry/src/interfaces/ISwapExecutor.sol b/foundry/src/interfaces/ISwapExecutor.sol new file mode 100644 index 0000000..d8bf82c --- /dev/null +++ b/foundry/src/interfaces/ISwapExecutor.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +pragma abicoder v2; + +interface ISwapExecutor { + /** + * @notice Performs a swap on a liquidity pool. + * @dev This method can either take the amount of the input token or the amount + * of the output token that we would like to swap. If called with the amount of + * the input token, the amount of the output token will be returned, and vice + * versa. Whether it is the input or output that is given, is encoded in the data + * parameter. + * + * Note Part of the informal interface is that the executor supports sending the received + * tokens to a receiver address. If the underlying smart contract does not provide this + * functionality consider adding an additional transfer in the implementation. + * + * This function is marked as `payable` to accommodate delegatecalls, which can forward + * a potential `msg.value` to it. + * + * @param givenAmount The amount of either the input token or output token to swap. + * @param data Data that holds information necessary to perform the swap. + * @return calculatedAmount The amount of either the input token or output token + * swapped, depending on the givenAmount inputted. + */ + function swap(uint256 givenAmount, bytes calldata data) + external + returns (uint256 calculatedAmount); +} + +interface ISwapExecutorErrors { + error InvalidParameterLength(uint256); + error UnknownCurveType(uint8); +} diff --git a/foundry/test/Constants.sol b/foundry/test/Constants.sol index dfd2c1b..058a27a 100644 --- a/foundry/test/Constants.sol +++ b/foundry/test/Constants.sol @@ -14,6 +14,7 @@ contract Constants is Test { address DUMMY = makeAddr("dummy"); address WETH_ADDR = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + address DAI_ADDR = address(0x6B175474E89094C44Da98b954EedeAC495271d0F); /** * @dev Deploys a dummy contract with non-empty bytecode diff --git a/foundry/test/executors/UniswapV2SwapExecutor.t.sol b/foundry/test/executors/UniswapV2SwapExecutor.t.sol new file mode 100644 index 0000000..4b9587a --- /dev/null +++ b/foundry/test/executors/UniswapV2SwapExecutor.t.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +import "@src/executors/Uniswapv2SwapExecutor.sol"; +import {Test} from "../../lib/forge-std/src/Test.sol"; +import {Constants} from "../Constants.sol"; + +contract UniswapV2SwapExecutorExposed is UniswapV2SwapExecutor { + function decodeParams(bytes calldata data) + external + pure + returns ( + IERC20 inToken, + address target, + address receiver, + bool zeroForOne, + bool exactOut + ) + { + return _decodeData(data); + } + + function getAmountOut(address target, uint256 amountIn, bool zeroForOne) + external + view + returns (uint256 amount) + { + return _getAmountOut(target, amountIn, zeroForOne); + } +} + +contract UniswapV2SwapExecutorTest is + UniswapV2SwapExecutorExposed, + Test, + Constants +{ + using SafeERC20 for IERC20; + + UniswapV2SwapExecutorExposed uniswapV2Exposed; + IERC20 WETH = IERC20(WETH_ADDR); + IERC20 DAI = IERC20(DAI_ADDR); + address WETH_DAI_POOL = 0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11; + + function setUp() public { + uint256 forkBlock = 17323404; + vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); + uniswapV2Exposed = new UniswapV2SwapExecutorExposed(); + } + + function testDecodeParams() public view { + bytes memory params = + abi.encodePacked(WETH_ADDR, address(2), address(3), false, true); + + ( + IERC20 tokenIn, + address target, + address receiver, + bool zeroForOne, + bool exactOut + ) = uniswapV2Exposed.decodeParams(params); + + assertEq(address(tokenIn), WETH_ADDR); + assertEq(target, address(2)); + assertEq(receiver, address(3)); + assertEq(zeroForOne, false); + assertEq(exactOut, true); + } + + function testAmountOut() public view { + uint256 amountOut = + uniswapV2Exposed.getAmountOut(WETH_DAI_POOL, 10 ** 18, false); + uint256 expAmountOut = 1847751195973566072891; + assertEq(amountOut, expAmountOut); + } + + // triggers a uint112 overflow on purpose + function testAmountOutInt112Overflow() public view { + address target = 0x0B9f5cEf1EE41f8CCCaA8c3b4c922Ab406c980CC; + uint256 amountIn = 83638098812630667483959471576; + + uint256 amountOut = + uniswapV2Exposed.getAmountOut(target, amountIn, true); + + assertGe(amountOut, 0); + } + + function testSwap() public { + uint256 amountIn = 10 ** 18; + uint256 amountOut = 1847751195973566072891; + bool zeroForOne = false; + bool exactOut = true; + bytes memory protocolData = abi.encodePacked( + WETH_ADDR, WETH_DAI_POOL, BOB, zeroForOne, exactOut + ); + + vm.startPrank(ADMIN); + deal(WETH_ADDR, address(uniswapV2Exposed), amountIn); + uniswapV2Exposed.swap(amountIn, protocolData); + vm.stopPrank(); + + uint256 finalBalance = DAI.balanceOf(BOB); + assertGe(finalBalance, amountOut); + } +}