Merge branch 'refs/heads/main' into feat/bebop-rfq-encoder-and-executor

# Conflicts:
#	foundry/test/TychoRouterProtocolIntegration.t.sol
#	foundry/test/TychoRouterTestSetup.sol
#	foundry/test/assets/calldata.txt
#	foundry/test/protocols/BebopExecutor.t.sol
#	src/encoding/evm/tycho_encoders.rs

Took 4 minutes
This commit is contained in:
Diana Carvalho
2025-06-24 10:17:33 +01:00
40 changed files with 3218 additions and 991 deletions

View File

@@ -0,0 +1,137 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;
import "../TestUtils.sol";
import "@src/executors/BalancerV2Executor.sol";
import {Constants} from "../Constants.sol";
contract BalancerV2ExecutorExposed is BalancerV2Executor {
constructor(address _permit2) BalancerV2Executor(_permit2) {}
function decodeParams(bytes calldata data)
external
pure
returns (
IERC20 tokenIn,
IERC20 tokenOut,
bytes32 poolId,
address receiver,
bool needsApproval,
TransferType transferType
)
{
return _decodeData(data);
}
}
contract BalancerV2ExecutorTest is Constants, TestUtils {
using SafeERC20 for IERC20;
BalancerV2ExecutorExposed balancerV2Exposed;
IERC20 WETH = IERC20(WETH_ADDR);
IERC20 BAL = IERC20(BAL_ADDR);
bytes32 constant WETH_BAL_POOL_ID =
0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014;
function setUp() public {
uint256 forkBlock = 17323404;
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
balancerV2Exposed = new BalancerV2ExecutorExposed(PERMIT2_ADDRESS);
}
function testDecodeParams() public view {
bytes memory params = abi.encodePacked(
WETH_ADDR,
BAL_ADDR,
WETH_BAL_POOL_ID,
address(2),
true,
RestrictTransferFrom.TransferType.None
);
(
IERC20 tokenIn,
IERC20 tokenOut,
bytes32 poolId,
address receiver,
bool needsApproval,
RestrictTransferFrom.TransferType transferType
) = balancerV2Exposed.decodeParams(params);
assertEq(address(tokenIn), WETH_ADDR);
assertEq(address(tokenOut), BAL_ADDR);
assertEq(poolId, WETH_BAL_POOL_ID);
assertEq(receiver, address(2));
assertEq(needsApproval, true);
assertEq(
uint8(transferType), uint8(RestrictTransferFrom.TransferType.None)
);
}
function testDecodeParamsInvalidDataLength() public {
bytes memory invalidParams =
abi.encodePacked(WETH_ADDR, BAL_ADDR, WETH_BAL_POOL_ID, address(2));
vm.expectRevert(BalancerV2Executor__InvalidDataLength.selector);
balancerV2Exposed.decodeParams(invalidParams);
}
function testSwap() public {
uint256 amountIn = 10 ** 18;
bytes memory protocolData = abi.encodePacked(
WETH_ADDR,
BAL_ADDR,
WETH_BAL_POOL_ID,
BOB,
true,
RestrictTransferFrom.TransferType.None
);
deal(WETH_ADDR, address(balancerV2Exposed), amountIn);
uint256 balanceBefore = BAL.balanceOf(BOB);
uint256 amountOut = balancerV2Exposed.swap(amountIn, protocolData);
uint256 balanceAfter = BAL.balanceOf(BOB);
assertGt(balanceAfter, balanceBefore);
assertEq(balanceAfter - balanceBefore, amountOut);
}
function testDecodeIntegration() public view {
bytes memory protocolData =
loadCallDataFromFile("test_encode_balancer_v2");
(
IERC20 tokenIn,
IERC20 tokenOut,
bytes32 poolId,
address receiver,
bool needsApproval,
RestrictTransferFrom.TransferType transferType
) = balancerV2Exposed.decodeParams(protocolData);
assertEq(address(tokenIn), WETH_ADDR);
assertEq(address(tokenOut), BAL_ADDR);
assertEq(poolId, WETH_BAL_POOL_ID);
assertEq(receiver, BOB);
assertEq(needsApproval, true);
assertEq(
uint8(transferType), uint8(RestrictTransferFrom.TransferType.None)
);
}
function testSwapIntegration() public {
// Generated by the SwapEncoder - test_encode_balancer_v2
bytes memory protocolData =
loadCallDataFromFile("test_encode_balancer_v2");
uint256 amountIn = 10 ** 18;
deal(WETH_ADDR, address(balancerV2Exposed), amountIn);
uint256 balanceBefore = BAL.balanceOf(BOB);
uint256 amountOut = balancerV2Exposed.swap(amountIn, protocolData);
uint256 balanceAfter = BAL.balanceOf(BOB);
assertGt(balanceAfter, balanceBefore);
assertEq(balanceAfter - balanceBefore, amountOut);
}
}

View File

