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
This commit is contained in:
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -4,3 +4,6 @@
|
|||||||
[submodule "foundry/lib/permit2"]
|
[submodule "foundry/lib/permit2"]
|
||||||
path = foundry/lib/permit2
|
path = foundry/lib/permit2
|
||||||
url = https://github.com/Uniswap/permit2
|
url = https://github.com/Uniswap/permit2
|
||||||
|
[submodule "foundry/lib/v2-core"]
|
||||||
|
path = foundry/lib/v2-core
|
||||||
|
url = https://github.com/uniswap/v2-core
|
||||||
|
|||||||
@@ -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 create --name tycho-execution python=3.10
|
||||||
conda activate tycho-execution
|
conda activate tycho-execution
|
||||||
|
|
||||||
python3 -m pip install slither-analyzer`
|
pip install slither-analyzer
|
||||||
cd foundry
|
cd foundry
|
||||||
slither .
|
slither .
|
||||||
```
|
```
|
||||||
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
Foundry consists of:
|
Foundry consists of:
|
||||||
|
|
||||||
- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools).
|
- **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.
|
- **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.
|
- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network.
|
||||||
- **Chisel**: Fast, utilitarian, and verbose solidity REPL.
|
- **Chisel**: Fast, utilitarian, and verbose solidity REPL.
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
@@ -45,12 +45,6 @@ $ forge snapshot
|
|||||||
$ anvil
|
$ anvil
|
||||||
```
|
```
|
||||||
|
|
||||||
### Deploy
|
|
||||||
|
|
||||||
```shell
|
|
||||||
$ forge script script/Counter.s.sol:CounterScript --rpc-url <your_rpc_url> --private-key <your_private_key>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Cast
|
### Cast
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
|
|||||||
1
foundry/lib/v2-core
Submodule
1
foundry/lib/v2-core
Submodule
Submodule foundry/lib/v2-core added at 4dd59067c7
@@ -1,4 +1,5 @@
|
|||||||
@openzeppelin/=lib/openzeppelin-contracts/
|
@openzeppelin/=lib/openzeppelin-contracts/
|
||||||
@interfaces/=interfaces/
|
@interfaces/=interfaces/
|
||||||
@permit2/=lib/permit2/
|
@permit2/=lib/permit2/
|
||||||
@src/=src/
|
@src/=src/
|
||||||
|
@uniswap-v2/=lib/v2-core/
|
||||||
73
foundry/src/executors/Uniswapv2SwapExecutor.sol
Normal file
73
foundry/src/executors/Uniswapv2SwapExecutor.sol
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
37
foundry/src/interfaces/ISwapExecutor.sol
Normal file
37
foundry/src/interfaces/ISwapExecutor.sol
Normal file
@@ -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);
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ contract Constants is Test {
|
|||||||
address DUMMY = makeAddr("dummy");
|
address DUMMY = makeAddr("dummy");
|
||||||
|
|
||||||
address WETH_ADDR = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
|
address WETH_ADDR = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
|
||||||
|
address DAI_ADDR = address(0x6B175474E89094C44Da98b954EedeAC495271d0F);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dev Deploys a dummy contract with non-empty bytecode
|
* @dev Deploys a dummy contract with non-empty bytecode
|
||||||
|
|||||||
104
foundry/test/executors/UniswapV2SwapExecutor.t.sol
Normal file
104
foundry/test/executors/UniswapV2SwapExecutor.t.sol
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user