From bee100b0757f17038be96e4b548b61323b3efbaf Mon Sep 17 00:00:00 2001 From: sandybradley Date: Thu, 4 Jan 2024 20:59:23 +0200 Subject: [PATCH] feat: staticcall route --- README.md | 2 +- src/MevEthRouter.sol | 124 ++++++++++++++++++++++---------- src/interfaces/ICurveV2Pool.sol | 25 +------ test/MevEthRouter.t.sol | 31 ++++++-- 4 files changed, 114 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index b1fe7ff..4c9b05a 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ Pools - Curve - Balancer - Uniswap V3 0.30% -- Uniswap V3 0.05% - Uniswap V3 1.00% +- Uniswap V3 0.05% - Sushiswap - Uniswap V2 diff --git a/src/MevEthRouter.sol b/src/MevEthRouter.sol index f4506b2..caa93ff 100644 --- a/src/MevEthRouter.sol +++ b/src/MevEthRouter.sol @@ -75,8 +75,6 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { IGyroECLPMath.Params internal params; IGyroECLPMath.DerivedParams internal derived; - IGyroECLPMath.Vector2 internal inv; - /// @notice struct for pool reserves /// @param reserveIn amount of reserves (or virtual reserves) in pool for tokenIn /// @param reserveOut amount of reserves (or virtual reserves) in pool for tokenOut @@ -292,11 +290,20 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { pools[7].pair = address(MEVETH); } + function balancerInvariant(uint256[] memory balances) internal view returns (IGyroECLPMath.Vector2 memory inv) { + // for some reason GyroECLPMath lib has a different selector + bytes memory data = abi.encodeWithSelector(0x78ace857, balances, params, derived); + (, bytes memory returnData) = address(gyroMath).staticcall(data); + (int256 invariant, int256 err) = abi.decode(returnData, (int256, int256)); + // (int256 invariant, int256 err) = gyroMath.calculateInvariantWithError(balances, params, derived); + inv = IGyroECLPMath.Vector2(invariant + 2 * err, invariant); + } + /// @notice Fetches swap data for each pair and amounts given an input amount /// @param amountIn Amount in for first token in path /// @param amountOutMin Min amount out /// @return swaps Array Swap data for each user swap in path - function getStakeRoute(uint256 amountIn, uint256 amountOutMin) public returns (Swap memory swaps) { + function getStakeRoute(uint256 amountIn, uint256 amountOutMin) public view returns (Swap memory swaps) { swaps.pools = _getPools(); swaps.tokenIn = address(WETH09); swaps.tokenOut = address(MEVETH); @@ -318,7 +325,7 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { /// @param amountIn Amount in for first token in path /// @param amountOutMin Min amount out /// @return swaps Array Swap data for each user swap in path - function getRedeemRoute(bool useQueue, uint256 amountIn, uint256 amountOutMin) public returns (Swap memory swaps) { + function getRedeemRoute(bool useQueue, uint256 amountIn, uint256 amountOutMin) public view returns (Swap memory swaps) { swaps.pools = _getPools(); swaps.tokenIn = address(MEVETH); swaps.tokenOut = address(WETH09); @@ -326,6 +333,7 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { uint256[8] memory amountsOut; { Reserve[8] memory reserves = _getReserves(false, swaps.pools); + // find optimal route (amountsIn, amountsOut) = _optimalRouteOut(useQueue, false, amountIn, amountOutMin, reserves); } @@ -338,7 +346,7 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { /// @dev populates and returns Reserve struct array for each pool address /// @param isDeposit true if deposit eth, false if redeem /// @param pools 5 element array of Pool structs populated with pool addresses - function _getReserves(bool isDeposit, Pool[8] memory pools) internal returns (Reserve[8] memory reserves) { + function _getReserves(bool isDeposit, Pool[8] memory pools) internal view returns (Reserve[8] memory reserves) { // 2 V2 pools for (uint256 i; i < 2; i = _inc(i)) { if (!MevEthLibrary.isContract(pools[i].pair)) continue; @@ -363,15 +371,6 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { { uint256[] memory balances = _getAllBalances(); (reserves[5].reserveIn, reserves[5].reserveOut) = isDeposit ? (balances[1], balances[0]) : (balances[0], balances[1]); - - // for some reason GyroECLPMath lib has a different selector - bytes memory data = abi.encodeWithSelector(0x78ace857, balances, params, derived); - (, bytes memory returnData) = address(gyroMath).staticcall(data); - (int256 invariant, int256 err) = abi.decode(returnData, (int256, int256)); - // (int256 invariant, int256 err) = gyroMath.calculateInvariantWithError(balances, params, derived); - if (invariant != inv.y) { - inv = IGyroECLPMath.Vector2(invariant + 2 * err, invariant); - } } // Curve CryptoV2 (i=6) // Note: Curve token order is opposite from balancer / uni / sushi @@ -397,6 +396,7 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { Reserve[8] memory reserves ) internal + view returns (uint256[8] memory amountsIn, uint256[8] memory amountsOut) { // calculate best rate for a single swap (i.e. no splitting) @@ -404,38 +404,44 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { // get ref rate for splits uint256[8] memory amountsOutSingleEth; - // first 3 pools have fee of 0.3% + // calc balancer invariant + uint256[] memory balances = new uint256[](2); + balances[0] = isDeposit ? reserves[5].reserveOut : reserves[5].reserveIn; + balances[1] = isDeposit ? reserves[5].reserveIn : reserves[5].reserveOut; + IGyroECLPMath.Vector2 memory inv = balancerInvariant(balances); + + // first 2 pools have fee of 0.3% for (uint256 i; i < 2; i = _inc(i)) { if (reserves[i].reserveOut > amountOutMin) { - amountsOutSingleSwap[i] = amountOutCall(isDeposit, i, amountIn, reserves[i].reserveIn, reserves[i].reserveOut); + amountsOutSingleSwap[i] = amountOutCall(isDeposit, i, amountIn, reserves[i].reserveIn, reserves[i].reserveOut, inv); } if (reserves[i].reserveOut > MIN_LIQUIDITY && reserves[i].reserveIn > MIN_LIQUIDITY && amountIn > MIN_LIQUIDITY) { - amountsOutSingleEth[i] = amountOutCall(isDeposit, i, 1 ether, reserves[i].reserveIn, reserves[i].reserveOut); + amountsOutSingleEth[i] = amountOutCall(isDeposit, i, 1 ether, reserves[i].reserveIn, reserves[i].reserveOut, inv); } } - // next 2 pools have variable rates + // next 3 pools have variable rates for (uint256 i = 2; i < 5; i = _inc(i)) { if (reserves[i].reserveOut > amountOutMin && reserves[i].reserveIn > amountIn) { - amountsOutSingleSwap[i] = amountOutCall(isDeposit, i, amountIn, reserves[i].reserveIn, reserves[i].reserveOut); + amountsOutSingleSwap[i] = amountOutCall(isDeposit, i, amountIn, reserves[i].reserveIn, reserves[i].reserveOut, inv); } if (reserves[i].reserveOut > MIN_LIQUIDITY && reserves[i].reserveIn > MIN_LIQUIDITY && amountIn > MIN_LIQUIDITY) { - amountsOutSingleEth[i] = amountOutCall(isDeposit, i, 1 ether, reserves[i].reserveIn, reserves[i].reserveOut); + amountsOutSingleEth[i] = amountOutCall(isDeposit, i, 1 ether, reserves[i].reserveIn, reserves[i].reserveOut, inv); } } // Balancer pool (todo: embed amount out calc) if (reserves[5].reserveOut > amountOutMin) { - amountsOutSingleSwap[5] = amountOutCall(isDeposit, 5, amountIn, reserves[5].reserveIn, reserves[5].reserveOut); + amountsOutSingleSwap[5] = amountOutCall(isDeposit, 5, amountIn, reserves[5].reserveIn, reserves[5].reserveOut, inv); } if (reserves[5].reserveOut > MIN_LIQUIDITY && amountIn > MIN_LIQUIDITY) { - amountsOutSingleEth[5] = amountOutCall(isDeposit, 5, 1 ether, reserves[5].reserveIn, reserves[5].reserveOut); + amountsOutSingleEth[5] = amountOutCall(isDeposit, 5, 1 ether, reserves[5].reserveIn, reserves[5].reserveOut, inv); } // Curve pool (todo: embed amount out calc) if (reserves[6].reserveOut > amountOutMin) { - amountsOutSingleSwap[6] = amountOutCall(isDeposit, 6, amountIn, reserves[6].reserveIn, reserves[6].reserveOut); + amountsOutSingleSwap[6] = amountOutCall(isDeposit, 6, amountIn, reserves[6].reserveIn, reserves[6].reserveOut, inv); } if (reserves[6].reserveOut > MIN_LIQUIDITY && amountIn > MIN_LIQUIDITY) { - amountsOutSingleEth[6] = amountOutCall(isDeposit, 6, 1 ether, reserves[6].reserveIn, reserves[6].reserveOut); + amountsOutSingleEth[6] = amountOutCall(isDeposit, 6, 1 ether, reserves[6].reserveIn, reserves[6].reserveOut, inv); } // MevEth if (isDeposit) { @@ -461,7 +467,7 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { } } - (amountsIn, amountsOut) = _splitSwapOut(isDeposit, amountIn, amountsOutSingleSwap, amountsOutSingleEth, reserves); + (amountsIn, amountsOut) = _splitSwapOut(isDeposit, amountIn, inv, amountsOutSingleSwap, amountsOutSingleEth, reserves); } function _scalingFactor(bool token0) internal view returns (uint256 scalingFactor) { @@ -499,7 +505,17 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { return balances; } - function balancerAmountOut(bool isDeposit, uint256 amountIn, uint256 reserveIn, uint256 reserveOut) internal view returns (uint256 amountOut) { + function balancerAmountOut( + bool isDeposit, + uint256 amountIn, + uint256 reserveIn, + uint256 reserveOut, + IGyroECLPMath.Vector2 memory inv + ) + internal + view + returns (uint256 amountOut) + { uint256 scalingFactorTokenIn = _scalingFactor(!isDeposit); uint256 scalingFactorTokenOut = _scalingFactor(isDeposit); uint256[] memory balances = new uint256[](2); @@ -516,7 +532,18 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { amountOut = abi.decode(returnData, (uint256)) * 1 ether / scalingFactorTokenOut; } - function amountOutCall(bool isDeposit, uint256 i, uint256 amountIn, uint256 reserveIn, uint256 reserveOut) internal returns (uint256 amountOut) { + function amountOutCall( + bool isDeposit, + uint256 i, + uint256 amountIn, + uint256 reserveIn, + uint256 reserveOut, + IGyroECLPMath.Vector2 memory inv + ) + internal + view + returns (uint256 amountOut) + { if (i < 2) { if (reserveOut > MIN_LIQUIDITY && amountIn < reserveOut) { amountOut = MevEthLibrary.getAmountOut(amountIn, reserveIn, reserveOut); @@ -529,7 +556,7 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { // Balancer pool if (i == 5) { if (reserveOut > MIN_LIQUIDITY && amountIn < reserveOut / 2) { - amountOut = balancerAmountOut(isDeposit, amountIn, reserveIn, reserveOut); + amountOut = balancerAmountOut(isDeposit, amountIn, reserveIn, reserveOut, inv); } } @@ -550,11 +577,13 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { function _splitSwapOut( bool isDeposit, uint256 amountIn, + IGyroECLPMath.Vector2 memory inv, uint256[8] memory amountsOutSingleSwap, uint256[8] memory amountsOutSingleEth, Reserve[8] memory reserves ) internal + view returns (uint256[8] memory amountsIn, uint256[8] memory amountsOut) { uint256[8] memory index = MevEthLibrary._sortArray(amountsOutSingleSwap); // sorts in ascending order (i.e. best price is last) @@ -587,7 +616,7 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { if (index2[i] == 7) { amountsIn[index2[i]] = amountIn - cumulativeAmount; amountsOut[index2[i]] = - amountOutCall(isDeposit, index2[i], amountsIn[index2[i]], reserves[index2[i]].reserveIn, reserves[index2[i]].reserveOut); + amountOutCall(isDeposit, index2[i], amountsIn[index2[i]], reserves[index2[i]].reserveIn, reserves[index2[i]].reserveOut, inv); return (amountsIn, amountsOut); } // meveth rate is fixed so no more iterations required @@ -600,7 +629,8 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { index2[i], amountsOutSingleEth[index2[_dec(i)]], reserves[index2[i]].reserveIn, - reserves[index2[i]].reserveOut + reserves[index2[i]].reserveOut, + inv ); // UniV3 adjustment if (index2[i] < 5 && index2[i] > 1 && amountsIn[index2[i]] > 8 * MIN_LIQUIDITY) { @@ -624,9 +654,11 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { uint256 index, uint256 amountsOutSingleEthTarget, uint256 reserveIn, - uint256 reserveOut + uint256 reserveOut, + IGyroECLPMath.Vector2 memory inv ) internal + view returns (uint256 amountInToSync, uint256 amountOut) { // todo: make more efficient @@ -654,7 +686,7 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { return (amountInToSync, amountOut); } - amountOut = amountOutCall(isDeposit, index, amount, reserveIn, reserveOut); + amountOut = amountOutCall(isDeposit, index, amount, reserveIn, reserveOut, inv); if (amountOut * 1 ether / amountsOutSingleEthTarget < amount) break; amountInToSync = amount; if (endLoop) return (amountInToSync, amountOut); @@ -671,7 +703,7 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { endLoop = true; } - amountOut = amountOutCall(isDeposit, index, amount, reserveIn, reserveOut); + amountOut = amountOutCall(isDeposit, index, amount, reserveIn, reserveOut, inv); if (amountOut * 1 ether / amountsOutSingleEthTarget < amount) break; amountInToSync = amount; if (endLoop) return (amountInToSync, amountOut); @@ -756,15 +788,33 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { } } - function _swapUniV3Call(bool isReverse, uint24 fee, address tokenIn, address tokenOut, uint256 amountIn) internal virtual returns (uint256 amountOut) { + function _swapUniV3Call( + bool isReverse, + uint24 fee, + address tokenIn, + address tokenOut, + uint256 amountIn + ) + internal + view + virtual + returns (uint256 amountOut) + { uint160 sqrtPriceLimitX96 = isReverse ? MAX_SQRT_RATIO - 1 : MIN_SQRT_RATIO + 1; IQuoterV2.QuoteExactInputSingleParams memory quoterParams = IQuoterV2.QuoteExactInputSingleParams({ tokenIn: tokenIn, tokenOut: tokenOut, amountIn: amountIn, fee: fee, sqrtPriceLimitX96: sqrtPriceLimitX96 }); - try quoter.quoteExactInputSingle(quoterParams) returns (uint256 out) { - amountOut = out; - } catch { + bytes memory input = abi.encodeWithSelector(quoter.quoteExactInputSingle.selector, quoterParams); + (bool success, bytes memory data) = address(quoter).staticcall(input); + if (!success) { amountOut = 0; + } else { + (amountOut) = abi.decode(data, (uint256)); } + // try quoter.quoteExactInputSingle(quoterParams) returns (uint256 out) { + // amountOut = out; + // } catch { + // amountOut = 0; + // } // (amountOut,,,) = quoter.quoteExactInputSingle(quoterParams); } diff --git a/src/interfaces/ICurveV2Pool.sol b/src/interfaces/ICurveV2Pool.sol index 87deb58..8c8b7c4 100644 --- a/src/interfaces/ICurveV2Pool.sol +++ b/src/interfaces/ICurveV2Pool.sol @@ -3,35 +3,12 @@ pragma solidity ^0.8.10; interface ICurveV2Pool { function exchange(uint256 i, uint256 j, uint256 dx, uint256 min_dy, bool use_eth, address receiver) external payable returns (uint256 dy); - function add_liquidity(uint256[2] calldata amounts, uint256 min_mint_amount) external payable returns (uint256); - - function add_liquidity(uint256[2] calldata amounts, uint256 min_mint_amount, bool use_eth) external payable returns (uint256); - - function add_liquidity(uint256[2] calldata amounts, uint256 min_mint_amount, bool use_eth, address receiver) external payable returns (uint256); - - function remove_liquidity(uint256 _amount, uint256[2] calldata min_amounts) external; - - function remove_liquidity(uint256 _amount, uint256[2] calldata min_amounts, bool use_eth) external; - - function remove_liquidity(uint256 _amount, uint256[2] calldata min_amounts, bool use_eth, address receiver) external; - - function remove_liquidity_one_coin(uint256 token_amount, uint256 i, uint256 min_amount) external returns (uint256); - - function remove_liquidity_one_coin(uint256 token_amount, uint256 i, uint256 min_amount, bool use_eth) external returns (uint256); - - function remove_liquidity_one_coin(uint256 token_amount, uint256 i, uint256 min_amount, bool use_eth, address receiver) external returns (uint256); - function calc_token_amount(uint256[2] calldata amounts) external view returns (uint256); - - function calc_withdraw_one_coin(uint256 token_amount, uint256 i) external view returns (uint256); - - function lp_price() external view returns (uint256); - function token() external view returns (address); function coins(uint256 arg0) external view returns (address); function balances(uint256 arg0) external view returns (uint256); - function get_dy(uint256 i, uint256 j, uint256 dx) external returns (uint256); + function get_dy(uint256 i, uint256 j, uint256 dx) external view returns (uint256); } diff --git a/test/MevEthRouter.t.sol b/test/MevEthRouter.t.sol index c4d0b85..108f2df 100644 --- a/test/MevEthRouter.t.sol +++ b/test/MevEthRouter.t.sol @@ -40,36 +40,54 @@ contract MevEthRouterTest is DSTest { receive() external payable { } + /// @notice Fuzz test staking Eth with static call for route + /// @dev default client side staking eth process, using static call on route finder to save gas function testStakeEthRaw(uint80 amountIn) external { vm.assume(amountIn > 0.1 ether); vm.assume(amountIn < 100_000 ether); // uint256 amountIn = 100 ether; vm.deal(address(this), amountIn); uint256 amountOutMin = MEVETH.previewDeposit(amountIn) * 99 / 100; - IMevEthRouter.Swap memory swaps = router.getStakeRoute(amountIn, amountOutMin); + // test static call for route to save gas at client side + bytes memory input = abi.encodeWithSelector(router.getStakeRoute.selector, amountIn, amountOutMin); + (, bytes memory data) = address(router).staticcall(input); + (IMevEthRouter.Swap memory swaps) = abi.decode(data, (IMevEthRouter.Swap)); + // IMevEthRouter.Swap memory swaps = router.getStakeRoute(amountIn, amountOutMin); uint256 shares = router.stakeEthForMevEthRaw{ value: amountIn }(address(this), amountIn, amountOutMin, block.timestamp, swaps); assertGt(shares, 0); } + /// @notice Fuzz test redeeming MevEth with static call for route + /// @dev default client side redeem meveth process, using static call on route finder to save gas function testRedeemEthRaw(uint80 amountIn) external { vm.assume(amountIn > 2 ether); vm.assume(amountIn < 10_000 ether); // uint256 amountIn = 100 ether; vm.deal(address(this), amountIn); uint256 shares = router.stakeEthForMevEth{ value: amountIn }(address(this), amountIn, 1, block.timestamp); - uint256 amountOutMin = 1; + uint256 amountOutMin = 1; // fuzz for first test can give high slippage ERC20(address(MEVETH)).approve(address(router), shares); - IMevEthRouter.Swap memory swaps = router.getRedeemRoute(false, shares / 2, amountOutMin); + // test static call for route to save gas at client side + bytes memory input = abi.encodeWithSelector(router.getRedeemRoute.selector, false, shares / 2, amountOutMin); + (, bytes memory data) = address(router).staticcall(input); + (IMevEthRouter.Swap memory swaps) = abi.decode(data, (IMevEthRouter.Swap)); + // IMevEthRouter.Swap memory swaps = router.getRedeemRoute(false, shares / 2, amountOutMin); + // Test half shares with no queue and any slippage (since fuzz will create high orders) uint256 assets = router.redeemMevEthForEthRaw(false, address(this), shares / 2, amountOutMin, block.timestamp, swaps); assertGt(assets, 0); - swaps = router.getRedeemRoute(true, shares / 2, amountOutMin); + // swaps = router.getRedeemRoute(true, shares / 2, amountOutMin); + input = abi.encodeWithSelector(router.getRedeemRoute.selector, true, shares / 2, amountOutMin); + (, data) = address(router).staticcall(input); + (swaps) = abi.decode(data, (IMevEthRouter.Swap)); + // Test other half shares with queue assets = router.redeemMevEthForEthRaw(true, address(this), shares / 2, amountOutMin, block.timestamp, swaps); assertGt(assets, 0); } + /// @notice Fuzz test staking Eth function testStakeEth(uint80 amountIn) external { vm.assume(amountIn > 0.1 ether); - vm.assume(amountIn < 100000 ether); + vm.assume(amountIn < 100_000 ether); // uint256 amountIn = 100 ether; vm.deal(address(this), amountIn); uint256 amountOutMin = MEVETH.previewDeposit(amountIn) * 99 / 100; @@ -77,9 +95,10 @@ contract MevEthRouterTest is DSTest { assertGt(shares, 0); } + /// @notice Fuzz test redeeming Eth function testRedeemEth(uint80 amountIn) external { vm.assume(amountIn > 2 ether); - vm.assume(amountIn < 10000 ether); + vm.assume(amountIn < 10_000 ether); // uint256 amountIn = 100 ether; vm.deal(address(this), amountIn); uint256 shares = router.stakeEthForMevEth{ value: amountIn }(address(this), amountIn, 1, block.timestamp);