@@ -0,0 +1,171 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;
import "../TychoRouterTestSetup.sol";
import {BalancerV3Executor__InvalidDataLength} from
"../../src/executors/BalancerV3Executor.sol";
contract BalancerV3ExecutorExposed is BalancerV3Executor {
constructor(address _permit2) BalancerV3Executor(_permit2) {}
function decodeParams(bytes calldata data)
external
pure
returns (
uint256 amountGiven,
IERC20 tokenIn,
IERC20 tokenOut,
address poolId,
TransferType transferType,
address receiver
)
{
return _decodeData(data);
}
}
contract BalancerV3ExecutorTest is Constants, TestUtils {
using SafeERC20 for IERC20;
BalancerV3ExecutorExposed balancerV3Exposed;
address WETH_osETH_pool =
address(0x57c23c58B1D8C3292c15BEcF07c62C5c52457A42);
address osETH_ADDR = address(0xf1C9acDc66974dFB6dEcB12aA385b9cD01190E38);
address waEthWETH_ADDR = address(0x0bfc9d54Fc184518A81162F8fB99c2eACa081202);
function setUp() public {
uint256 forkBlock = 22625131;
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
balancerV3Exposed = new BalancerV3ExecutorExposed(PERMIT2_ADDRESS);
}
function testDecodeParams() public view {
bytes memory params = abi.encodePacked(
uint256(1 ether),
osETH_ADDR,
waEthWETH_ADDR,
WETH_osETH_pool,
RestrictTransferFrom.TransferType.None,
BOB
);
(
uint256 amountGiven,
IERC20 tokenIn,
IERC20 tokenOut,
address poolId,
RestrictTransferFrom.TransferType transferType,
address receiver
) = balancerV3Exposed.decodeParams(params);
assertEq(amountGiven, 1 ether);
assertEq(address(tokenIn), osETH_ADDR);
assertEq(address(tokenOut), waEthWETH_ADDR);
assertEq(poolId, WETH_osETH_pool);
assertEq(
uint8(transferType), uint8(RestrictTransferFrom.TransferType.None)
);
assertEq(receiver, BOB);
}
function testSwapInvalidDataLength() public {
bytes memory invalidParams = abi.encodePacked(
osETH_ADDR,
waEthWETH_ADDR,
WETH_osETH_pool,
RestrictTransferFrom.TransferType.None
);
vm.expectRevert(BalancerV3Executor__InvalidDataLength.selector);
balancerV3Exposed.swap(1 ether, invalidParams);
}
function testSwap() public {
uint256 amountIn = 10 ** 18;
bytes memory protocolData = abi.encodePacked(
osETH_ADDR,
waEthWETH_ADDR,
WETH_osETH_pool,
RestrictTransferFrom.TransferType.Transfer,
BOB
);
deal(osETH_ADDR, address(balancerV3Exposed), amountIn);
uint256 balanceBefore = IERC20(waEthWETH_ADDR).balanceOf(BOB);
uint256 amountOut = balancerV3Exposed.swap(amountIn, protocolData);
uint256 balanceAfter = IERC20(waEthWETH_ADDR).balanceOf(BOB);
assertGt(balanceAfter, balanceBefore);
assertEq(balanceAfter - balanceBefore, amountOut);
}
function testSwapIntegration() public {
bytes memory protocolData =
loadCallDataFromFile("test_encode_balancer_v3");
uint256 amountIn = 10 ** 18;
address waEthUSDT_ADDR =
address(0x7Bc3485026Ac48b6cf9BaF0A377477Fff5703Af8);
address aaveGHO_ADDR =
address(0xC71Ea051a5F82c67ADcF634c36FFE6334793D24C);
deal(waEthUSDT_ADDR, address(balancerV3Exposed), amountIn);
uint256 balanceBefore = IERC20(aaveGHO_ADDR).balanceOf(BOB);
uint256 amountOut = balancerV3Exposed.swap(amountIn, protocolData);
uint256 balanceAfter = IERC20(aaveGHO_ADDR).balanceOf(BOB);
assertGt(balanceAfter, balanceBefore);
assertEq(balanceAfter - balanceBefore, amountOut);
}
}
contract TychoRouterForBalancerV3Test is TychoRouterTestSetup {
function getForkBlock() public pure override returns (uint256) {
return 22644371;
}
function testSingleBalancerV3Integration() public {
address steakUSDTlite =
address(0x097FFEDb80d4b2Ca6105a07a4D90eB739C45A666);
address steakUSDR = address(0x30881Baa943777f92DC934d53D3bFdF33382cab3);
deal(steakUSDTlite, ALICE, 1 ether);
uint256 balanceBefore = IERC20(steakUSDTlite).balanceOf(ALICE);
vm.startPrank(ALICE);
IERC20(steakUSDTlite).approve(tychoRouterAddr, type(uint256).max);
bytes memory callData =
loadCallDataFromFile("test_single_encoding_strategy_balancer_v3");
(bool success,) = tychoRouterAddr.call(callData);
uint256 balanceAfter = IERC20(steakUSDR).balanceOf(ALICE);
assertTrue(success, "Call Failed");
assertGe(balanceAfter - balanceBefore, 999725);
assertEq(IERC20(steakUSDR).balanceOf(tychoRouterAddr), 0);
}
function testUSV3BalancerV3Integration() public {
// It tests if we can optimize the in transfer to balancer v3 (we can not)
// WETH ───(USV3)──> WBTC ───(balancer v3)──> QNT
address QNT_ADDR = address(0x4a220E6096B25EADb88358cb44068A3248254675);
deal(WETH_ADDR, ALICE, 0.01 ether);
uint256 balanceBefore = IERC20(QNT_ADDR).balanceOf(ALICE);
vm.startPrank(ALICE);
IERC20(WETH_ADDR).approve(tychoRouterAddr, type(uint256).max);
bytes memory callData =
loadCallDataFromFile("test_uniswap_v3_balancer_v3");
(bool success,) = tychoRouterAddr.call(callData);
vm.stopPrank();
uint256 balanceAfter = IERC20(QNT_ADDR).balanceOf(ALICE);
assertTrue(success, "Call Failed");
assertEq(balanceAfter - balanceBefore, 219116541871727003);
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,413 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;
import "../TychoRouterTestSetup.sol";
import "@src/executors/CurveExecutor.sol";
import {Constants} from "../Constants.sol";
import {Test} from "../../lib/forge-std/src/Test.sol";
interface ICurvePool {
function coins(uint256 i) external view returns (address);
}
// Curve pool registry
// This is the registry that contains the information about the pool
// The naming convention is different because it is in vyper
interface MetaRegistry {
function get_n_coins(address pool) external view returns (uint256);
function get_coin_indices(address pool, address from, address to)
external
view
returns (int128, int128, bool);
}
contract CurveExecutorExposed is CurveExecutor {
constructor(address _nativeToken, address _permit2)
CurveExecutor(_nativeToken, _permit2)
{}
function decodeData(bytes calldata data)
external
pure
returns (
address tokenIn,
address tokenOut,
address pool,
uint8 poolType,
int128 i,
int128 j,
bool tokenApprovalNeeded,
RestrictTransferFrom.TransferType transferType,
address receiver
)
{
return _decodeData(data);
}
}
contract CurveExecutorTest is Test, Constants {
using SafeERC20 for IERC20;
CurveExecutorExposed curveExecutorExposed;
MetaRegistry metaRegistry;
function setUp() public {
uint256 forkBlock = 22031795;
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
curveExecutorExposed =
new CurveExecutorExposed(ETH_ADDR_FOR_CURVE, PERMIT2_ADDRESS);
metaRegistry = MetaRegistry(CURVE_META_REGISTRY);
}
function testDecodeParams() public view {
bytes memory data = abi.encodePacked(
WETH_ADDR,
USDC_ADDR,
TRICRYPTO_POOL,
uint8(3),
uint8(2),
uint8(0),
true,
RestrictTransferFrom.TransferType.None,
ALICE
);
(
address tokenIn,
address tokenOut,
address pool,
uint8 poolType,
int128 i,
int128 j,
bool tokenApprovalNeeded,
RestrictTransferFrom.TransferType transferType,
address receiver
) = curveExecutorExposed.decodeData(data);
assertEq(tokenIn, WETH_ADDR);
assertEq(tokenOut, USDC_ADDR);
assertEq(pool, TRICRYPTO_POOL);
assertEq(poolType, 3);
assertEq(i, 2);
assertEq(j, 0);
assertEq(tokenApprovalNeeded, true);
assertEq(
uint8(transferType), uint8(RestrictTransferFrom.TransferType.None)
);
assertEq(receiver, ALICE);
}
function testTriPool() public {
// Swapping DAI -> USDC on TriPool 0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7
uint256 amountIn = 1 ether;
deal(DAI_ADDR, address(curveExecutorExposed), amountIn);
bytes memory data = _getData(
DAI_ADDR,
USDC_ADDR,
TRIPOOL,
1,
ALICE,
RestrictTransferFrom.TransferType.None
);
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 999797);
assertEq(IERC20(USDC_ADDR).balanceOf(ALICE), amountOut);
}
function testStEthPool() public {
// Swapping ETH -> stETH on StEthPool 0xDC24316b9AE028F1497c275EB9192a3Ea0f67022
uint256 amountIn = 1 ether;
deal(address(curveExecutorExposed), amountIn);
bytes memory data = _getData(
ETH_ADDR_FOR_CURVE,
STETH_ADDR,
STETH_POOL,
1,
ALICE,
RestrictTransferFrom.TransferType.None
);
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 1001072414418410897);
assertEq(
IERC20(STETH_ADDR).balanceOf(ALICE),
amountOut - 1 // there is something weird in this pool, but won't investigate for now because we don't currently support it in the simulation
);
}
function testTricrypto2Pool() public {
// Swapping WETH -> WBTC on Tricrypto2Pool 0xD51a44d3FaE010294C616388b506AcdA1bfAAE46
uint256 amountIn = 1 ether;
deal(WETH_ADDR, address(curveExecutorExposed), amountIn);
bytes memory data = _getData(
WETH_ADDR,
WBTC_ADDR,
TRICRYPTO2_POOL,
3,
ALICE,
RestrictTransferFrom.TransferType.None
);
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 2279618);
assertEq(IERC20(WBTC_ADDR).balanceOf(ALICE), amountOut);
}
function testSUSDPool() public {
// Swapping USDC -> SUSD on SUSDPool 0xA5407eAE9Ba41422680e2e00537571bcC53efBfD
uint256 amountIn = 100 * 10 ** 6;
deal(USDC_ADDR, address(curveExecutorExposed), amountIn);
bytes memory data = _getData(
USDC_ADDR,
SUSD_ADDR,
SUSD_POOL,
1,
ALICE,
RestrictTransferFrom.TransferType.None
);
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 100488101605550214590);
assertEq(IERC20(SUSD_ADDR).balanceOf(ALICE), amountOut);
}
function testFraxUsdcPool() public {
// Swapping FRAX -> USDC on FraxUsdcPool 0xDcEF968d416a41Cdac0ED8702fAC8128A64241A2
uint256 amountIn = 1 ether;
deal(FRAX_ADDR, address(curveExecutorExposed), amountIn);
bytes memory data = _getData(
FRAX_ADDR,
USDC_ADDR,
FRAX_USDC_POOL,
1,
ALICE,
RestrictTransferFrom.TransferType.None
);
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 998097);
assertEq(IERC20(USDC_ADDR).balanceOf(ALICE), amountOut);
}
function testUsdeUsdcPool() public {
// Swapping USDC -> USDE on a CryptoSwapNG, deployed by factory 0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf (plain pool)
uint256 amountIn = 100 * 10 ** 6;
deal(USDC_ADDR, address(curveExecutorExposed), amountIn);
bytes memory data = _getData(
USDC_ADDR,
USDE_ADDR,
USDE_USDC_POOL,
1,
ALICE,
RestrictTransferFrom.TransferType.None
);
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 100064812138999986170);
assertEq(IERC20(USDE_ADDR).balanceOf(ALICE), amountOut);
}
function testDolaFraxPyusdPool() public {
// Swapping DOLA -> FRAXPYUSD on a CryptoSwapNG, deployed by factory 0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf (meta pool)
uint256 amountIn = 100 * 10 ** 6;
deal(DOLA_ADDR, address(curveExecutorExposed), amountIn);
bytes memory data = _getData(
DOLA_ADDR,
FRAXPYUSD_POOL,
DOLA_FRAXPYUSD_POOL,
1,
ALICE,
RestrictTransferFrom.TransferType.None
);
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 99688992);
assertEq(IERC20(FRAXPYUSD_POOL).balanceOf(ALICE), amountOut);
}
function testCryptoPoolWithETH() public {
// Swapping XYO -> ETH on a CryptoPool, deployed by factory 0xF18056Bbd320E96A48e3Fbf8bC061322531aac99
uint256 amountIn = 1 ether;
uint256 initialBalance = address(ALICE).balance; // this address already has some ETH assigned to it
deal(XYO_ADDR, address(curveExecutorExposed), amountIn);
bytes memory data = _getData(
XYO_ADDR,
ETH_ADDR_FOR_CURVE,
ETH_XYO_POOL,
2,
ALICE,
RestrictTransferFrom.TransferType.None
);
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 6081816039338);
assertEq(ALICE.balance, initialBalance + amountOut);
}
function testCryptoPool() public {
// Swapping BSGG -> USDT on a CryptoPool, deployed by factory 0xF18056Bbd320E96A48e3Fbf8bC061322531aac99
uint256 amountIn = 1000 ether;
deal(BSGG_ADDR, address(curveExecutorExposed), amountIn);
bytes memory data = _getData(
BSGG_ADDR,
USDT_ADDR,
BSGG_USDT_POOL,
2,
ALICE,
RestrictTransferFrom.TransferType.None
);
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 23429);
assertEq(IERC20(USDT_ADDR).balanceOf(ALICE), amountOut);
}
function testTricryptoPool() public {
// Swapping WETH -> USDC on a Tricrypto pool, deployed by factory 0x0c0e5f2fF0ff18a3be9b835635039256dC4B4963
uint256 amountIn = 1 ether;
deal(WETH_ADDR, address(curveExecutorExposed), amountIn);
bytes memory data = _getData(
WETH_ADDR,
USDC_ADDR,
TRICRYPTO_POOL,
2,
ALICE,
RestrictTransferFrom.TransferType.None
);
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 1861130974);
assertEq(IERC20(USDC_ADDR).balanceOf(ALICE), amountOut);
}
function testTwoCryptoPool() public {
// Swapping UWU -> WETH on a Twocrypto pool, deployed by factory 0x98ee851a00abee0d95d08cf4ca2bdce32aeaaf7f
uint256 amountIn = 1 ether;
deal(UWU_ADDR, address(curveExecutorExposed), amountIn);
bytes memory data = _getData(
UWU_ADDR,
WETH_ADDR,
UWU_WETH_POOL,
2,
ALICE,
RestrictTransferFrom.TransferType.None
);
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 2873786684675);
assertEq(IERC20(WETH_ADDR).balanceOf(ALICE), amountOut);
}
function testStableSwapPool() public {
// Swapping CRVUSD -> USDT on a StableSwap pool, deployed by factory 0x4F8846Ae9380B90d2E71D5e3D042dff3E7ebb40d (plain pool)
uint256 amountIn = 1 ether;
deal(USDT_ADDR, address(curveExecutorExposed), amountIn);
bytes memory data = _getData(
USDT_ADDR,
CRVUSD_ADDR,
CRVUSD_USDT_POOL,
1,
ALICE,
RestrictTransferFrom.TransferType.None
);
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 10436946786333182306400100);
assertEq(IERC20(CRVUSD_ADDR).balanceOf(ALICE), amountOut);
}
function testMetaPool() public {
// Swapping WTAO -> WSTTAO on a MetaPool deployed by factory 0xB9fC157394Af804a3578134A6585C0dc9cc990d4 (plain pool)
uint256 amountIn = 100 * 10 ** 9; // 9 decimals
deal(WTAO_ADDR, address(curveExecutorExposed), amountIn);
bytes memory data = _getData(
WTAO_ADDR,
WSTTAO_ADDR,
WSTTAO_WTAO_POOL,
1,
ALICE,
RestrictTransferFrom.TransferType.None
);
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 32797923610);
assertEq(IERC20(WSTTAO_ADDR).balanceOf(ALICE), amountOut);
}
function _getData(
address tokenIn,
address tokenOut,
address pool,
uint8 poolType,
address receiver,
RestrictTransferFrom.TransferType transferType
) internal view returns (bytes memory data) {
(int128 i, int128 j) = _getIndexes(tokenIn, tokenOut, pool);
data = abi.encodePacked(
tokenIn,
tokenOut,
pool,
poolType,
uint8(uint256(uint128(i))),
uint8(uint256(uint128(j))),
true,
transferType,
receiver
);
}
function _getIndexes(address tokenIn, address tokenOut, address pool)
internal
view
returns (int128, int128)
{
(int128 coinInIndex, int128 coinOutIndex,) =
metaRegistry.get_coin_indices(pool, tokenIn, tokenOut);
return (coinInIndex, coinOutIndex);
}
}
contract TychoRouterForBalancerV3Test is TychoRouterTestSetup {
function testSingleCurveIntegration() public {
deal(UWU_ADDR, ALICE, 1 ether);
vm.startPrank(ALICE);
IERC20(UWU_ADDR).approve(tychoRouterAddr, type(uint256).max);
bytes memory callData =
loadCallDataFromFile("test_single_encoding_strategy_curve");
(bool success,) = tychoRouterAddr.call(callData);
assertTrue(success, "Call Failed");
assertEq(IERC20(WETH_ADDR).balanceOf(ALICE), 2877855391767);
vm.stopPrank();
}
}

