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:
TAMARA LIPOWSKI
2025-01-29 19:51:17 -05:00
parent d8b44f623b
commit ca32446a9e
6 changed files with 231 additions and 9 deletions

View File

@@ -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
*/

View File

@@ -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);
}
}

View File

@@ -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
);
}
}

View 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);
}
}