feat: Refactor Curve Executor not to use the router

We don't need to use all the functionalities of the Curve Router, only the swap type 1 (exchange). By bypassing the router we can save gas on 2 token transfers and with smaller calldata
A nice side effect is that the executor is much more readable and understandable now

--- don't change below this line ---
ENG-4305 Took 2 hours 25 minutes


Took 12 seconds
This commit is contained in:
Diana Carvalho
2025-04-03 18:14:17 +01:00
parent f468a7831a
commit 9f2184258a
5 changed files with 197 additions and 283 deletions

View File

@@ -130,9 +130,6 @@ contract Constants is Test, BaseConstants {
bytes32 PANCAKEV3_POOL_CODE_INIT_HASH =
0x6ce8eb472fa82df5469c6ab6d485f17c3ad13c8cd7af59b3d4a8026c5ce0f7e2;
// Curve router
address CURVE_ROUTER = 0x16C6521Dff6baB339122a0FE25a9116693265353;
// Curve meta registry
address CURVE_META_REGISTRY = 0xF98B45FA17DE75FB1aD0e7aFD971b0ca00e379fC;

View File

@@ -1,42 +0,0 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;
import {Constants} from "./Constants.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ICurveRouter} from "../interfaces/ICurveRouter.sol";
contract CurveRouterGasTest is Constants {
ICurveRouter curveRouter = ICurveRouter(CURVE_ROUTER);
function setUp() public {
uint256 forkBlock = 22031795;
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
}
function testCurveRouter() public {
address[11] memory route;
route[0] = WETH_ADDR;
route[1] = TRICRYPTO_POOL;
route[2] = USDC_ADDR;
uint256[5][5] memory swapParams;
swapParams[0][0] = 2; // tokenIn Index
swapParams[0][1] = 0; // tokenOut Index
swapParams[0][2] = 1; // swap type
swapParams[0][3] = 3; // pool type
swapParams[0][4] = 3; // n_coins
uint256 amountIn = 1 ether;
uint256 minAmountOut = 0;
address[5] memory pools;
deal(WETH_ADDR, ALICE, amountIn);
vm.startPrank(ALICE);
IERC20(WETH_ADDR).approve(address(curveRouter), amountIn);
curveRouter.exchange(
route, swapParams, amountIn, minAmountOut, pools, address(this)
);
vm.stopPrank();
}
}

View File

@@ -35,14 +35,20 @@ interface IAaveLendingPool {
}
contract CurveExecutorExposed is CurveExecutor {
constructor(address _curveRouter, address _nativeToken)
CurveExecutor(_curveRouter, _nativeToken)
{}
constructor(address _nativeToken) CurveExecutor(_nativeToken) {}
function decodeParams(bytes calldata data)
function decodeData(bytes calldata data)
external
pure
returns (CurveSwapParams memory params)
returns (
address tokenIn,
address tokenOut,
address pool,
uint8 poolType,
int128 i,
int128 j,
bool tokenApprovalNeeded
)
{
return _decodeData(data);
}
@@ -57,86 +63,78 @@ contract CurveExecutorTest is Test, Constants {
function setUp() public {
uint256 forkBlock = 22031795;
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
curveExecutorExposed = new CurveExecutorExposed(CURVE_ROUTER, ETH_ADDR);
curveExecutorExposed = new CurveExecutorExposed(ETH_ADDR);
metaRegistry = MetaRegistry(CURVE_META_REGISTRY);
}
function testDecodeParams() public view {
address[11] memory route =
_getRoute(WETH_ADDR, USDC_ADDR, TRICRYPTO_POOL);
bytes memory data = abi.encodePacked(
WETH_ADDR,
USDC_ADDR,
TRICRYPTO_POOL,
uint8(3),
uint8(2),
uint8(0),
true
);
// The meta registry does not have information about the pool.
// We manually set the swap params.
uint256[5][5] memory swapParams;
swapParams[0][0] = 2; // tokenIn Index
swapParams[0][1] = 0; // tokenOut Index
swapParams[0][2] = 1; // swap type
swapParams[0][3] = 3; // pool type
swapParams[0][4] = 3; // n_coins
(
address tokenIn,
address tokenOut,
address pool,
uint8 poolType,
int128 i,
int128 j,
bool tokenApprovalNeeded
) = curveExecutorExposed.decodeData(data);
bytes memory data = abi.encode(route, swapParams, address(this), true);
CurveExecutor.CurveSwapParams memory params =
curveExecutorExposed.decodeParams(data);
assertEq(params.route[0], WETH_ADDR);
assertEq(params.route[1], TRICRYPTO_POOL);
assertEq(params.route[2], USDC_ADDR);
assertEq(params.swapParams[0][0], 2);
assertEq(params.swapParams[0][1], 0);
assertEq(params.swapParams[0][2], 1);
assertEq(params.swapParams[0][3], 3);
assertEq(params.swapParams[0][4], 3);
assertEq(params.receiver, address(this));
assertEq(params.needsApproval, true);
assertEq(tokenIn, WETH_ADDR);
assertEq(tokenOut, USDC_ADDR);
assertEq(pool, TRICRYPTO_POOL);
assertEq(poolType, 3);
assertEq(i, 2);
assertEq(j, 0);
assertEq(tokenApprovalNeeded, true);
}
function testTriPool() public {
// Swapping DAI -> USDC on TriPool 0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7
address[11] memory route = _getRoute(DAI_ADDR, USDC_ADDR, TRIPOOL);
uint256[5][5] memory swapParams =
_getSwapParams(TRIPOOL, DAI_ADDR, USDC_ADDR, 1, 1);
uint256 amountIn = 1 ether;
deal(DAI_ADDR, address(curveExecutorExposed), amountIn);
bytes memory data = abi.encode(route, swapParams, address(this), true);
bytes memory data = _getData(DAI_ADDR, USDC_ADDR, TRIPOOL, 1);
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 999796);
assertEq(IERC20(USDC_ADDR).balanceOf(address(this)), amountOut);
assertEq(amountOut, 999797);
assertEq(
IERC20(USDC_ADDR).balanceOf(address(curveExecutorExposed)),
amountOut
);
}
function testStEthPool() public {
// Swapping ETH -> stETH on StEthPool 0xDC24316b9AE028F1497c275EB9192a3Ea0f67022
address[11] memory route = _getRoute(ETH_ADDR, STETH_ADDR, STETH_POOL);
uint256[5][5] memory swapParams =
_getSwapParams(STETH_POOL, ETH_ADDR, STETH_ADDR, 1, 1);
uint256 amountIn = 1 ether;
deal(address(curveExecutorExposed), amountIn);
bytes memory data = abi.encode(route, swapParams, address(this), false);
bytes memory data = _getData(ETH_ADDR, STETH_ADDR, STETH_POOL, 1);
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertTrue(amountOut == 1001072414418410898);
assertEq(IERC20(STETH_ADDR).balanceOf(address(this)), amountOut - 1); //// Gets 1 wei less than amountOut
assertEq(amountOut, 1001072414418410897);
assertEq(
IERC20(STETH_ADDR).balanceOf(address(curveExecutorExposed)),
amountOut
);
}
function testTricrypto2Pool() public {
// Swapping WETH -> WBTC on Tricrypto2Pool 0xD51a44d3FaE010294C616388b506AcdA1bfAAE46
address[11] memory route =
_getRoute(WETH_ADDR, WBTC_ADDR, TRICRYPTO2_POOL);
uint256[5][5] memory swapParams =
_getSwapParams(TRICRYPTO2_POOL, WETH_ADDR, WBTC_ADDR, 1, 3);
uint256 amountIn = 1 ether;
deal(WETH_ADDR, address(curveExecutorExposed), amountIn);
bytes memory data =
abi.encode(route, swapParams, address(curveExecutorExposed), true);
bytes memory data = _getData(WETH_ADDR, WBTC_ADDR, TRICRYPTO2_POOL, 3);
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
@@ -149,15 +147,10 @@ contract CurveExecutorTest is Test, Constants {
function testSUSDPool() public {
// Swapping USDC -> SUSD on SUSDPool 0xA5407eAE9Ba41422680e2e00537571bcC53efBfD
address[11] memory route = _getRoute(USDC_ADDR, SUSD_ADDR, SUSD_POOL);
uint256[5][5] memory swapParams =
_getSwapParams(SUSD_POOL, USDC_ADDR, SUSD_ADDR, 1, 1);
uint256 amountIn = 100 * 10 ** 6;
deal(USDC_ADDR, address(curveExecutorExposed), amountIn);
bytes memory data =
abi.encode(route, swapParams, address(curveExecutorExposed), true);
bytes memory data = _getData(USDC_ADDR, SUSD_ADDR, SUSD_POOL, 1);
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
@@ -170,20 +163,14 @@ contract CurveExecutorTest is Test, Constants {
function testFraxUsdcPool() public {
// Swapping FRAX -> USDC on FraxUsdcPool 0xDcEF968d416a41Cdac0ED8702fAC8128A64241A2
address[11] memory route =
_getRoute(FRAX_ADDR, USDC_ADDR, FRAX_USDC_POOL);
uint256[5][5] memory swapParams =
_getSwapParams(FRAX_USDC_POOL, FRAX_ADDR, USDC_ADDR, 1, 1);
uint256 amountIn = 1 ether;
deal(FRAX_ADDR, address(curveExecutorExposed), amountIn);
bytes memory data =
abi.encode(route, swapParams, address(curveExecutorExposed), true);
bytes memory data = _getData(FRAX_ADDR, USDC_ADDR, FRAX_USDC_POOL, 1);
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 998096);
assertEq(amountOut, 998097);
assertEq(
IERC20(USDC_ADDR).balanceOf(address(curveExecutorExposed)),
amountOut
@@ -192,16 +179,10 @@ contract CurveExecutorTest is Test, Constants {
function testUsdeUsdcPool() public {
// Swapping USDC -> USDE on a CryptoSwapNG, deployed by factory 0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf (plain pool)
address[11] memory route =
_getRoute(USDC_ADDR, USDE_ADDR, USDE_USDC_POOL);
uint256[5][5] memory swapParams =
_getSwapParams(USDE_USDC_POOL, USDC_ADDR, USDE_ADDR, 1, 1);
uint256 amountIn = 100 * 10 ** 6;
deal(USDC_ADDR, address(curveExecutorExposed), amountIn);
bytes memory data =
abi.encode(route, swapParams, address(curveExecutorExposed), true);
bytes memory data = _getData(USDC_ADDR, USDE_ADDR, USDE_USDC_POOL, 1);
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
@@ -214,20 +195,15 @@ contract CurveExecutorTest is Test, Constants {
function testDolaFraxPyusdPool() public {
// Swapping DOLA -> FRAXPYUSD on a CryptoSwapNG, deployed by factory 0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf (meta pool)
address[11] memory route =
_getRoute(DOLA_ADDR, FRAXPYUSD_POOL, DOLA_FRAXPYUSD_POOL);
uint256[5][5] memory swapParams =
_getSwapParams(DOLA_FRAXPYUSD_POOL, DOLA_ADDR, FRAXPYUSD_POOL, 1, 1);
uint256 amountIn = 100 * 10 ** 6;
deal(DOLA_ADDR, address(curveExecutorExposed), amountIn);
bytes memory data =
abi.encode(route, swapParams, address(curveExecutorExposed), true);
_getData(DOLA_ADDR, FRAXPYUSD_POOL, DOLA_FRAXPYUSD_POOL, 1);
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 99688991);
assertEq(amountOut, 99688992);
assertEq(
IERC20(FRAXPYUSD_POOL).balanceOf(address(curveExecutorExposed)),
amountOut
@@ -236,15 +212,11 @@ contract CurveExecutorTest is Test, Constants {
function testCryptoPoolWithETH() public {
// Swapping XYO -> ETH on a CryptoPool, deployed by factory 0xF18056Bbd320E96A48e3Fbf8bC061322531aac99
address[11] memory route = _getRoute(XYO_ADDR, ETH_ADDR, ETH_XYO_POOL);
uint256[5][5] memory swapParams =
_getSwapParams(ETH_XYO_POOL, XYO_ADDR, ETH_ADDR, 1, 2);
uint256 amountIn = 1 ether;
uint256 initialBalance = address(curveExecutorExposed).balance; // this address already has some ETH assigned to it
deal(XYO_ADDR, address(curveExecutorExposed), amountIn);
bytes memory data =
abi.encode(route, swapParams, address(curveExecutorExposed), true);
bytes memory data = _getData(XYO_ADDR, ETH_ADDR, ETH_XYO_POOL, 2);
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
@@ -256,16 +228,10 @@ contract CurveExecutorTest is Test, Constants {
function testCryptoPool() public {
// Swapping BSGG -> USDT on a CryptoPool, deployed by factory 0xF18056Bbd320E96A48e3Fbf8bC061322531aac99
address[11] memory route =
_getRoute(BSGG_ADDR, USDT_ADDR, BSGG_USDT_POOL);
uint256[5][5] memory swapParams =
_getSwapParams(BSGG_USDT_POOL, BSGG_ADDR, USDT_ADDR, 1, 2);
uint256 amountIn = 1000 ether;
deal(BSGG_ADDR, address(curveExecutorExposed), amountIn);
bytes memory data =
abi.encode(route, swapParams, address(curveExecutorExposed), true);
bytes memory data = _getData(BSGG_ADDR, USDT_ADDR, BSGG_USDT_POOL, 2);
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
@@ -278,33 +244,26 @@ contract CurveExecutorTest is Test, Constants {
function testTricryptoPool() public {
// Swapping WETH -> USDC on a Tricrypto pool, deployed by factory 0x0c0e5f2fF0ff18a3be9b835635039256dC4B4963
address[11] memory route =
_getRoute(WETH_ADDR, USDC_ADDR, TRICRYPTO_POOL);
uint256[5][5] memory swapParams =
_getSwapParams(TRICRYPTO_POOL, WETH_ADDR, USDC_ADDR, 1, 2);
uint256 amountIn = 1 ether;
deal(WETH_ADDR, address(curveExecutorExposed), amountIn);
bytes memory data = abi.encode(route, swapParams, address(this), true);
bytes memory data = _getData(WETH_ADDR, USDC_ADDR, TRICRYPTO_POOL, 2);
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 1861130973);
assertEq(IERC20(USDC_ADDR).balanceOf(address(this)), amountOut);
assertEq(amountOut, 1861130974);
assertEq(
IERC20(USDC_ADDR).balanceOf(address(curveExecutorExposed)),
amountOut
);
}
function testTwoCryptoPool() public {
// Swapping UWU -> WETH on a Twocrypto pool, deployed by factory 0x98ee851a00abee0d95d08cf4ca2bdce32aeaaf7f
address[11] memory route = _getRoute(UWU_ADDR, WETH_ADDR, UWU_WETH_POOL);
uint256[5][5] memory swapParams =
_getSwapParams(UWU_WETH_POOL, UWU_ADDR, WETH_ADDR, 1, 2);
uint256 amountIn = 1 ether;
deal(UWU_ADDR, address(curveExecutorExposed), amountIn);
bytes memory data =
abi.encode(route, swapParams, address(curveExecutorExposed), true);
bytes memory data = _getData(UWU_ADDR, WETH_ADDR, UWU_WETH_POOL, 2);
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
@@ -317,16 +276,11 @@ contract CurveExecutorTest is Test, Constants {
function testStableSwapPool() public {
// Swapping CRVUSD -> USDT on a StableSwap pool, deployed by factory 0x4F8846Ae9380B90d2E71D5e3D042dff3E7ebb40d (plain pool)
address[11] memory route =
_getRoute(CRVUSD_ADDR, USDT_ADDR, CRVUSD_USDT_POOL);
uint256[5][5] memory swapParams =
_getSwapParams(CRVUSD_USDT_POOL, CRVUSD_ADDR, USDT_ADDR, 1, 1);
uint256 amountIn = 1 ether;
deal(CRVUSD_ADDR, address(curveExecutorExposed), amountIn);
bytes memory data =
abi.encode(route, swapParams, address(curveExecutorExposed), true);
_getData(CRVUSD_ADDR, USDT_ADDR, CRVUSD_USDT_POOL, 1);
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
@@ -339,52 +293,47 @@ contract CurveExecutorTest is Test, Constants {
function testMetaPool() public {
// Swapping WTAO -> WSTTAO on a MetaPool deployed by factory 0xB9fC157394Af804a3578134A6585C0dc9cc990d4 (plain pool)
address[11] memory route =
_getRoute(WTAO_ADDR, WSTTAO_ADDR, WSTTAO_WTAO_POOL);
uint256[5][5] memory swapParams =
_getSwapParams(WSTTAO_WTAO_POOL, WTAO_ADDR, WSTTAO_ADDR, 1, 1);
uint256 amountIn = 100 * 10 ** 9; // 9 decimals
deal(WTAO_ADDR, address(curveExecutorExposed), amountIn);
bytes memory data =
abi.encode(route, swapParams, address(curveExecutorExposed), true);
_getData(WTAO_ADDR, WSTTAO_ADDR, WSTTAO_WTAO_POOL, 1);
uint256 amountOut = curveExecutorExposed.swap(amountIn, data);
assertEq(amountOut, 32797923609);
assertEq(amountOut, 32797923610);
assertEq(
IERC20(WSTTAO_ADDR).balanceOf(address(curveExecutorExposed)),
amountOut
);
}
function _getRoute(address tokenIn, address tokenOut, address pool)
internal
pure
returns (address[11] memory route)
{
route[0] = tokenIn;
route[2] = tokenOut;
route[1] = pool;
}
function _getSwapParams(
address pool,
function _getData(
address tokenIn,
address tokenOut,
uint256 swapType,
uint256 poolType
) internal view returns (uint256[5][5] memory swapParams) {
uint256 nCoins = metaRegistry.get_n_coins(pool);
address pool,
uint8 poolType
) 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
);
}
function _getIndexes(address tokenIn, address tokenOut, address pool)
internal
view
returns (int128, int128)
{
(int128 coinInIndex, int128 coinOutIndex,) =
metaRegistry.get_coin_indices(pool, tokenIn, tokenOut);
swapParams[0][0] = uint256(int256(coinInIndex));
swapParams[0][1] = uint256(int256(coinOutIndex));
swapParams[0][2] = swapType;
swapParams[0][3] = poolType;
swapParams[0][4] = nCoins;
return (coinInIndex, coinOutIndex);
}
function dealAaveDai() internal {