View File

@@ -0,0 +1,182 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;
import "../TestUtils.sol";
import "../TychoRouterTestSetup.sol";
import "@src/executors/EkuboExecutor.sol";
import {Constants} from "../Constants.sol";
import {ICore} from "@ekubo/interfaces/ICore.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {NATIVE_TOKEN_ADDRESS} from "@ekubo/math/constants.sol";
import {console} from "forge-std/Test.sol";
contract EkuboExecutorTest is Constants, TestUtils {
address constant EXECUTOR_ADDRESS =
0xcA4F73Fe97D0B987a0D12B39BBD562c779BAb6f6; // Same address as in swap_encoder.rs tests
EkuboExecutor executor;
IERC20 USDC = IERC20(USDC_ADDR);
IERC20 USDT = IERC20(USDT_ADDR);
address constant CORE_ADDRESS = 0xe0e0e08A6A4b9Dc7bD67BCB7aadE5cF48157d444;
bytes32 constant ORACLE_CONFIG =
0x51d02a5948496a67827242eabc5725531342527c000000000000000000000000;
function setUp() public {
vm.createSelectFork(vm.rpcUrl("mainnet"), 22082754);
deployCodeTo(
"executors/EkuboExecutor.sol",
abi.encode(CORE_ADDRESS, PERMIT2_ADDRESS),
EXECUTOR_ADDRESS
);
executor = EkuboExecutor(payable(EXECUTOR_ADDRESS));
}
function testSingleSwapEth() public {
uint256 amountIn = 1 ether;
deal(address(executor), amountIn);
uint256 ethBalanceBeforeCore = CORE_ADDRESS.balance;
uint256 ethBalanceBeforeExecutor = address(executor).balance;
uint256 usdcBalanceBeforeCore = USDC.balanceOf(CORE_ADDRESS);
uint256 usdcBalanceBeforeExecutor = USDC.balanceOf(address(executor));
bytes memory data = abi.encodePacked(
uint8(RestrictTransferFrom.TransferType.Transfer), // transfer type (transfer from executor to core)
address(executor), // receiver
NATIVE_TOKEN_ADDRESS, // tokenIn
USDC_ADDR, // tokenOut
ORACLE_CONFIG // poolConfig
);
uint256 gasBefore = gasleft();
uint256 amountOut = executor.swap(amountIn, data);
console.log(gasBefore - gasleft());
console.log(amountOut);
assertEq(CORE_ADDRESS.balance, ethBalanceBeforeCore + amountIn);
assertEq(address(executor).balance, ethBalanceBeforeExecutor - amountIn);
assertEq(
USDC.balanceOf(CORE_ADDRESS), usdcBalanceBeforeCore - amountOut
);
assertEq(
USDC.balanceOf(address(executor)),
usdcBalanceBeforeExecutor + amountOut
);
}
function testSingleSwapERC20() public {
uint256 amountIn = 1_000_000_000;
deal(USDC_ADDR, address(executor), amountIn);
uint256 usdcBalanceBeforeCore = USDC.balanceOf(CORE_ADDRESS);
uint256 usdcBalanceBeforeExecutor = USDC.balanceOf(address(executor));
uint256 ethBalanceBeforeCore = CORE_ADDRESS.balance;
uint256 ethBalanceBeforeExecutor = address(executor).balance;
bytes memory data = abi.encodePacked(
uint8(RestrictTransferFrom.TransferType.Transfer), // transferNeeded (transfer from executor to core)
address(executor), // receiver
USDC_ADDR, // tokenIn
NATIVE_TOKEN_ADDRESS, // tokenOut
ORACLE_CONFIG // config
);
uint256 gasBefore = gasleft();
uint256 amountOut = executor.swap(amountIn, data);
console.log(gasBefore - gasleft());
console.log(amountOut);
assertEq(USDC.balanceOf(CORE_ADDRESS), usdcBalanceBeforeCore + amountIn);
assertEq(
USDC.balanceOf(address(executor)),
usdcBalanceBeforeExecutor - amountIn
);
assertEq(CORE_ADDRESS.balance, ethBalanceBeforeCore - amountOut);
assertEq(
address(executor).balance, ethBalanceBeforeExecutor + amountOut
);
}
// Expects input that encodes the same test case as swap_encoder::tests::ekubo::test_encode_swap_multi
function multiHopSwap(bytes memory data) internal {
uint256 amountIn = 1 ether;
deal(address(executor), amountIn);
uint256 ethBalanceBeforeCore = CORE_ADDRESS.balance;
uint256 ethBalanceBeforeExecutor = address(executor).balance;
uint256 usdtBalanceBeforeCore = USDT.balanceOf(CORE_ADDRESS);
uint256 usdtBalanceBeforeExecutor = USDT.balanceOf(address(executor));
uint256 gasBefore = gasleft();
uint256 amountOut = executor.swap(amountIn, data);
console.log(gasBefore - gasleft());
console.log(amountOut);
assertEq(CORE_ADDRESS.balance, ethBalanceBeforeCore + amountIn);
assertEq(address(executor).balance, ethBalanceBeforeExecutor - amountIn);
assertEq(
USDT.balanceOf(CORE_ADDRESS), usdtBalanceBeforeCore - amountOut
);
assertEq(
USDT.balanceOf(address(executor)),
usdtBalanceBeforeExecutor + amountOut
);
}
// Same test case as in swap_encoder::tests::ekubo::test_encode_swap_multi
function testMultiHopSwap() public {
bytes memory data = abi.encodePacked(
uint8(RestrictTransferFrom.TransferType.Transfer), // transferNeeded (transfer from executor to core)
address(executor), // receiver
NATIVE_TOKEN_ADDRESS, // tokenIn
USDC_ADDR, // tokenOut of 1st swap
ORACLE_CONFIG, // config of 1st swap
USDT_ADDR, // tokenOut of 2nd swap
bytes32(
0x00000000000000000000000000000000000000000001a36e2eb1c43200000032
) // config of 2nd swap (0.0025% fee & 0.005% base pool)
);
multiHopSwap(data);
}
// Data is generated by test case in swap_encoder::tests::ekubo::test_encode_swap_multi
function testMultiHopSwapIntegration() public {
multiHopSwap(loadCallDataFromFile("test_ekubo_encode_swap_multi"));
}
}
contract TychoRouterForBalancerV3Test is TychoRouterTestSetup {
function testSingleEkuboIntegration() public {
vm.stopPrank();
deal(ALICE, 1 ether);
uint256 balanceBefore = IERC20(USDC_ADDR).balanceOf(ALICE);
// Approve permit2
vm.startPrank(ALICE);
bytes memory callData =
loadCallDataFromFile("test_single_encoding_strategy_ekubo");
(bool success,) = tychoRouterAddr.call{value: 1 ether}(callData);
uint256 balanceAfter = IERC20(USDC_ADDR).balanceOf(ALICE);
assertTrue(success, "Call Failed");
assertGe(balanceAfter - balanceBefore, 26173932);
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
}
}

