feat: UniswapV3Executor and integration tests
- Note: I think we can get the fee straight from the pool... why did we always encode this and send it from the solver? Is this bound to change sometimes?
This commit is contained in:
@@ -369,8 +369,8 @@ contract TychoRouter is
|
||||
int256 amount0Delta,
|
||||
int256 amount1Delta,
|
||||
bytes calldata data
|
||||
) internal view returns (uint256 amountOwed, address tokenOwed) {
|
||||
address tokenIn = address(bytes20(data[0:20]));
|
||||
) internal view returns (uint256 amountOwed, address tokenIn) {
|
||||
tokenIn = address(bytes20(data[0:20]));
|
||||
address tokenOut = address(bytes20(data[20:40]));
|
||||
uint24 poolFee = uint24(bytes3(data[40:43]));
|
||||
|
||||
@@ -382,6 +382,6 @@ contract TychoRouter is
|
||||
amountOwed =
|
||||
amount0Delta > 0 ? uint256(amount0Delta) : uint256(amount1Delta);
|
||||
|
||||
return (amountOwed, tokenOwed);
|
||||
return (amountOwed, tokenIn);
|
||||
}
|
||||
}
|
||||
|
||||
85
foundry/src/executors/UniswapV3Executor.sol
Normal file
85
foundry/src/executors/UniswapV3Executor.sol
Normal file
@@ -0,0 +1,85 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.28;
|
||||
|
||||
import "@interfaces/IExecutor.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
|
||||
|
||||
error UniswapV3Executor__InvalidDataLength();
|
||||
|
||||
contract UniswapV3Executor is IExecutor {
|
||||
uint160 private constant MIN_SQRT_RATIO = 4295128739;
|
||||
uint160 private constant MAX_SQRT_RATIO =
|
||||
1461446703485210103287273052203988822378723970342;
|
||||
address private constant factoryV3 =
|
||||
0x1F98431c8aD98523631AE4a59f267346ea31F984;
|
||||
|
||||
// slither-disable-next-line locked-ether
|
||||
function swap(uint256 amountIn, bytes calldata data)
|
||||
external
|
||||
payable
|
||||
returns (uint256 amountOut)
|
||||
{
|
||||
(
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
uint24 fee,
|
||||
address receiver,
|
||||
address target,
|
||||
bool zeroForOne
|
||||
) = _decodeData(data);
|
||||
int256 amount0;
|
||||
int256 amount1;
|
||||
IUniswapV3Pool pool = IUniswapV3Pool(target);
|
||||
|
||||
bytes memory callbackData = _makeV3CallbackData(tokenIn, tokenOut, fee);
|
||||
|
||||
{
|
||||
(amount0, amount1) = pool.swap(
|
||||
receiver,
|
||||
zeroForOne,
|
||||
// positive means exactIn
|
||||
int256(amountIn),
|
||||
zeroForOne ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1,
|
||||
callbackData
|
||||
);
|
||||
}
|
||||
|
||||
if (zeroForOne) {
|
||||
amountOut = amount1 > 0 ? uint256(amount1) : uint256(-amount1);
|
||||
} else {
|
||||
amountOut = amount0 > 0 ? uint256(amount0) : uint256(-amount0);
|
||||
}
|
||||
}
|
||||
|
||||
function _decodeData(bytes calldata data)
|
||||
internal
|
||||
pure
|
||||
returns (
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
uint24 fee,
|
||||
address receiver,
|
||||
address target,
|
||||
bool zeroForOne
|
||||
)
|
||||
{
|
||||
if (data.length != 84) {
|
||||
revert UniswapV3Executor__InvalidDataLength();
|
||||
}
|
||||
tokenIn = address(bytes20(data[0:20]));
|
||||
tokenOut = address(bytes20(data[20:40]));
|
||||
fee = uint24(bytes3(data[40:43]));
|
||||
receiver = address(bytes20(data[43:63]));
|
||||
target = address(bytes20(data[63:83]));
|
||||
zeroForOne = uint8(data[83]) > 0;
|
||||
}
|
||||
|
||||
function _makeV3CallbackData(address tokenIn, address tokenOut, uint24 fee)
|
||||
internal
|
||||
pure
|
||||
returns (bytes memory)
|
||||
{
|
||||
return abi.encodePacked(tokenIn, tokenOut, fee);
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,9 @@ contract Constants is Test {
|
||||
address WETH_WBTC_POOL = 0xBb2b8038a1640196FbE3e38816F3e67Cba72D940;
|
||||
address USDC_WBTC_POOL = 0x004375Dff511095CC5A197A54140a24eFEF3A416;
|
||||
|
||||
// uniswap v3
|
||||
address DAI_WETH_USV3 = 0xC2e9F25Be6257c210d7Adf0D4Cd6E3E881ba25f8;
|
||||
|
||||
/**
|
||||
* @dev Deploys a dummy contract with non-empty bytecode
|
||||
*/
|
||||
|
||||
@@ -215,7 +215,7 @@ contract TychoRouterTest is TychoRouterTestSetup {
|
||||
function testSwapSimple() public {
|
||||
// Trade 1 WETH for DAI with 1 swap on Uniswap V2
|
||||
// 1 WETH -> DAI
|
||||
// (univ2)
|
||||
// (USV2)
|
||||
uint256 amountIn = 1 ether;
|
||||
deal(WETH_ADDR, tychoRouterAddr, amountIn);
|
||||
|
||||
@@ -234,7 +234,7 @@ contract TychoRouterTest is TychoRouterTestSetup {
|
||||
bytes[] memory swaps = new bytes[](1);
|
||||
swaps[0] = swap;
|
||||
|
||||
tychoRouter.ExposedSwap(amountIn, 2, pleEncode(swaps));
|
||||
tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps));
|
||||
|
||||
uint256 daiBalance = IERC20(DAI_ADDR).balanceOf(tychoRouterAddr);
|
||||
assertEq(daiBalance, 2630432278145144658455);
|
||||
@@ -271,7 +271,7 @@ contract TychoRouterTest is TychoRouterTestSetup {
|
||||
encodeUniswapV2Swap(DAI_ADDR, DAI_USDC_POOL, tychoRouterAddr, true)
|
||||
);
|
||||
|
||||
tychoRouter.ExposedSwap(amountIn, 3, pleEncode(swaps));
|
||||
tychoRouter.exposedSwap(amountIn, 3, pleEncode(swaps));
|
||||
|
||||
uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(tychoRouterAddr);
|
||||
assertEq(usdcBalance, 2610580090);
|
||||
@@ -332,7 +332,7 @@ contract TychoRouterTest is TychoRouterTestSetup {
|
||||
encodeUniswapV2Swap(DAI_ADDR, DAI_USDC_POOL, tychoRouterAddr, true)
|
||||
);
|
||||
|
||||
tychoRouter.ExposedSwap(amountIn, 4, pleEncode(swaps));
|
||||
tychoRouter.exposedSwap(amountIn, 4, pleEncode(swaps));
|
||||
|
||||
uint256 usdcBalance = IERC20(USDC_ADDR).balanceOf(tychoRouterAddr);
|
||||
assertEq(usdcBalance, 2581503157);
|
||||
@@ -606,4 +606,52 @@ contract TychoRouterTest is TychoRouterTestSetup {
|
||||
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function testUSV3Callback() public {
|
||||
uint24 poolFee = 3000;
|
||||
uint256 amountOwed = 1000000000000000000;
|
||||
deal(WETH_ADDR, tychoRouterAddr, amountOwed);
|
||||
uint256 initialPoolReserve = IERC20(WETH_ADDR).balanceOf(DAI_WETH_USV3);
|
||||
|
||||
vm.startPrank(DAI_WETH_USV3);
|
||||
tychoRouter.uniswapV3SwapCallback(
|
||||
-2631245338449998525223,
|
||||
int256(amountOwed),
|
||||
abi.encodePacked(WETH_ADDR, DAI_ADDR, poolFee)
|
||||
);
|
||||
vm.stopPrank();
|
||||
|
||||
uint256 finalPoolReserve = IERC20(WETH_ADDR).balanceOf(DAI_WETH_USV3);
|
||||
assertEq(finalPoolReserve - initialPoolReserve, amountOwed);
|
||||
}
|
||||
|
||||
function testSwapSingleUSV3() public {
|
||||
// Trade 1 WETH for DAI with 1 swap on Uniswap V3
|
||||
// 1 WETH -> DAI
|
||||
// (USV3)
|
||||
uint256 amountIn = 10 ** 18;
|
||||
deal(WETH_ADDR, tychoRouterAddr, amountIn);
|
||||
|
||||
uint256 expAmountOut = 1205_128428842122129186; //Swap 1 WETH for 1205.12 DAI
|
||||
bool zeroForOne = false;
|
||||
bytes memory protocolData = encodeUniswapV3Swap(
|
||||
WETH_ADDR, DAI_ADDR, tychoRouterAddr, DAI_WETH_USV3, zeroForOne
|
||||
);
|
||||
bytes memory swap = encodeSwap(
|
||||
uint8(0),
|
||||
uint8(1),
|
||||
uint24(0),
|
||||
address(usv3Executor),
|
||||
bytes4(0),
|
||||
protocolData
|
||||
);
|
||||
|
||||
bytes[] memory swaps = new bytes[](1);
|
||||
swaps[0] = swap;
|
||||
|
||||
tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps));
|
||||
|
||||
uint256 finalBalance = IERC20(DAI_ADDR).balanceOf(tychoRouterAddr);
|
||||
assertGe(finalBalance, expAmountOut);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import "./Constants.sol";
|
||||
import "./mock/MockERC20.sol";
|
||||
import "@src/TychoRouter.sol";
|
||||
import {WETH} from "../lib/permit2/lib/solmate/src/tokens/WETH.sol";
|
||||
import "../src/executors/UniswapV3Executor.sol";
|
||||
|
||||
contract TychoRouterExposed is TychoRouter {
|
||||
constructor(address _permit2, address weth, address usv3Factory)
|
||||
@@ -20,7 +21,7 @@ contract TychoRouterExposed is TychoRouter {
|
||||
return _unwrapETH(amount);
|
||||
}
|
||||
|
||||
function ExposedSwap(
|
||||
function exposedSwap(
|
||||
uint256 amountIn,
|
||||
uint256 nTokens,
|
||||
bytes calldata swaps
|
||||
@@ -34,6 +35,7 @@ contract TychoRouterTestSetup is Test, Constants {
|
||||
address tychoRouterAddr;
|
||||
address permit2Address = address(0x000000000022D473030F116dDEE9F6B43aC78BA3);
|
||||
UniswapV2Executor public usv2Executor;
|
||||
UniswapV3Executor public usv3Executor;
|
||||
MockERC20[] tokens;
|
||||
|
||||
function setUp() public {
|
||||
@@ -41,8 +43,9 @@ contract TychoRouterTestSetup is Test, Constants {
|
||||
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
|
||||
|
||||
vm.startPrank(ADMIN);
|
||||
address factoryV3 = address(0x1F98431c8aD98523631AE4a59f267346ea31F984);
|
||||
tychoRouter =
|
||||
new TychoRouterExposed(permit2Address, WETH_ADDR, address(1));
|
||||
new TychoRouterExposed(permit2Address, WETH_ADDR, factoryV3);
|
||||
tychoRouterAddr = address(tychoRouter);
|
||||
tychoRouter.grantRole(keccak256("FUND_RESCUER_ROLE"), FUND_RESCUER);
|
||||
tychoRouter.grantRole(keccak256("FEE_SETTER_ROLE"), FEE_SETTER);
|
||||
@@ -55,8 +58,10 @@ contract TychoRouterTestSetup is Test, Constants {
|
||||
vm.stopPrank();
|
||||
|
||||
usv2Executor = new UniswapV2Executor();
|
||||
usv3Executor = new UniswapV3Executor();
|
||||
vm.startPrank(EXECUTOR_SETTER);
|
||||
tychoRouter.setExecutor(address(usv2Executor));
|
||||
tychoRouter.setExecutor(address(usv3Executor));
|
||||
vm.stopPrank();
|
||||
|
||||
vm.startPrank(BOB);
|
||||
@@ -190,4 +195,17 @@ contract TychoRouterTestSetup is Test, Constants {
|
||||
) internal pure returns (bytes memory) {
|
||||
return abi.encodePacked(tokenIn, target, receiver, zero2one);
|
||||
}
|
||||
|
||||
function encodeUniswapV3Swap(
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
address receiver,
|
||||
address target,
|
||||
bool zero2one
|
||||
) internal view returns (bytes memory) {
|
||||
IUniswapV3Pool pool = IUniswapV3Pool(target);
|
||||
return abi.encodePacked(
|
||||
tokenIn, tokenOut, pool.fee(), receiver, target, zero2one
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
68
foundry/test/executors/UniswapV3Executor.t.sol
Normal file
68
foundry/test/executors/UniswapV3Executor.t.sol
Normal file
@@ -0,0 +1,68 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.28;
|
||||
|
||||
import "@src/executors/UniswapV3Executor.sol";
|
||||
import {Test} from "../../lib/forge-std/src/Test.sol";
|
||||
import {Constants} from "../Constants.sol";
|
||||
|
||||
contract UniswapV3ExecutorExposed is UniswapV3Executor {
|
||||
function decodeData(bytes calldata data)
|
||||
external
|
||||
pure
|
||||
returns (
|
||||
address inToken,
|
||||
address outToken,
|
||||
uint24 fee,
|
||||
address receiver,
|
||||
address target,
|
||||
bool zeroForOne
|
||||
)
|
||||
{
|
||||
return _decodeData(data);
|
||||
}
|
||||
}
|
||||
|
||||
contract UniswapV3ExecutorTest is UniswapV3ExecutorExposed, Test, Constants {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
UniswapV3ExecutorExposed uniswapV3Exposed;
|
||||
IERC20 WETH = IERC20(WETH_ADDR);
|
||||
IERC20 DAI = IERC20(DAI_ADDR);
|
||||
|
||||
function setUp() public {
|
||||
uint256 forkBlock = 17323404;
|
||||
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
|
||||
uniswapV3Exposed = new UniswapV3ExecutorExposed();
|
||||
}
|
||||
|
||||
function testDecodeParams() public view {
|
||||
uint24 expectedPoolFee = 500;
|
||||
bytes memory data = abi.encodePacked(
|
||||
WETH_ADDR, DAI_ADDR, expectedPoolFee, address(2), address(3), false
|
||||
);
|
||||
|
||||
(
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
uint24 fee,
|
||||
address receiver,
|
||||
address target,
|
||||
bool zeroForOne
|
||||
) = uniswapV3Exposed.decodeData(data);
|
||||
|
||||
assertEq(tokenIn, WETH_ADDR);
|
||||
assertEq(tokenOut, DAI_ADDR);
|
||||
assertEq(fee, expectedPoolFee);
|
||||
assertEq(receiver, address(2));
|
||||
assertEq(target, address(3));
|
||||
assertEq(zeroForOne, false);
|
||||
}
|
||||
|
||||
function testDecodeParamsInvalidDataLength() public {
|
||||
bytes memory invalidParams =
|
||||
abi.encodePacked(WETH_ADDR, address(2), address(3));
|
||||
|
||||
vm.expectRevert(UniswapV3Executor__InvalidDataLength.selector);
|
||||
uniswapV3Exposed.decodeData(invalidParams);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user