diff --git a/foundry/interfaces/ICurveRouter.sol b/foundry/interfaces/ICurveRouter.sol new file mode 100644 index 0000000..00bb9e2 --- /dev/null +++ b/foundry/interfaces/ICurveRouter.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.26; + +interface ICurveRouter { + function exchange( + address[11] memory route, + uint256[5][5] memory swapParams, + uint256 amountIn, + uint256 minAmountOut, + address[5] memory pools, + address receiver + ) external payable returns (uint256); + + function get_dy( + address[] memory route, + uint256[] memory swapParams, + uint256 amountIn, + address[] memory pools + ) external view returns (uint256); + + +} + + struct CurveRouterParams { + address[11] route; + uint256[5][5] swapParams; + uint256 amountIn; + uint256 minAmountOut; + address[5] pools; + address receiver; + } + diff --git a/foundry/src/executors/CurveExecutor.sol b/foundry/src/executors/CurveExecutor.sol new file mode 100644 index 0000000..0fd277c --- /dev/null +++ b/foundry/src/executors/CurveExecutor.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.26; + +import "@interfaces/IExecutor.sol"; +import "@interfaces/ICurveRouter.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +contract CurveExecutor is IExecutor { + using SafeERC20 for IERC20; + + ICurveRouter public immutable curveRouter; + + constructor(address _curveRouter) { + curveRouter = ICurveRouter(_curveRouter); + } + + function swap(uint256 amountIn, bytes calldata data) + external + payable + returns (uint256) + { + CurveRouterParams memory params = _decodeData(data); + IERC20(params.route[0]).approve(address(curveRouter), amountIn); + return curveRouter.exchange( + params.route, + params.swapParams, + amountIn, + params.minAmountOut, + params.pools, + params.receiver + ); + } + + function _decodeData(bytes calldata data) + internal + pure + returns (CurveRouterParams memory params) + { + return abi.decode(data, (CurveRouterParams)); + } +} diff --git a/foundry/test/Constants.sol b/foundry/test/Constants.sol index 1a1fff7..b99f102 100644 --- a/foundry/test/Constants.sol +++ b/foundry/test/Constants.sol @@ -76,6 +76,10 @@ contract Constants is Test, BaseConstants { address PANCAKESWAPV3_DEPLOYER_ETHEREUM = 0x41ff9AA7e16B8B1a8a8dc4f0eFacd93D02d071c9; + // Curve + address TRICRYPTO_USDC_WBTC_WETH = + 0x7F86Bf177Dd4F3494b841a37e810A34dD56c829B; + // Uniswap universal router address UNIVERSAL_ROUTER = 0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af; @@ -94,6 +98,9 @@ contract Constants is Test, BaseConstants { bytes32 PANCAKEV3_POOL_CODE_INIT_HASH = 0x6ce8eb472fa82df5469c6ab6d485f17c3ad13c8cd7af59b3d4a8026c5ce0f7e2; + // Curve router + address CURVE_ROUTER = 0x16C6521Dff6baB339122a0FE25a9116693265353; + /** * @dev Deploys a dummy contract with non-empty bytecode */ diff --git a/foundry/test/CurveRouterGasTest.t.sol b/foundry/test/CurveRouterGasTest.t.sol new file mode 100644 index 0000000..38c8cf4 --- /dev/null +++ b/foundry/test/CurveRouterGasTest.t.sol @@ -0,0 +1,43 @@ +// 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 {console} from "forge-std/console.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_USDC_WBTC_WETH; + 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); + uint256 amountOut = curveRouter.exchange( + route, swapParams, amountIn, minAmountOut, pools, address(this) + ); + vm.stopPrank(); + } +} diff --git a/foundry/test/executors/CurveExecutor.t.sol b/foundry/test/executors/CurveExecutor.t.sol new file mode 100644 index 0000000..ce60c5c --- /dev/null +++ b/foundry/test/executors/CurveExecutor.t.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.26; + +import "@src/executors/CurveExecutor.sol"; +import {Test} from "../../lib/forge-std/src/Test.sol"; +import {Constants} from "../Constants.sol"; + +contract CurveExecutorExposed is CurveExecutor { + constructor(address _curveRouter) CurveExecutor(_curveRouter) {} + + function decodeParams(bytes calldata data) + external + pure + returns (CurveRouterParams memory params) + { + return _decodeData(data); + } +} + +contract CurveExecutorTest is Test, Constants { + using SafeERC20 for IERC20; + + CurveExecutorExposed curveExecutorExposed; + + function setUp() public { + uint256 forkBlock = 22031795; + vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); + curveExecutorExposed = new CurveExecutorExposed(CURVE_ROUTER); + } + + function testDecodeParams() public view { + address[11] memory route; + route[0] = WETH_ADDR; + route[1] = TRICRYPTO_USDC_WBTC_WETH; + 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; + + bytes memory data = abi.encode( + route, swapParams, amountIn, minAmountOut, pools, address(this) + ); + + CurveRouterParams memory params = + curveExecutorExposed.decodeParams(data); + + assertEq(params.route[0], WETH_ADDR); + assertEq(params.route[1], TRICRYPTO_USDC_WBTC_WETH); + 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); + } + + function testSwapCurve() public { + address[11] memory route; + route[0] = WETH_ADDR; + route[1] = TRICRYPTO_USDC_WBTC_WETH; + 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, address(curveExecutorExposed), amountIn); + bytes memory data = abi.encode( + route, swapParams, amountIn, minAmountOut, pools, address(this) + ); + + uint256 amountOut = curveExecutorExposed.swap(amountIn, data); + + assertEq(amountOut, 1861130973); + assertEq(IERC20(USDC_ADDR).balanceOf(address(this)), amountOut); + } +}