View File

@@ -0,0 +1,149 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;
import "../TestUtils.sol";
import "../TychoRouterTestSetup.sol";
import "@src/executors/MaverickV2Executor.sol";
import {Constants} from "../Constants.sol";
contract MaverickV2ExecutorExposed is MaverickV2Executor {
constructor(address _factory, address _permit2)
MaverickV2Executor(_factory, _permit2)
{}
function decodeParams(bytes calldata data)
external
pure
returns (
IERC20 tokenIn,
address target,
address receiver,
RestrictTransferFrom.TransferType transferType
)
{
return _decodeData(data);
}
}
contract MaverickV2ExecutorTest is TestUtils, Constants {
using SafeERC20 for IERC20;
MaverickV2ExecutorExposed maverickV2Exposed;
IERC20 GHO = IERC20(GHO_ADDR);
IERC20 USDC = IERC20(USDC_ADDR);
function setUp() public {
uint256 forkBlock = 22096000;
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
maverickV2Exposed =
new MaverickV2ExecutorExposed(MAVERICK_V2_FACTORY, PERMIT2_ADDRESS);
}
function testDecodeParams() public view {
bytes memory params = abi.encodePacked(
GHO_ADDR,
GHO_USDC_POOL,
address(2),
RestrictTransferFrom.TransferType.Transfer
);
(
IERC20 tokenIn,
address target,
address receiver,
RestrictTransferFrom.TransferType transferType
) = maverickV2Exposed.decodeParams(params);
assertEq(address(tokenIn), GHO_ADDR);
assertEq(target, GHO_USDC_POOL);
assertEq(receiver, address(2));
assertEq(
uint8(transferType),
uint8(RestrictTransferFrom.TransferType.Transfer)
);
}
function testDecodeParamsInvalidDataLength() public {
bytes memory invalidParams =
abi.encodePacked(GHO_ADDR, GHO_USDC_POOL, address(2));
vm.expectRevert(MaverickV2Executor__InvalidDataLength.selector);
maverickV2Exposed.decodeParams(invalidParams);
}
function testSwap() public {
uint256 amountIn = 10e18;
bytes memory protocolData = abi.encodePacked(
GHO_ADDR,
GHO_USDC_POOL,
BOB,
RestrictTransferFrom.TransferType.Transfer
);
deal(GHO_ADDR, address(maverickV2Exposed), amountIn);
uint256 balanceBefore = USDC.balanceOf(BOB);
uint256 amountOut = maverickV2Exposed.swap(amountIn, protocolData);
uint256 balanceAfter = USDC.balanceOf(BOB);
assertGt(balanceAfter, balanceBefore);
assertEq(balanceAfter - balanceBefore, amountOut);
}
function testDecodeIntegration() public view {
// Generated by the SwapEncoder - test_encode_maverick_v2
bytes memory protocolData =
loadCallDataFromFile("test_encode_maverick_v2");
(
IERC20 tokenIn,
address pool,
address receiver,
RestrictTransferFrom.TransferType transferType
) = maverickV2Exposed.decodeParams(protocolData);
assertEq(address(tokenIn), GHO_ADDR);
assertEq(pool, GHO_USDC_POOL);
assertEq(receiver, BOB);
assertEq(
uint8(transferType),
uint8(RestrictTransferFrom.TransferType.Transfer)
);
}
function testSwapIntegration() public {
// Generated by the SwapEncoder - test_encode_maverick_v2
bytes memory protocolData =
loadCallDataFromFile("test_encode_maverick_v2");
uint256 amountIn = 10 ** 18;
deal(GHO_ADDR, address(maverickV2Exposed), amountIn);
uint256 balanceBefore = USDC.balanceOf(BOB);
uint256 amountOut = maverickV2Exposed.swap(amountIn, protocolData);
uint256 balanceAfter = USDC.balanceOf(BOB);
assertGt(balanceAfter, balanceBefore);
assertEq(balanceAfter - balanceBefore, amountOut);
}
}
contract TychoRouterForBalancerV3Test is TychoRouterTestSetup {
function testSingleMaverickIntegration() public {
deal(GHO_ADDR, ALICE, 1 ether);
uint256 balanceBefore = IERC20(USDC_ADDR).balanceOf(ALICE);
vm.startPrank(ALICE);
IERC20(GHO_ADDR).approve(tychoRouterAddr, type(uint256).max);
bytes memory callData =
loadCallDataFromFile("test_single_encoding_strategy_maverick");
(bool success,) = tychoRouterAddr.call(callData);
uint256 balanceAfter = IERC20(USDC_ADDR).balanceOf(ALICE);
assertTrue(success, "Call Failed");
assertGe(balanceAfter - balanceBefore, 999725);
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
}
}

View File

@@ -0,0 +1,266 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;
import "../TestUtils.sol";
import "@src/executors/UniswapV2Executor.sol";
import {Constants} from "../Constants.sol";
import {Permit2TestHelper} from "../Permit2TestHelper.sol";
import {Test} from "../../lib/forge-std/src/Test.sol";
contract UniswapV2ExecutorExposed is UniswapV2Executor {
constructor(
address _factory,
bytes32 _initCode,
address _permit2,
uint256 _feeBps
) UniswapV2Executor(_factory, _initCode, _permit2, _feeBps) {}
function decodeParams(bytes calldata data)
external
pure
returns (
IERC20 inToken,
address target,
address receiver,
bool zeroForOne,
RestrictTransferFrom.TransferType transferType
)
{
return _decodeData(data);
}
function getAmountOut(address target, uint256 amountIn, bool zeroForOne)
external
view
returns (uint256 amount)
{
return _getAmountOut(target, amountIn, zeroForOne);
}
function verifyPairAddress(address target) external view {
_verifyPairAddress(target);
}
}
contract FakeUniswapV2Pool {
address public token0;
address public token1;
constructor(address _tokenA, address _tokenB) {
token0 = _tokenA < _tokenB ? _tokenA : _tokenB;
token1 = _tokenA < _tokenB ? _tokenB : _tokenA;
}
}
contract UniswapV2ExecutorTest is Constants, Permit2TestHelper, TestUtils {
using SafeERC20 for IERC20;
UniswapV2ExecutorExposed uniswapV2Exposed;
UniswapV2ExecutorExposed sushiswapV2Exposed;
UniswapV2ExecutorExposed pancakeswapV2Exposed;
IERC20 WETH = IERC20(WETH_ADDR);
IERC20 DAI = IERC20(DAI_ADDR);
function setUp() public {
uint256 forkBlock = 17323404;
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
uniswapV2Exposed = new UniswapV2ExecutorExposed(
USV2_FACTORY_ETHEREUM, USV2_POOL_CODE_INIT_HASH, PERMIT2_ADDRESS, 30
);
sushiswapV2Exposed = new UniswapV2ExecutorExposed(
SUSHISWAPV2_FACTORY_ETHEREUM,
SUSHIV2_POOL_CODE_INIT_HASH,
PERMIT2_ADDRESS,
30
);
pancakeswapV2Exposed = new UniswapV2ExecutorExposed(
PANCAKESWAPV2_FACTORY_ETHEREUM,
PANCAKEV2_POOL_CODE_INIT_HASH,
PERMIT2_ADDRESS,
25
);
}
function testDecodeParams() public view {
bytes memory params = abi.encodePacked(
WETH_ADDR,
address(2),
address(3),
false,
RestrictTransferFrom.TransferType.Transfer
);
(
IERC20 tokenIn,
address target,
address receiver,
bool zeroForOne,
RestrictTransferFrom.TransferType transferType
) = uniswapV2Exposed.decodeParams(params);
assertEq(address(tokenIn), WETH_ADDR);
assertEq(target, address(2));
assertEq(receiver, address(3));
assertEq(zeroForOne, false);
assertEq(
uint8(transferType),
uint8(RestrictTransferFrom.TransferType.Transfer)
);
}
function testDecodeParamsInvalidDataLength() public {
bytes memory invalidParams =
abi.encodePacked(WETH_ADDR, address(2), address(3));
vm.expectRevert(UniswapV2Executor__InvalidDataLength.selector);
uniswapV2Exposed.decodeParams(invalidParams);
}
function testVerifyPairAddress() public view {
uniswapV2Exposed.verifyPairAddress(WETH_DAI_POOL);
}
function testVerifyPairAddressSushi() public view {
sushiswapV2Exposed.verifyPairAddress(SUSHISWAP_WBTC_WETH_POOL);
}
function testVerifyPairAddressPancake() public view {
pancakeswapV2Exposed.verifyPairAddress(PANCAKESWAP_WBTC_WETH_POOL);
}
function testInvalidTarget() public {
address fakePool = address(new FakeUniswapV2Pool(WETH_ADDR, DAI_ADDR));
vm.expectRevert(UniswapV2Executor__InvalidTarget.selector);
uniswapV2Exposed.verifyPairAddress(fakePool);
}
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 testSwapWithTransfer() public {
uint256 amountIn = 10 ** 18;
uint256 amountOut = 1847751195973566072891;
bool zeroForOne = false;
bytes memory protocolData = abi.encodePacked(
WETH_ADDR,
WETH_DAI_POOL,
BOB,
zeroForOne,
RestrictTransferFrom.TransferType.Transfer
);
deal(WETH_ADDR, address(uniswapV2Exposed), amountIn);
uniswapV2Exposed.swap(amountIn, protocolData);
uint256 finalBalance = DAI.balanceOf(BOB);
assertGe(finalBalance, amountOut);
}
function testSwapNoTransfer() public {
uint256 amountIn = 10 ** 18;
uint256 amountOut = 1847751195973566072891;
bool zeroForOne = false;
bytes memory protocolData = abi.encodePacked(
WETH_ADDR,
WETH_DAI_POOL,
BOB,
zeroForOne,
RestrictTransferFrom.TransferType.None
);
deal(WETH_ADDR, address(this), amountIn);
IERC20(WETH_ADDR).transfer(address(WETH_DAI_POOL), amountIn);
uniswapV2Exposed.swap(amountIn, protocolData);
uint256 finalBalance = DAI.balanceOf(BOB);
assertGe(finalBalance, amountOut);
}
function testDecodeIntegration() public view {
bytes memory protocolData =
hex"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc288e6a0c2ddd26feeb64f039a2c41296fcb3f564000000000000000000000000000000000000000010001";
(
IERC20 tokenIn,
address target,
address receiver,
bool zeroForOne,
RestrictTransferFrom.TransferType transferType
) = uniswapV2Exposed.decodeParams(protocolData);
assertEq(address(tokenIn), WETH_ADDR);
assertEq(target, 0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640);
assertEq(receiver, 0x0000000000000000000000000000000000000001);
assertEq(zeroForOne, false);
assertEq(
uint8(transferType),
uint8(RestrictTransferFrom.TransferType.Transfer)
);
}
function testSwapIntegration() public {
bytes memory protocolData =
loadCallDataFromFile("test_encode_uniswap_v2");
uint256 amountIn = 10 ** 18;
uint256 amountOut = 1847751195973566072891;
deal(WETH_ADDR, address(uniswapV2Exposed), amountIn);
uniswapV2Exposed.swap(amountIn, protocolData);
uint256 finalBalance = DAI.balanceOf(BOB);
assertGe(finalBalance, amountOut);
}
function testSwapFailureInvalidTarget() public {
uint256 amountIn = 10 ** 18;
bool zeroForOne = false;
address fakePool = address(new FakeUniswapV2Pool(WETH_ADDR, DAI_ADDR));
bytes memory protocolData = abi.encodePacked(
WETH_ADDR,
fakePool,
BOB,
zeroForOne,
RestrictTransferFrom.TransferType.Transfer
);
deal(WETH_ADDR, address(uniswapV2Exposed), amountIn);
vm.expectRevert(UniswapV2Executor__InvalidTarget.selector);
uniswapV2Exposed.swap(amountIn, protocolData);
}
// Base Network Tests
// Make sure to set the RPC_URL to base network
function testSwapBaseNetwork() public {
vm.skip(true);
vm.rollFork(26857267);
uint256 amountIn = 10 * 10 ** 6;
bool zeroForOne = true;
bytes memory protocolData = abi.encodePacked(
BASE_USDC,
USDC_MAG7_POOL,
BOB,
zeroForOne,
RestrictTransferFrom.TransferType.Transfer
);
deal(BASE_USDC, address(uniswapV2Exposed), amountIn);
uniswapV2Exposed.swap(amountIn, protocolData);
assertEq(IERC20(BASE_MAG7).balanceOf(BOB), 1379830606);
}
}

View File

@@ -0,0 +1,260 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;
import "../TychoRouterTestSetup.sol";
import "@permit2/src/interfaces/IAllowanceTransfer.sol";
import "@src/executors/UniswapV3Executor.sol";
import {Constants} from "../Constants.sol";
import {Permit2TestHelper} from "../Permit2TestHelper.sol";
import {Test} from "../../lib/forge-std/src/Test.sol";
contract UniswapV3ExecutorExposed is UniswapV3Executor {
constructor(address _factory, bytes32 _initCode, address _permit2)
UniswapV3Executor(_factory, _initCode, _permit2)
{}
function decodeData(bytes calldata data)
external
pure
returns (
address inToken,
address outToken,
uint24 fee,
address receiver,
address target,
bool zeroForOne,
RestrictTransferFrom.TransferType transferType
)
{
return _decodeData(data);
}
function verifyPairAddress(
address tokenA,
address tokenB,
uint24 fee,
address target
) external view {
_verifyPairAddress(tokenA, tokenB, fee, target);
}
}
contract UniswapV3ExecutorTest is Test, Constants, Permit2TestHelper {
using SafeERC20 for IERC20;
UniswapV3ExecutorExposed uniswapV3Exposed;
UniswapV3ExecutorExposed pancakeV3Exposed;
IERC20 DAI = IERC20(DAI_ADDR);
IAllowanceTransfer permit2;
function setUp() public {
uint256 forkBlock = 17323404;
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
uniswapV3Exposed = new UniswapV3ExecutorExposed(
USV3_FACTORY_ETHEREUM, USV3_POOL_CODE_INIT_HASH, PERMIT2_ADDRESS
);
pancakeV3Exposed = new UniswapV3ExecutorExposed(
PANCAKESWAPV3_DEPLOYER_ETHEREUM,
PANCAKEV3_POOL_CODE_INIT_HASH,
PERMIT2_ADDRESS
);
permit2 = IAllowanceTransfer(PERMIT2_ADDRESS);
}
function testDecodeParams() public view {
uint24 expectedPoolFee = 500;
bytes memory data = abi.encodePacked(
WETH_ADDR,
DAI_ADDR,
expectedPoolFee,
address(2),
address(3),
false,
RestrictTransferFrom.TransferType.Transfer
);
(
address tokenIn,
address tokenOut,
uint24 fee,
address receiver,
address target,
bool zeroForOne,
RestrictTransferFrom.TransferType transferType
) = 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);
assertEq(
uint8(transferType),
uint8(RestrictTransferFrom.TransferType.Transfer)
);
}
function testSwapIntegration() public {
uint256 amountIn = 10 ** 18;
deal(WETH_ADDR, address(uniswapV3Exposed), amountIn);
uint256 expAmountOut = 1205_128428842122129186; //Swap 1 WETH for 1205.12 DAI
bool zeroForOne = false;
bytes memory data = encodeUniswapV3Swap(
WETH_ADDR,
DAI_ADDR,
address(this),
DAI_WETH_USV3,
zeroForOne,
RestrictTransferFrom.TransferType.Transfer
);
uint256 amountOut = uniswapV3Exposed.swap(amountIn, data);
assertGe(amountOut, expAmountOut);
assertEq(IERC20(WETH_ADDR).balanceOf(address(uniswapV3Exposed)), 0);
assertGe(IERC20(DAI_ADDR).balanceOf(address(this)), expAmountOut);
}
function testDecodeParamsInvalidDataLength() public {
bytes memory invalidParams =
abi.encodePacked(WETH_ADDR, address(2), address(3));
vm.expectRevert(UniswapV3Executor__InvalidDataLength.selector);
uniswapV3Exposed.decodeData(invalidParams);
}
function testVerifyPairAddress() public view {
uniswapV3Exposed.verifyPairAddress(
WETH_ADDR, DAI_ADDR, 3000, DAI_WETH_USV3
);
}
function testVerifyPairAddressPancake() public view {
pancakeV3Exposed.verifyPairAddress(
WETH_ADDR, USDT_ADDR, 500, PANCAKESWAPV3_WETH_USDT_POOL
);
}
function testUSV3Callback() public {
uint24 poolFee = 3000;
uint256 amountOwed = 1000000000000000000;
deal(WETH_ADDR, address(uniswapV3Exposed), amountOwed);
uint256 initialPoolReserve = IERC20(WETH_ADDR).balanceOf(DAI_WETH_USV3);
vm.startPrank(DAI_WETH_USV3);
bytes memory protocolData = abi.encodePacked(
WETH_ADDR,
DAI_ADDR,
poolFee,
RestrictTransferFrom.TransferType.Transfer,
address(uniswapV3Exposed)
);
uint256 dataOffset = 3; // some offset
uint256 dataLength = protocolData.length;
bytes memory callbackData = abi.encodePacked(
bytes4(0xfa461e33),
int256(amountOwed), // amount0Delta
int256(0), // amount1Delta
dataOffset,
dataLength,
protocolData
);
uniswapV3Exposed.handleCallback(callbackData);
vm.stopPrank();
uint256 finalPoolReserve = IERC20(WETH_ADDR).balanceOf(DAI_WETH_USV3);
assertEq(finalPoolReserve - initialPoolReserve, amountOwed);
}
function testSwapFailureInvalidTarget() public {
uint256 amountIn = 10 ** 18;
deal(WETH_ADDR, address(uniswapV3Exposed), amountIn);
bool zeroForOne = false;
address fakePool = DUMMY; // Contract with minimal code
bytes memory protocolData = abi.encodePacked(
WETH_ADDR,
DAI_ADDR,
uint24(3000),
address(this),
fakePool,
zeroForOne,
RestrictTransferFrom.TransferType.Transfer
);
vm.expectRevert(UniswapV3Executor__InvalidTarget.selector);
uniswapV3Exposed.swap(amountIn, protocolData);
}
function encodeUniswapV3Swap(
address tokenIn,
address tokenOut,
address receiver,
address target,
bool zero2one,
RestrictTransferFrom.TransferType transferType
) internal view returns (bytes memory) {
IUniswapV3Pool pool = IUniswapV3Pool(target);
return abi.encodePacked(
tokenIn,
tokenOut,
pool.fee(),
receiver,
target,
zero2one,
transferType
);
}
}
contract TychoRouterForBalancerV3Test is TychoRouterTestSetup {
function testSingleSwapUSV3Permit2() public {
// Trade 1 WETH for DAI with 1 swap on Uniswap V3 using Permit2
// Tests entire USV3 flow including callback
// 1 WETH -> DAI
// (USV3)
vm.startPrank(ALICE);
uint256 amountIn = 10 ** 18;
deal(WETH_ADDR, ALICE, amountIn);
(
IAllowanceTransfer.PermitSingle memory permitSingle,
bytes memory signature
) = handlePermit2Approval(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,
ALICE,
DAI_WETH_USV3,
zeroForOne,
RestrictTransferFrom.TransferType.TransferFrom
);
bytes memory swap =
encodeSingleSwap(address(usv3Executor), protocolData);
tychoRouter.singleSwapPermit2(
amountIn,
WETH_ADDR,
DAI_ADDR,
expAmountOut - 1,
false,
false,
ALICE,
permitSingle,
signature,
swap
);
uint256 finalBalance = IERC20(DAI_ADDR).balanceOf(ALICE);
assertGe(finalBalance, expAmountOut);
vm.stopPrank();
}
}

View File

@@ -0,0 +1,386 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;
import "../../src/executors/UniswapV4Executor.sol";
import "../TestUtils.sol";
import "../TychoRouterTestSetup.sol";
import "./UniswapV4Utils.sol";
import "@src/executors/UniswapV4Executor.sol";
import {Constants} from "../Constants.sol";
import {SafeCallback} from "@uniswap/v4-periphery/src/base/SafeCallback.sol";
import {Test} from "../../lib/forge-std/src/Test.sol";
contract UniswapV4ExecutorExposed is UniswapV4Executor {
constructor(IPoolManager _poolManager, address _permit2)
UniswapV4Executor(_poolManager, _permit2)
{}
function decodeData(bytes calldata data)
external
pure
returns (
address tokenIn,
address tokenOut,
bool zeroForOne,
RestrictTransferFrom.TransferType transferType,
address receiver,
UniswapV4Pool[] memory pools
)
{
return _decodeData(data);
}
}
contract UniswapV4ExecutorTest is Constants, TestUtils {
using SafeERC20 for IERC20;
UniswapV4ExecutorExposed uniswapV4Exposed;
IERC20 USDE = IERC20(USDE_ADDR);
IERC20 USDT = IERC20(USDT_ADDR);
address poolManager = 0x000000000004444c5dc75cB358380D2e3dE08A90;
function setUp() public {
uint256 forkBlock = 21817316;
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
uniswapV4Exposed = new UniswapV4ExecutorExposed(
IPoolManager(poolManager), PERMIT2_ADDRESS
);
}
function testDecodeParams() public view {
bool zeroForOne = true;
uint24 pool1Fee = 500;
int24 tickSpacing1 = 60;
uint24 pool2Fee = 1000;
int24 tickSpacing2 = -10;
UniswapV4Executor.UniswapV4Pool[] memory pools =
new UniswapV4Executor.UniswapV4Pool[](2);
pools[0] = UniswapV4Executor.UniswapV4Pool({
intermediaryToken: USDT_ADDR,
fee: pool1Fee,
tickSpacing: tickSpacing1
});
pools[1] = UniswapV4Executor.UniswapV4Pool({
intermediaryToken: USDE_ADDR,
fee: pool2Fee,
tickSpacing: tickSpacing2
});
bytes memory data = UniswapV4Utils.encodeExactInput(
USDE_ADDR,
USDT_ADDR,
zeroForOne,
RestrictTransferFrom.TransferType.Transfer,
ALICE,
pools
);
(
address tokenIn,
address tokenOut,
bool zeroForOneDecoded,
RestrictTransferFrom.TransferType transferType,
address receiver,
UniswapV4Executor.UniswapV4Pool[] memory decodedPools
) = uniswapV4Exposed.decodeData(data);
assertEq(tokenIn, USDE_ADDR);
assertEq(tokenOut, USDT_ADDR);
assertEq(zeroForOneDecoded, zeroForOne);
assertEq(
uint8(transferType),
uint8(RestrictTransferFrom.TransferType.Transfer)
);
assertEq(receiver, ALICE);
assertEq(decodedPools.length, 2);
assertEq(decodedPools[0].intermediaryToken, USDT_ADDR);
assertEq(decodedPools[0].fee, pool1Fee);
assertEq(decodedPools[0].tickSpacing, tickSpacing1);
assertEq(decodedPools[1].intermediaryToken, USDE_ADDR);
assertEq(decodedPools[1].fee, pool2Fee);
assertEq(decodedPools[1].tickSpacing, tickSpacing2);
}
function testSingleSwap() public {
uint256 amountIn = 100 ether;
deal(USDE_ADDR, address(uniswapV4Exposed), amountIn);
uint256 usdeBalanceBeforePool = USDE.balanceOf(poolManager);
uint256 usdeBalanceBeforeSwapExecutor =
USDE.balanceOf(address(uniswapV4Exposed));
UniswapV4Executor.UniswapV4Pool[] memory pools =
new UniswapV4Executor.UniswapV4Pool[](1);
pools[0] = UniswapV4Executor.UniswapV4Pool({
intermediaryToken: USDT_ADDR,
fee: uint24(100),
tickSpacing: int24(1)
});
bytes memory data = UniswapV4Utils.encodeExactInput(
USDE_ADDR,
USDT_ADDR,
true,
RestrictTransferFrom.TransferType.Transfer,
ALICE,
pools
);
uint256 amountOut = uniswapV4Exposed.swap(amountIn, data);
assertEq(USDE.balanceOf(poolManager), usdeBalanceBeforePool + amountIn);
assertEq(
USDE.balanceOf(address(uniswapV4Exposed)),
usdeBalanceBeforeSwapExecutor - amountIn
);
assertTrue(USDT.balanceOf(ALICE) == amountOut);
}
function testSingleSwapIntegration() public {
// USDE -> USDT
bytes memory protocolData =
loadCallDataFromFile("test_encode_uniswap_v4_simple_swap");
uint256 amountIn = 100 ether;
deal(USDE_ADDR, address(uniswapV4Exposed), amountIn);
uint256 usdeBalanceBeforePool = USDE.balanceOf(poolManager);
uint256 usdeBalanceBeforeSwapExecutor =
USDE.balanceOf(address(uniswapV4Exposed));
uint256 amountOut = uniswapV4Exposed.swap(amountIn, protocolData);
assertEq(USDE.balanceOf(poolManager), usdeBalanceBeforePool + amountIn);
assertEq(
USDE.balanceOf(ALICE), usdeBalanceBeforeSwapExecutor - amountIn
);
assertTrue(USDT.balanceOf(ALICE) == amountOut);
}
function testMultipleSwap() public {
// USDE -> USDT -> WBTC
uint256 amountIn = 100 ether;
deal(USDE_ADDR, address(uniswapV4Exposed), amountIn);
uint256 usdeBalanceBeforePool = USDE.balanceOf(poolManager);
uint256 usdeBalanceBeforeSwapExecutor =
USDE.balanceOf(address(uniswapV4Exposed));
UniswapV4Executor.UniswapV4Pool[] memory pools =
new UniswapV4Executor.UniswapV4Pool[](2);
pools[0] = UniswapV4Executor.UniswapV4Pool({
intermediaryToken: USDT_ADDR,
fee: uint24(100),
tickSpacing: int24(1)
});
pools[1] = UniswapV4Executor.UniswapV4Pool({
intermediaryToken: WBTC_ADDR,
fee: uint24(3000),
tickSpacing: int24(60)
});
bytes memory data = UniswapV4Utils.encodeExactInput(
USDE_ADDR,
WBTC_ADDR,
true,
RestrictTransferFrom.TransferType.Transfer,
ALICE,
pools
);
uint256 amountOut = uniswapV4Exposed.swap(amountIn, data);
assertEq(USDE.balanceOf(poolManager), usdeBalanceBeforePool + amountIn);
assertEq(
USDE.balanceOf(address(uniswapV4Exposed)),
usdeBalanceBeforeSwapExecutor - amountIn
);
assertTrue(IERC20(WBTC_ADDR).balanceOf(ALICE) == amountOut);
}
function testMultipleSwapIntegration() public {
// USDE -> USDT -> WBTC
bytes memory protocolData =
loadCallDataFromFile("test_encode_uniswap_v4_sequential_swap");
uint256 amountIn = 100 ether;
deal(USDE_ADDR, address(uniswapV4Exposed), amountIn);
uint256 usdeBalanceBeforePool = USDE.balanceOf(poolManager);
uint256 usdeBalanceBeforeSwapExecutor =
USDE.balanceOf(address(uniswapV4Exposed));
uint256 amountOut = uniswapV4Exposed.swap(amountIn, protocolData);
assertEq(USDE.balanceOf(poolManager), usdeBalanceBeforePool + amountIn);
assertEq(
USDE.balanceOf(address(uniswapV4Exposed)),
usdeBalanceBeforeSwapExecutor - amountIn
);
assertTrue(IERC20(WBTC_ADDR).balanceOf(ALICE) == amountOut);
}
}
contract TychoRouterForBalancerV3Test is TychoRouterTestSetup {
function testSingleSwapUSV4CallbackPermit2() public {
vm.startPrank(ALICE);
uint256 amountIn = 100 ether;
deal(USDE_ADDR, ALICE, amountIn);
(
IAllowanceTransfer.PermitSingle memory permitSingle,
bytes memory signature
) = handlePermit2Approval(USDE_ADDR, tychoRouterAddr, amountIn);
UniswapV4Executor.UniswapV4Pool[] memory pools =
new UniswapV4Executor.UniswapV4Pool[](1);
pools[0] = UniswapV4Executor.UniswapV4Pool({
intermediaryToken: USDT_ADDR,
fee: uint24(100),
tickSpacing: int24(1)
});
bytes memory protocolData = UniswapV4Utils.encodeExactInput(
USDE_ADDR,
USDT_ADDR,
true,
RestrictTransferFrom.TransferType.TransferFrom,
ALICE,
pools
);
bytes memory swap =
encodeSingleSwap(address(usv4Executor), protocolData);
tychoRouter.singleSwapPermit2(
amountIn,
USDE_ADDR,
USDT_ADDR,
99943850,
false,
false,
ALICE,
permitSingle,
signature,
swap
);
assertEq(IERC20(USDT_ADDR).balanceOf(ALICE), 99963618);
vm.stopPrank();
}
function testSplitSwapMultipleUSV4Callback() public {
// This test has two uniswap v4 hops that will be executed inside of the V4 pool manager
// USDE -> USDT -> WBTC
uint256 amountIn = 100 ether;
deal(USDE_ADDR, ALICE, amountIn);
UniswapV4Executor.UniswapV4Pool[] memory pools =
new UniswapV4Executor.UniswapV4Pool[](2);
pools[0] = UniswapV4Executor.UniswapV4Pool({
intermediaryToken: USDT_ADDR,
fee: uint24(100),
tickSpacing: int24(1)
});
pools[1] = UniswapV4Executor.UniswapV4Pool({
intermediaryToken: WBTC_ADDR,
fee: uint24(3000),
tickSpacing: int24(60)
});
bytes memory protocolData = UniswapV4Utils.encodeExactInput(
USDE_ADDR,
WBTC_ADDR,
true,
RestrictTransferFrom.TransferType.TransferFrom,
ALICE,
pools
);
bytes memory swap =
encodeSingleSwap(address(usv4Executor), protocolData);
vm.startPrank(ALICE);
IERC20(USDE_ADDR).approve(tychoRouterAddr, amountIn);
tychoRouter.singleSwap(
amountIn,
USDE_ADDR,
WBTC_ADDR,
118280,
false,
false,
ALICE,
true,
swap
);
assertEq(IERC20(WBTC_ADDR).balanceOf(ALICE), 118281);
}
function testSingleUSV4IntegrationGroupedSwap() public {
// Test created with calldata from our router encoder.
// Performs a single swap from USDC to PEPE though ETH using two
// consecutive USV4 pools. It's a single swap because it is a consecutive grouped swaps
//
// USDC ──(USV4)──> ETH ───(USV4)──> PEPE
//
deal(USDC_ADDR, ALICE, 1 ether);
uint256 balanceBefore = IERC20(PEPE_ADDR).balanceOf(ALICE);
// Approve permit2
vm.startPrank(ALICE);
IERC20(USDC_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max);
bytes memory callData = loadCallDataFromFile(
"test_single_encoding_strategy_usv4_grouped_swap"
);
(bool success,) = tychoRouterAddr.call(callData);
vm.stopPrank();
uint256 balanceAfter = IERC20(PEPE_ADDR).balanceOf(ALICE);
assertTrue(success, "Call Failed");
assertEq(balanceAfter - balanceBefore, 123172000092711286554274694);
}
function testSingleUSV4IntegrationInputETH() public {
// Test created with calldata from our router encoder.
// Performs a single swap from ETH to PEPE without wrapping or unwrapping
//
// ETH ───(USV4)──> PEPE
//
deal(ALICE, 1 ether);
uint256 balanceBefore = IERC20(PEPE_ADDR).balanceOf(ALICE);
bytes memory callData =
loadCallDataFromFile("test_single_encoding_strategy_usv4_eth_in");
(bool success,) = tychoRouterAddr.call{value: 1 ether}(callData);
vm.stopPrank();
uint256 balanceAfter = IERC20(PEPE_ADDR).balanceOf(ALICE);
assertTrue(success, "Call Failed");
assertEq(balanceAfter - balanceBefore, 235610487387677804636755778);
}
function testSingleUSV4IntegrationOutputETH() public {
// Test created with calldata from our router encoder.
// Performs a single swap from USDC to ETH without wrapping or unwrapping
//
// USDC ───(USV4)──> ETH
//
deal(USDC_ADDR, ALICE, 3000_000000);
uint256 balanceBefore = ALICE.balance;
// Approve permit2
vm.startPrank(ALICE);
IERC20(USDC_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max);
bytes memory callData =
loadCallDataFromFile("test_single_encoding_strategy_usv4_eth_out");
(bool success,) = tychoRouterAddr.call(callData);
vm.stopPrank();
uint256 balanceAfter = ALICE.balance;
assertTrue(success, "Call Failed");
console.logUint(balanceAfter - balanceBefore);
assertEq(balanceAfter - balanceBefore, 1474406268748155809);
}
}

View File

@@ -0,0 +1,30 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;
import "@src/executors/UniswapV4Executor.sol";
library UniswapV4Utils {
function encodeExactInput(
address tokenIn,
address tokenOut,
bool zeroForOne,
RestrictTransferFrom.TransferType transferType,
address receiver,
UniswapV4Executor.UniswapV4Pool[] memory pools
) public pure returns (bytes memory) {
bytes memory encodedPools;
for (uint256 i = 0; i < pools.length; i++) {
encodedPools = abi.encodePacked(
encodedPools,
pools[i].intermediaryToken,
bytes3(pools[i].fee),
pools[i].tickSpacing
);
}
return abi.encodePacked(
tokenIn, tokenOut, zeroForOne, transferType, receiver, encodedPools
);
}
}