From 16257d99b1bb09a683df1e70cdf91dc4b7cc5dd7 Mon Sep 17 00:00:00 2001 From: sandybradley Date: Tue, 9 Jan 2024 09:13:53 +0200 Subject: [PATCH 1/6] chore: downsize contract --- src/MevEthRouter.sol | 223 +++++++++------------------- src/interfaces/IMevEthRouter.sol | 10 +- src/interfaces/IUniswapV2Router.sol | 53 ------- test/MevEthRouter.t.sol | 101 ++----------- 4 files changed, 87 insertions(+), 300 deletions(-) delete mode 100644 src/interfaces/IUniswapV2Router.sol diff --git a/src/MevEthRouter.sol b/src/MevEthRouter.sol index be5534c..d9ed39f 100644 --- a/src/MevEthRouter.sol +++ b/src/MevEthRouter.sol @@ -63,13 +63,14 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { address internal gov; /// @dev Curve V2 pool address address internal curveV2Pool = 0x429cCFCCa8ee06D2B41DAa6ee0e4F0EdBB77dFad; - /// @dev Balancer pool id - bytes32 internal poolId = 0xb3b675a9a3cb0df8f66caf08549371bfb76a9867000200000000000000000611; /// @dev Gyro pool IGyro internal constant gyro = IGyro(0xb3b675a9A3CB0DF8F66Caf08549371BfB76A9867); IRateProvider internal constant rateProvider0 = IRateProvider(0xf518f2EbeA5df8Ca2B5E9C7996a2A25e8010014b); + /// @dev Balancer pool id + bytes32 internal poolId = 0xb3b675a9a3cb0df8f66caf08549371bfb76a9867000200000000000000000611; + uint256[3] internal uniV3Caps = [0, 0, 15 ether]; IGyroECLPMath.Params internal params; @@ -93,48 +94,13 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { (params, derived) = gyro.getECLPParams(); } - /// @notice Perform optimal route for converting Eth => MevEth - /// @param receiver Address of MevEth receiver - /// @param amountIn Amount of eth or weth to deposit - /// @param amountOutMin Min amount of MevEth to receive - /// @param deadline Timestamp deadline - /// @return shares Amount of MevEth received - function stakeEthForMevEth(address receiver, uint256 amountIn, uint256 amountOutMin, uint256 deadline) external payable returns (uint256 shares) { - // check inputs + function checkInputs(address receiver, uint256 amountIn, uint256 deadline) internal view { // check receiver if (receiver == address(0)) revert ZeroAddress(); // check block.timestamp > deadline (timestamp in seconds) ensure(deadline); // check amountIn if (amountIn == 0) revert ZeroAmount(); - // check eth or weth deposit - if (msg.value != amountIn) { - // either weth or wrong amount - // transfer weth amountIn from sender, will revert if insufficient allowance - WETH09.safeTransferFrom(msg.sender, address(this), amountIn); - } else { - WETH09.deposit{ value: amountIn }(); - } - // get optimal route - Swap memory swaps = getStakeRoute(amountIn, amountOutMin); - - // UniV2 / Sushi require amounts transfered directly to pool - for (uint256 i; i < 2; i = _inc(i)) { - if (_isNonZero(swaps.pools[i].amountIn)) { - WETH09.safeTransfer(swaps.pools[i].pair, swaps.pools[i].amountIn); - } - } - - // execute swaps, retreive actual amounts - uint256[] memory amounts = _swap(true, receiver, deadline, swaps); - // check output is sufficient - if (amountOutMin > amounts[1]) revert InsufficientOutputAmount(); - // assign returned shares - shares = amounts[1]; - // refund V3 dust if any - if (amounts[0] < amountIn && (amountIn - amounts[0]) > 50_000 * block.basefee) { - WETH09.safeTransfer(msg.sender, amountIn - amounts[0]); - } } /// @notice Gas efficient stakeEthForMevEth @@ -145,7 +111,7 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { /// @param deadline Timestamp deadline /// @param swaps output of getStakeRoute /// @return shares Amount of MevEth received - function stakeEthForMevEthRaw( + function stakeEthForMevEth( address receiver, uint256 amountIn, uint256 amountOutMin, @@ -157,12 +123,7 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { returns (uint256 shares) { // check inputs - // check receiver - if (receiver == address(0)) revert ZeroAddress(); - // check block.timestamp > deadline (timestamp in seconds) - ensure(deadline); - // check amountIn - if (amountIn == 0) revert ZeroAmount(); + checkInputs(receiver, amountIn, deadline); // check eth or weth deposit if (msg.value != amountIn) { // either weth or wrong amount @@ -191,45 +152,6 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { } } - /// @notice Perform optimal route for converting MevEth => Eth - /// @param receiver Address of Eth receiver - /// @param shares Amount of meveth to redeem - /// @param amountOutMin Min amount of eth to receive - /// @param deadline Timestamp deadline - /// @return assets Eth received - function redeemMevEthForEth(bool useQueue, address receiver, uint256 shares, uint256 amountOutMin, uint256 deadline) external returns (uint256 assets) { - // check inputs - // check receiver - if (receiver == address(0)) revert ZeroAddress(); - // check block.timestamp > deadline (timestamp in seconds) - ensure(deadline); - // check amountIn - if (shares == 0) revert ZeroAmount(); - // check eth or weth deposit - ERC20(address(MEVETH)).safeTransferFrom(msg.sender, address(this), shares); - - // get optimal route - Swap memory swaps = getRedeemRoute(useQueue, shares, amountOutMin); - - // UniV2 / Sushi require amounts transfered directly to pool - for (uint256 i; i < 2; i = _inc(i)) { - if (_isNonZero(swaps.pools[i].amountIn)) { - ERC20(address(MEVETH)).safeTransfer(swaps.pools[i].pair, swaps.pools[i].amountIn); - } - } - - // execute swaps, retreive actual amounts - uint256[] memory amounts = _swap(useQueue, receiver, deadline, swaps); - // check output is sufficient - if (amountOutMin > amounts[1]) revert InsufficientOutputAmount(); - // assign returned shares - assets = amounts[1]; - // refund V3 dust if any - if (amounts[0] < shares && (shares - amounts[0]) > 50_000 * block.basefee) { - ERC20(address(MEVETH)).safeTransfer(msg.sender, shares - amounts[0]); - } - } - /// @notice Gas efficient redeemMevEthForEth /// @dev requires calling getRedeemRoute first /// @param receiver Address of Eth receiver @@ -238,7 +160,7 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { /// @param deadline Timestamp deadline /// @param swaps output of getRedeemRoute /// @return assets Eth received - function redeemMevEthForEthRaw( + function redeemMevEthForEth( bool useQueue, address receiver, uint256 shares, @@ -250,12 +172,7 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { returns (uint256 assets) { // check inputs - // check receiver - if (receiver == address(0)) revert ZeroAddress(); - // check block.timestamp > deadline (timestamp in seconds) - ensure(deadline); - // check amountIn - if (shares == 0) revert ZeroAmount(); + checkInputs(receiver, shares, deadline); // check eth or weth deposit ERC20(address(MEVETH)).safeTransferFrom(msg.sender, address(this), shares); @@ -292,8 +209,8 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { 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); + // bytes memory data = abi.encodeWithSelector(0x78ace857, balances, params, derived); + (, bytes memory returnData) = address(gyroMath).staticcall(abi.encodeWithSelector(0x78ace857, balances, params, derived)); (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); @@ -303,7 +220,7 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { /// @param amountIn Amount in for first token in path /// @param amountOutMin Min amount out /// @return swaps struct for split order details - function getStakeRoute(uint256 amountIn, uint256 amountOutMin) public view returns (Swap memory swaps) { + function getStakeRoute(uint256 amountIn, uint256 amountOutMin) internal view returns (Swap memory swaps) { swaps.pools = _getPools(); swaps.tokenIn = address(WETH09); swaps.tokenOut = address(MEVETH); @@ -325,7 +242,7 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { /// @param amountIn Amount in for first token in path /// @param amountOutMin Min amount out /// @return swaps struct for split order details - function getRedeemRoute(bool useQueue, uint256 amountIn, uint256 amountOutMin) public view returns (Swap memory swaps) { + function getRedeemRoute(bool useQueue, uint256 amountIn, uint256 amountOutMin) internal view returns (Swap memory swaps) { swaps.pools = _getPools(); swaps.tokenIn = address(MEVETH); swaps.tokenOut = address(WETH09); @@ -519,9 +436,6 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { * Essentially copied from the 3CLP. */ function _getAllBalances() internal view returns (uint256[] memory balances) { - // The below is more gas-efficient than the following line because the token slots don't have to be read in the - // vault. - // (, uint256[] memory balances, ) = getVault().getPoolTokens(getPoolId()); balances = new uint256[](2); balances[0] = _getScaledTokenBalance(address(MEVETH), _scalingFactor(true)); balances[1] = _getScaledTokenBalance(address(WETH09), _scalingFactor(false)); @@ -539,20 +453,21 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { view returns (uint256 amountOut) { - uint256 scalingFactorTokenIn = _scalingFactor(!isDeposit); - uint256 scalingFactorTokenOut = _scalingFactor(isDeposit); + // uint256 scalingFactorTokenIn = _scalingFactor(!isDeposit); + // uint256 scalingFactorTokenOut = _scalingFactor(isDeposit); uint256[] memory balances = new uint256[](2); balances[0] = isDeposit ? reserveOut : reserveIn; balances[1] = isDeposit ? reserveIn : reserveOut; - uint256 feeAmount = amountIn * MevEthLibrary.getFee(5) / 1_000_000; - amountIn = (amountIn - feeAmount) * scalingFactorTokenIn / 1 ether; + // uint256 feeAmount = amountIn * MevEthLibrary.getFee(5) / 1_000_000; + // amountIn = (amountIn - feeAmount) * _scalingFactor(!isDeposit) / 1 ether; + amountIn = (amountIn - amountIn * MevEthLibrary.getFee(5) / 1_000_000) * _scalingFactor(!isDeposit) / 1 ether; // same selector workaround here - bytes memory data = abi.encodeWithSelector(0x61ff4236, balances, amountIn, !isDeposit, params, derived, inv); - (, bytes memory returnData) = address(gyroMath).staticcall(data); + // bytes memory data = abi.encodeWithSelector(0x61ff4236, balances, amountIn, !isDeposit, params, derived, inv); + (, bytes memory returnData) = address(gyroMath).staticcall(abi.encodeWithSelector(0x61ff4236, balances, amountIn, !isDeposit, params, derived, inv)); // amountOut = gyroMath.calcOutGivenIn(balances, amountIn, !isDeposit, params, derived, inv) * 1 ether / scalingFactorTokenOut; - amountOut = abi.decode(returnData, (uint256)) * 1 ether / scalingFactorTokenOut; + amountOut = abi.decode(returnData, (uint256)) * 1 ether / _scalingFactor(isDeposit); } function amountOutCall( @@ -617,34 +532,18 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { return (amountsIn, amountsOut); } - // check split estimate - uint256 numPools; - for (uint256 i; i < 8; i = _inc(i)) { - if (_isZero(amountsOutSingleEth[i])) continue; - unchecked { - ++numPools; - } - } - - if (numPools < 2) { - amountsIn[index[7]] = amountIn; // set best price as default, before splitting - amountsOut[index[7]] = amountsOutSingleSwap[index[7]]; - return (amountsIn, amountsOut); - } uint256[8] memory index2 = MevEthLibrary._sortArray(amountsOutSingleEth); // sorts in ascending order (i.e. best price is last) if (_isNonZero(amountsOutSingleEth[index2[7]])) { uint256 cumulativeAmount; // calculate amount to sync prices cascading through each pool with best prices first, while cumulative amount < amountIn for (uint256 i = 7; _isNonZero(i); i = _dec(i)) { - if (index2[i] == 7) { + if (index2[i] == 7 || _isZero(amountsOutSingleEth[index2[_dec(i)]])) { amountsIn[index2[i]] = amountIn - cumulativeAmount; amountsOut[index2[i]] = 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 - if (_isZero(amountsOutSingleEth[index2[_dec(i)]])) break; - (amountsIn[index2[i]], amountsOut[index2[i]]) = amountToSync( isDeposit, amountIn, @@ -655,13 +554,19 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { reserves[index2[i]].reserveOut, inv ); + // UniV3 adjustment // Expensive in gas, so only used for high values and preferably called off-chain - if (index2[i] < 5 && index2[i] > 1 && amountsIn[index2[i]] > 8 * MIN_LIQUIDITY) { - address tokenIn = isDeposit ? address(WETH09) : address(MEVETH); - address tokenOut = isDeposit ? address(MEVETH) : address(WETH09); - amountsOut[index2[i]] = - _swapUniV3Call(!isDeposit, uint24(MevEthLibrary.getFee(index2[i])), tokenIn, tokenOut, amountsIn[index2[i]]); + if (index2[i] < 5 && index2[i] > 1 && amountsIn[index2[i]] > uniV3Caps[index2[i]] / 2) { + // address tokenIn = isDeposit ? address(WETH09) : address(MEVETH); + // address tokenOut = isDeposit ? address(MEVETH) : address(WETH09); + amountsOut[index2[i]] = _swapUniV3Call( + !isDeposit, + uint24(MevEthLibrary.getFee(index2[i])), + isDeposit ? address(WETH09) : address(MEVETH), + isDeposit ? address(MEVETH) : address(WETH09), + amountsIn[index2[i]] + ); if (amountsOut[index2[i]] == 0) { amountsIn[index2[i]] = 0; } @@ -687,23 +592,23 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { view returns (uint256 amountInToSync, uint256 amountOut) { - // todo: make more efficient uint256 amount; uint256 chunk = amountIn / 10; - uint256 precision = 0.1 ether; - if (chunk < precision) { - chunk = precision; + // uint256 precision = 0.1 ether; + if (chunk < 0.1 ether) { + chunk = 0.1 ether; } for (uint256 i; i < 10; i = _inc(i)) { amount = amount + chunk; + bool endLoop; if (index > 1 && index < 5 && amount > uniV3Caps[index - 2]) { amount = uniV3Caps[index - 2]; endLoop = true; // hard cap univ3 amounts as they become more unpredictable } - if (amount + precision > amountIn - cumulativeAmount) { + if (amount + 0.1 ether > amountIn - cumulativeAmount) { amount = amountIn - cumulativeAmount; endLoop = true; } @@ -712,14 +617,20 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { return (amountInToSync, amountOut); } - amountOut = amountOutCall(isDeposit, index, amount, reserveIn, reserveOut, inv); - if (amountOut * 1 ether / amountsOutSingleEthTarget < amount) break; + { + uint256 amountOutTmp = amountOutCall(isDeposit, index, amount, reserveIn, reserveOut, inv); + if (amountOutTmp * 1 ether / amountsOutSingleEthTarget < amount) break; + amountOut = amountOutTmp; + } + amountInToSync = amount; + if (endLoop) return (amountInToSync, amountOut); } // refine - if (chunk > precision) { + if (chunk > 0.1 ether) { amount = amountInToSync; + chunk = chunk / 10; for (uint256 i; i < 10; i = _inc(i)) { amount = amount + chunk; @@ -729,9 +640,14 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { endLoop = true; } - amountOut = amountOutCall(isDeposit, index, amount, reserveIn, reserveOut, inv); - if (amountOut * 1 ether / amountsOutSingleEthTarget < amount) break; + { + uint256 amountOutTmp = amountOutCall(isDeposit, index, amount, reserveIn, reserveOut, inv); + if (amountOut * 1 ether / amountsOutSingleEthTarget < amount) break; + amountOut = amountOutTmp; + } + amountInToSync = amount; + if (endLoop) return (amountInToSync, amountOut); } } @@ -799,14 +715,12 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { virtual returns (uint256 amountInActual, uint256 amountOut) { - bytes memory data = abi.encodePacked(tokenIn, tokenOut, fee); - uint160 sqrtPriceLimitX96 = isReverse ? MAX_SQRT_RATIO - 1 : MIN_SQRT_RATIO + 1; - - // (int256 amount0, int256 amount1) = IUniswapV3Pool(pair).swap(to, !isReverse, int256(amountIn), sqrtPriceLimitX96, data); - // amountOut = isReverse ? uint256(-(amount0)) : uint256(-(amount1)); - // amountInActual = isReverse ? uint256(amount1) : uint256(amount0); + // bytes memory data = abi.encodePacked(tokenIn, tokenOut, fee); + // uint160 sqrtPriceLimitX96 = isReverse ? MAX_SQRT_RATIO - 1 : MIN_SQRT_RATIO + 1; - try IUniswapV3Pool(pair).swap(to, !isReverse, int256(amountIn), sqrtPriceLimitX96, data) returns (int256 amount0, int256 amount1) { + try IUniswapV3Pool(pair).swap( + to, !isReverse, int256(amountIn), isReverse ? MAX_SQRT_RATIO - 1 : MIN_SQRT_RATIO + 1, abi.encodePacked(tokenIn, tokenOut, fee) + ) returns (int256 amount0, int256 amount1) { amountOut = isReverse ? uint256(-(amount0)) : uint256(-(amount1)); amountInActual = isReverse ? uint256(amount1) : uint256(amount0); } catch { @@ -826,22 +740,21 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { 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 }); - bytes memory input = abi.encodeWithSelector(quoter.quoteExactInputSingle.selector, quoterParams); - (bool success, bytes memory data) = address(quoter).staticcall(input); + // 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: isReverse ? MAX_SQRT_RATIO - 1 : MIN_SQRT_RATIO + 1 + }); + // bytes memory input = abi.encodeWithSelector(quoter.quoteExactInputSingle.selector, quoterParams); + (bool success, bytes memory data) = address(quoter).staticcall(abi.encodeWithSelector(quoter.quoteExactInputSingle.selector, quoterParams)); 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); } /// @dev Internal core swap. Requires the initial amount to have already been sent to the first pair (for v2 pairs). diff --git a/src/interfaces/IMevEthRouter.sol b/src/interfaces/IMevEthRouter.sol index 4c64423..9125c4d 100644 --- a/src/interfaces/IMevEthRouter.sol +++ b/src/interfaces/IMevEthRouter.sol @@ -26,8 +26,8 @@ interface IMevEthRouter { function amountOutStake(uint256 amountIn) external view returns (uint256 amountOut, Swap memory swaps); function amountOutRedeem(bool useQueue, uint256 amountIn) external view returns (uint256 amountOut, Swap memory swaps); - function stakeEthForMevEth(address receiver, uint256 amountIn, uint256 amountOutMin, uint256 deadline) external payable returns (uint256 shares); - function stakeEthForMevEthRaw( + + function stakeEthForMevEth( address receiver, uint256 amountIn, uint256 amountOutMin, @@ -37,8 +37,8 @@ interface IMevEthRouter { external payable returns (uint256 shares); - function redeemMevEthForEth(bool useQueue, address receiver, uint256 shares, uint256 amountOutMin, uint256 deadline) external returns (uint256 assets); - function redeemMevEthForEthRaw( + + function redeemMevEthForEth( bool useQueue, address receiver, uint256 shares, @@ -48,6 +48,4 @@ interface IMevEthRouter { ) external returns (uint256 assets); - function getStakeRoute(uint256 amountIn, uint256 amountOutMin) external returns (Swap memory swaps); - function getRedeemRoute(bool useQueue, uint256 amountIn, uint256 amountOutMin) external returns (Swap memory swaps); } diff --git a/src/interfaces/IUniswapV2Router.sol b/src/interfaces/IUniswapV2Router.sol deleted file mode 100644 index 55d9faf..0000000 --- a/src/interfaces/IUniswapV2Router.sol +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED - -pragma solidity >=0.8.13 <0.9.0; - -interface IUniswapV2Router01 { - function factory() external pure returns (address); - - - function swapExactTokensForTokens( - uint256 amountIn, - uint256 amountOutMin, - address[] calldata path, - address to, - uint256 deadline - ) - external - returns (uint256[] memory amounts); - - function swapExactETHForTokens( - uint256 amountOutMin, - address[] calldata path, - address to, - uint256 deadline - ) - external - payable - returns (uint256[] memory amounts); - - function swapExactTokensForETH( - uint256 amountIn, - uint256 amountOutMin, - address[] calldata path, - address to, - uint256 deadline - ) - external - returns (uint256[] memory amounts); - - - function quote(uint256 amountA, uint256 reserveA, uint256 reserveB) external pure returns (uint256 amountB); - - function getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut) external pure returns (uint256 amountOut); - - function getAmountIn(uint256 amountOut, uint256 reserveIn, uint256 reserveOut) external pure returns (uint256 amountIn); - - function getAmountsOut(uint256 amountIn, address[] calldata path) external view returns (uint256[] memory amounts); - - function getAmountsIn(uint256 amountOut, address[] calldata path) external view returns (uint256[] memory amounts); -} - -interface IUniswapV2Router02 is IUniswapV2Router01 { - -} diff --git a/test/MevEthRouter.t.sol b/test/MevEthRouter.t.sol index 90277e1..093469f 100644 --- a/test/MevEthRouter.t.sol +++ b/test/MevEthRouter.t.sol @@ -5,7 +5,6 @@ pragma solidity >=0.8.13 <0.9.0; import "forge-std/Test.sol"; import { Vm } from "forge-std/Vm.sol"; import { MevEthRouter } from "../src/MevEthRouter.sol"; -import { IUniswapV2Router02 } from "../src/interfaces/IUniswapV2Router.sol"; import { IUniswapV2Pair } from "../src/interfaces/IUniswapV2Pair.sol"; import { IWETH } from "../src/interfaces/IWETH.sol"; import { IMevEth } from "../src/interfaces/IMevEth.sol"; @@ -25,8 +24,6 @@ contract MevEthRouterTest is DSTest { IMevEth internal constant MEVETH = IMevEth(0x24Ae2dA0f361AA4BE46b48EB19C91e02c5e4f27E); IWETH weth = IWETH(WETH); - IUniswapV2Router02 uniRouter = IUniswapV2Router02(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D); - IUniswapV2Router02 routerOld = IUniswapV2Router02(0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F); uint256 minLiquidity = uint256(1000); function setUp() public { @@ -40,99 +37,31 @@ contract MevEthRouterTest is DSTest { receive() external payable { } - /// @dev Fuzz test amountOut Stake call - function testamountOutStake(uint80 amountIn) external { + /// @dev Fuzz test amountOut, Stake and redeem + function testStakeAndRedeem(uint80 amountIn) external { vm.assume(amountIn > 0.1 ether); vm.assume(amountIn < 100_000 ether); - // vm.deal(address(this), amountIn); + vm.deal(address(this), amountIn); + // test getting swap route bytes memory input = abi.encodeWithSelector(router.amountOutStake.selector, amountIn); (, bytes memory data) = address(router).staticcall(input); - (uint256 amountOut, ) = abi.decode(data, (uint256, IMevEthRouter.Swap)); + (uint256 amountOut, IMevEthRouter.Swap memory swaps) = abi.decode(data, (uint256, IMevEthRouter.Swap)); assertGt(amountOut, 0); - } - - /// @dev Fuzz test amountOut Redeem call - function testamountOutRedeem(uint80 amountIn) external { - vm.assume(amountIn > 0.1 ether); - vm.assume(amountIn < 10_000 ether); - // vm.deal(address(this), amountIn); + // test stake + uint256 shares = router.stakeEthForMevEth{ value: amountIn }(address(this), amountIn, amountOut * 99 / 100, block.timestamp, swaps); + assertGt(shares, 0); + // test redeem route bool useQueue; - if (amountIn > 15 ether){ + if (amountIn > 15 ether) { useQueue = true; } - bytes memory input = abi.encodeWithSelector(router.amountOutRedeem.selector, useQueue, amountIn); - (, bytes memory data) = address(router).staticcall(input); - (uint256 amountOut, ) = abi.decode(data, (uint256, IMevEthRouter.Swap)); - assertGt(amountOut, 0); - } - - /// @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; - // 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; // fuzz for first test can give high slippage - ERC20(address(MEVETH)).approve(address(router), shares); - // 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); - input = abi.encodeWithSelector(router.getRedeemRoute.selector, true, shares / 2, amountOutMin); + input = abi.encodeWithSelector(router.amountOutRedeem.selector, useQueue, shares); (, 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 < 100_000 ether); - // uint256 amountIn = 100 ether; - vm.deal(address(this), amountIn); - uint256 amountOutMin = MEVETH.previewDeposit(amountIn) * 99 / 100; - uint256 shares = router.stakeEthForMevEth{ value: amountIn }(address(this), amountIn, amountOutMin, block.timestamp); - assertGt(shares, 0); - } - - /// @notice Fuzz test redeeming Eth - function testRedeemEth(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; + (amountOut, swaps) = abi.decode(data, (uint256, IMevEthRouter.Swap)); + assertGt(amountOut, 0); + // test redeem ERC20(address(MEVETH)).approve(address(router), shares); - uint256 assets = router.redeemMevEthForEth(false, address(this), shares / 2, amountOutMin, block.timestamp); - assertGt(assets, 0); - assets = router.redeemMevEthForEth(true, address(this), shares / 2, amountOutMin, block.timestamp); + uint256 assets = router.redeemMevEthForEth(useQueue, address(this), shares, amountOut * 99 / 100, block.timestamp, swaps); assertGt(assets, 0); } } From 89dd1f1a4392dacd36928f486aea8c3a443c4927 Mon Sep 17 00:00:00 2001 From: sandybradley Date: Tue, 9 Jan 2024 21:09:46 +0200 Subject: [PATCH 2/6] chore: minor useQueue amountout correction --- src/MevEthRouter.sol | 3 ++- test/MevEthRouter.t.sol | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/MevEthRouter.sol b/src/MevEthRouter.sol index d9ed39f..8e24460 100644 --- a/src/MevEthRouter.sol +++ b/src/MevEthRouter.sol @@ -839,7 +839,8 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { } else { ERC20(address(MEVETH)).approve(address(MEVETH), amountIn); if (useQueue) { - amounts[1] = amounts[1] + MEVETH.withdrawQueue(MEVETH.previewRedeem(amountIn) - 1, to, address(this)); + MEVETH.withdrawQueue(MEVETH.previewRedeem(amountIn) - 1, to, address(this)); + amounts[1] = amounts[1] + MEVETH.previewRedeem(amountIn); } else { amounts[1] = amounts[1] + MEVETH.redeem(amountIn, to, address(this)); } diff --git a/test/MevEthRouter.t.sol b/test/MevEthRouter.t.sol index 093469f..d8992c2 100644 --- a/test/MevEthRouter.t.sol +++ b/test/MevEthRouter.t.sol @@ -46,7 +46,7 @@ contract MevEthRouterTest is DSTest { bytes memory input = abi.encodeWithSelector(router.amountOutStake.selector, amountIn); (, bytes memory data) = address(router).staticcall(input); (uint256 amountOut, IMevEthRouter.Swap memory swaps) = abi.decode(data, (uint256, IMevEthRouter.Swap)); - assertGt(amountOut, 0); + assertGt(amountOut, MEVETH.previewDeposit(amountIn) * 98 / 100); // test stake uint256 shares = router.stakeEthForMevEth{ value: amountIn }(address(this), amountIn, amountOut * 99 / 100, block.timestamp, swaps); assertGt(shares, 0); @@ -58,7 +58,7 @@ contract MevEthRouterTest is DSTest { input = abi.encodeWithSelector(router.amountOutRedeem.selector, useQueue, shares); (, data) = address(router).staticcall(input); (amountOut, swaps) = abi.decode(data, (uint256, IMevEthRouter.Swap)); - assertGt(amountOut, 0); + assertGt(amountOut, MEVETH.previewRedeem(shares) * 95 /100); // test redeem ERC20(address(MEVETH)).approve(address(router), shares); uint256 assets = router.redeemMevEthForEth(useQueue, address(this), shares, amountOut * 99 / 100, block.timestamp, swaps); From f72a5c097e763f51531c0cc3982521c3ccaf5783 Mon Sep 17 00:00:00 2001 From: sandybradley Date: Tue, 9 Jan 2024 22:54:14 +0200 Subject: [PATCH 3/6] chore: refine meveth withdraw calc --- src/MevEthRouter.sol | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/MevEthRouter.sol b/src/MevEthRouter.sol index 8e24460..343f193 100644 --- a/src/MevEthRouter.sol +++ b/src/MevEthRouter.sol @@ -391,16 +391,16 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { } } else { if (useQueue) { - amountsOutSingleSwap[7] = reserves[7].reserveIn * amountIn * 9999 / (10_000 * reserves[7].reserveOut); + amountsOutSingleSwap[7] = reserves[7].reserveOut * amountIn * 9999 / (10_000 * reserves[7].reserveIn); if (amountIn > MIN_LIQUIDITY) { - amountsOutSingleEth[7] = reserves[7].reserveIn * 1 ether * 9999 / (10_000 * reserves[7].reserveOut); + amountsOutSingleEth[7] = reserves[7].reserveOut * 1 ether * 9999 / (10_000 * reserves[7].reserveIn); } } else { uint256 bal = address(MEVETH).balance; if (bal > amountIn) { - amountsOutSingleSwap[7] = reserves[7].reserveIn * amountIn * 9999 / (10_000 * reserves[7].reserveOut); + amountsOutSingleSwap[7] = reserves[7].reserveOut * amountIn * 9999 / (10_000 * reserves[7].reserveIn); if (amountIn > MIN_LIQUIDITY) { - amountsOutSingleEth[7] = reserves[7].reserveIn * 1 ether * 9999 / (10_000 * reserves[7].reserveOut); + amountsOutSingleEth[7] = reserves[7].reserveOut * 1 ether * 9999 / (10_000 * reserves[7].reserveIn); } } // todo: partial withdraws based on balance @@ -507,7 +507,7 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { // MevEth if (i == 7) { - amountOut = isDeposit ? reserveOut * amountIn / reserveIn : reserveIn * amountIn * 9999 / (10_000 * reserveOut); + amountOut = isDeposit ? reserveOut * amountIn / reserveIn : reserveOut * amountIn * 9999 / (10_000 * reserveIn); } } @@ -822,7 +822,6 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { if (_isNonZero(amountIn)) { if (swaps.tokenIn == address(WETH09)) { // Contrary to the other pools, token order is WETH / MEVETH - // todo: calc limit WETH09.approve(curveV2Pool, amountIn); amounts[1] = amounts[1] + ICurveV2Pool(curveV2Pool).exchange(0, 1, amountIn, 1, false, to); } else { From 1fcefe661127fb9b7837eee573dc98eacf5c81c5 Mon Sep 17 00:00:00 2001 From: sandybradley Date: Thu, 11 Jan 2024 11:21:18 +0200 Subject: [PATCH 4/6] fix: amountout call adge case fix and isDeposit call setting --- src/MevEthRouter.sol | 3 ++- test/MevEthRouter.t.sol | 37 ++++++++++++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/MevEthRouter.sol b/src/MevEthRouter.sol index 343f193..c697750 100644 --- a/src/MevEthRouter.sol +++ b/src/MevEthRouter.sol @@ -221,6 +221,7 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { /// @param amountOutMin Min amount out /// @return swaps struct for split order details function getStakeRoute(uint256 amountIn, uint256 amountOutMin) internal view returns (Swap memory swaps) { + swaps.isDeposit = true; swaps.pools = _getPools(); swaps.tokenIn = address(WETH09); swaps.tokenOut = address(MEVETH); @@ -642,7 +643,7 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { { uint256 amountOutTmp = amountOutCall(isDeposit, index, amount, reserveIn, reserveOut, inv); - if (amountOut * 1 ether / amountsOutSingleEthTarget < amount) break; + if (amountOutTmp * 1 ether / amountsOutSingleEthTarget < amount) break; amountOut = amountOutTmp; } diff --git a/test/MevEthRouter.t.sol b/test/MevEthRouter.t.sol index d8992c2..62b138b 100644 --- a/test/MevEthRouter.t.sol +++ b/test/MevEthRouter.t.sol @@ -46,7 +46,33 @@ contract MevEthRouterTest is DSTest { bytes memory input = abi.encodeWithSelector(router.amountOutStake.selector, amountIn); (, bytes memory data) = address(router).staticcall(input); (uint256 amountOut, IMevEthRouter.Swap memory swaps) = abi.decode(data, (uint256, IMevEthRouter.Swap)); - assertGt(amountOut, MEVETH.previewDeposit(amountIn) * 98 / 100); + assertGt(amountOut, MEVETH.previewDeposit(amountIn) * 999 / 1000); + // test stake + uint256 shares = router.stakeEthForMevEth{ value: amountIn }(address(this), amountIn, amountOut * 99 / 100, block.timestamp, swaps); + assertGt(shares, amountOut * 99 / 100); + // test redeem route + bool useQueue; + if (amountIn > 15 ether) { + useQueue = true; + } + input = abi.encodeWithSelector(router.amountOutRedeem.selector, useQueue, shares); + (, data) = address(router).staticcall(input); + (amountOut, swaps) = abi.decode(data, (uint256, IMevEthRouter.Swap)); + assertGt(amountOut, MEVETH.previewRedeem(shares) * 95 /100); + // test redeem + ERC20(address(MEVETH)).approve(address(router), shares); + uint256 assets = router.redeemMevEthForEth(useQueue, address(this), shares, amountOut * 99 / 100, block.timestamp, swaps); + assertGt(assets, 0); + } + + function testStakePools() external { + uint256 amountIn = 10 ether; + vm.deal(address(this), amountIn); + // test getting swap route + bytes memory input = abi.encodeWithSelector(router.amountOutStake.selector, amountIn); + (, bytes memory data) = address(router).staticcall(input); + (uint256 amountOut, IMevEthRouter.Swap memory swaps) = abi.decode(data, (uint256, IMevEthRouter.Swap)); + assertGt(amountOut, MEVETH.previewDeposit(amountIn) * 999 / 1000); // test stake uint256 shares = router.stakeEthForMevEth{ value: amountIn }(address(this), amountIn, amountOut * 99 / 100, block.timestamp, swaps); assertGt(shares, 0); @@ -64,4 +90,13 @@ contract MevEthRouterTest is DSTest { uint256 assets = router.redeemMevEthForEth(useQueue, address(this), shares, amountOut * 99 / 100, block.timestamp, swaps); assertGt(assets, 0); } + + function testEdgeAmount() external { + // test redeem route + bool useQueue = false; + bytes memory input = abi.encodeWithSelector(router.amountOutRedeem.selector, useQueue, 6 ether); + (,bytes memory data) = address(router).staticcall(input); + (uint256 amountOut, ) = abi.decode(data, (uint256, IMevEthRouter.Swap)); + assertGt(amountOut, 5.98 ether); + } } From e6f5adb5bcd5f96288310315b3af672bc39ab75c Mon Sep 17 00:00:00 2001 From: sandybradley Date: Fri, 12 Jan 2024 00:03:13 +0200 Subject: [PATCH 5/6] fix: uniV3 adjustments and remove partial redeems --- foundry.toml | 2 +- src/MevEthRouter.sol | 51 +++++++++++++------------------- src/interfaces/IRateProvider.sol | 1 - test/MevEthRouter.t.sol | 46 ++++++++++++++++++---------- 4 files changed, 51 insertions(+), 49 deletions(-) diff --git a/foundry.toml b/foundry.toml index 3707064..6a831a5 100644 --- a/foundry.toml +++ b/foundry.toml @@ -20,7 +20,7 @@ stackAllocation = true mainnet = { key = "${ETHERSCAN_API}" } [rpc_endpoints] -mainnet = "" +mainnet = "${RPC_MAINNET}" optimism = "" arbitrum = "" polygon = "" diff --git a/src/MevEthRouter.sol b/src/MevEthRouter.sol index c697750..f15fb3f 100644 --- a/src/MevEthRouter.sol +++ b/src/MevEthRouter.sol @@ -396,15 +396,6 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { if (amountIn > MIN_LIQUIDITY) { amountsOutSingleEth[7] = reserves[7].reserveOut * 1 ether * 9999 / (10_000 * reserves[7].reserveIn); } - } else { - uint256 bal = address(MEVETH).balance; - if (bal > amountIn) { - amountsOutSingleSwap[7] = reserves[7].reserveOut * amountIn * 9999 / (10_000 * reserves[7].reserveIn); - if (amountIn > MIN_LIQUIDITY) { - amountsOutSingleEth[7] = reserves[7].reserveOut * 1 ether * 9999 / (10_000 * reserves[7].reserveIn); - } - } - // todo: partial withdraws based on balance } } @@ -539,30 +530,32 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { // calculate amount to sync prices cascading through each pool with best prices first, while cumulative amount < amountIn for (uint256 i = 7; _isNonZero(i); i = _dec(i)) { if (index2[i] == 7 || _isZero(amountsOutSingleEth[index2[_dec(i)]])) { + // meveth rate is fixed so no more iterations required + // other case is there are no more viable pools to swap amountsIn[index2[i]] = amountIn - cumulativeAmount; amountsOut[index2[i]] = 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 - - (amountsIn[index2[i]], amountsOut[index2[i]]) = amountToSync( - isDeposit, - amountIn, - cumulativeAmount, - index2[i], - amountsOutSingleEth[index2[_dec(i)]], - reserves[index2[i]].reserveIn, - reserves[index2[i]].reserveOut, - inv - ); + // return (amountsIn, amountsOut); + } else { + (amountsIn[index2[i]], amountsOut[index2[i]]) = amountToSync( + isDeposit, + amountIn, + cumulativeAmount, + index2[i], + amountsOutSingleEth[index2[_dec(i)]], + reserves[index2[i]].reserveIn, + reserves[index2[i]].reserveOut, + inv + ); + } // UniV3 adjustment // Expensive in gas, so only used for high values and preferably called off-chain - if (index2[i] < 5 && index2[i] > 1 && amountsIn[index2[i]] > uniV3Caps[index2[i]] / 2) { + if (index2[i] < 5 && index2[i] > 1 && amountsIn[index2[i]] > uniV3Caps[index2[i] - 2] / 2) { // address tokenIn = isDeposit ? address(WETH09) : address(MEVETH); // address tokenOut = isDeposit ? address(MEVETH) : address(WETH09); amountsOut[index2[i]] = _swapUniV3Call( - !isDeposit, + isDeposit, uint24(MevEthLibrary.getFee(index2[i])), isDeposit ? address(WETH09) : address(MEVETH), isDeposit ? address(MEVETH) : address(WETH09), @@ -575,6 +568,7 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { cumulativeAmount = cumulativeAmount + amountsIn[index2[i]]; if (cumulativeAmount == amountIn) break; + if (_isZero(amountsOutSingleEth[index2[_dec(i)]])) break; } } } @@ -613,10 +607,6 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { amount = amountIn - cumulativeAmount; endLoop = true; } - if (amount == 0) { - amountOut = 0; - return (amountInToSync, amountOut); - } { uint256 amountOutTmp = amountOutCall(isDeposit, index, amount, reserveIn, reserveOut, inv); @@ -750,6 +740,7 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { sqrtPriceLimitX96: isReverse ? MAX_SQRT_RATIO - 1 : MIN_SQRT_RATIO + 1 }); // bytes memory input = abi.encodeWithSelector(quoter.quoteExactInputSingle.selector, quoterParams); + // (bool success, bytes memory data) = address(quoter).staticcall(input); (bool success, bytes memory data) = address(quoter).staticcall(abi.encodeWithSelector(quoter.quoteExactInputSingle.selector, quoterParams)); if (!success) { amountOut = 0; @@ -785,7 +776,7 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { amountIn = swaps.pools[j].amountIn; if (_isNonZero(amountIn)) { (, uint256 amountOut) = - _swapUniV3(!swaps.isDeposit, uint24(MevEthLibrary.getFee(j)), to, swaps.tokenIn, swaps.tokenOut, swaps.pools[j].pair, amountIn); // single v3 + _swapUniV3(swaps.isDeposit, uint24(MevEthLibrary.getFee(j)), to, swaps.tokenIn, swaps.tokenOut, swaps.pools[j].pair, amountIn); // single v3 // swap amounts[1] = amounts[1] + amountOut; if (amountOut == 0) { @@ -841,8 +832,6 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { if (useQueue) { MEVETH.withdrawQueue(MEVETH.previewRedeem(amountIn) - 1, to, address(this)); amounts[1] = amounts[1] + MEVETH.previewRedeem(amountIn); - } else { - amounts[1] = amounts[1] + MEVETH.redeem(amountIn, to, address(this)); } } } diff --git a/src/interfaces/IRateProvider.sol b/src/interfaces/IRateProvider.sol index ba23266..b51dd7b 100644 --- a/src/interfaces/IRateProvider.sol +++ b/src/interfaces/IRateProvider.sol @@ -9,7 +9,6 @@ * @custom.org.encryption="manifoldfinance.com/.well-known/pgp-key.asc" * @custom:org.preferred-languages="en" */ - pragma solidity ^0.8.19; /// @title IRateProvider diff --git a/test/MevEthRouter.t.sol b/test/MevEthRouter.t.sol index 62b138b..fb07b77 100644 --- a/test/MevEthRouter.t.sol +++ b/test/MevEthRouter.t.sol @@ -27,7 +27,7 @@ contract MevEthRouterTest is DSTest { uint256 minLiquidity = uint256(1000); function setUp() public { - FORK_ID = vm.createSelectFork(RPC_ETH_MAINNET); + FORK_ID = vm.createSelectFork(RPC_ETH_MAINNET, 18_919_140); // fix block number for benchmarking tests router = new MevEthRouter(0x617c8dE5BdE54ffbb8d92716CC947858cA38f582); } @@ -58,7 +58,7 @@ contract MevEthRouterTest is DSTest { input = abi.encodeWithSelector(router.amountOutRedeem.selector, useQueue, shares); (, data) = address(router).staticcall(input); (amountOut, swaps) = abi.decode(data, (uint256, IMevEthRouter.Swap)); - assertGt(amountOut, MEVETH.previewRedeem(shares) * 95 /100); + assertGt(amountOut, MEVETH.previewRedeem(shares) * 95 / 100); // test redeem ERC20(address(MEVETH)).approve(address(router), shares); uint256 assets = router.redeemMevEthForEth(useQueue, address(this), shares, amountOut * 99 / 100, block.timestamp, swaps); @@ -66,37 +66,51 @@ contract MevEthRouterTest is DSTest { } function testStakePools() external { - uint256 amountIn = 10 ether; + uint256 amountIn = 240 ether; vm.deal(address(this), amountIn); // test getting swap route bytes memory input = abi.encodeWithSelector(router.amountOutStake.selector, amountIn); (, bytes memory data) = address(router).staticcall(input); (uint256 amountOut, IMevEthRouter.Swap memory swaps) = abi.decode(data, (uint256, IMevEthRouter.Swap)); - assertGt(amountOut, MEVETH.previewDeposit(amountIn) * 999 / 1000); - // test stake + uint256 numPools; + for (uint256 i; i < swaps.pools.length; ++i) { + if (swaps.pools[i].amountIn > 0) ++numPools; + } + assertGt(numPools, 2); + router.stakeEthForMevEth{ value: amountIn }(address(this), amountIn, amountOut * 98 / 100, block.timestamp, swaps); + } + + function testUniV3Swap() external { + uint256 amountIn = 4 ether; + vm.deal(address(this), amountIn); + // test getting swap route + bytes memory input = abi.encodeWithSelector(router.amountOutStake.selector, amountIn); + (, bytes memory data) = address(router).staticcall(input); + (uint256 amountOut, IMevEthRouter.Swap memory swaps) = abi.decode(data, (uint256, IMevEthRouter.Swap)); + assertGt(swaps.pools[4].amountIn, 0); uint256 shares = router.stakeEthForMevEth{ value: amountIn }(address(this), amountIn, amountOut * 99 / 100, block.timestamp, swaps); - assertGt(shares, 0); - // test redeem route + assertGt(shares, amountOut * 99 / 100); + bool useQueue; - if (amountIn > 15 ether) { - useQueue = true; - } + shares = 8 ether; + writeTokenBalance(address(this), address(MEVETH), shares); input = abi.encodeWithSelector(router.amountOutRedeem.selector, useQueue, shares); (, data) = address(router).staticcall(input); (amountOut, swaps) = abi.decode(data, (uint256, IMevEthRouter.Swap)); - assertGt(amountOut, MEVETH.previewRedeem(shares) * 95 /100); + assertGt(amountOut, MEVETH.previewRedeem(shares) * 95 / 100); + assertGt(swaps.pools[4].amountIn, 0); // test redeem ERC20(address(MEVETH)).approve(address(router), shares); uint256 assets = router.redeemMevEthForEth(useQueue, address(this), shares, amountOut * 99 / 100, block.timestamp, swaps); - assertGt(assets, 0); + assertGt(assets, amountOut * 99 / 100); } - function testEdgeAmount() external { + function testEdgeRedeemAmount() external { // test redeem route bool useQueue = false; bytes memory input = abi.encodeWithSelector(router.amountOutRedeem.selector, useQueue, 6 ether); - (,bytes memory data) = address(router).staticcall(input); - (uint256 amountOut, ) = abi.decode(data, (uint256, IMevEthRouter.Swap)); - assertGt(amountOut, 5.98 ether); + (, bytes memory data) = address(router).staticcall(input); + (uint256 amountOut,) = abi.decode(data, (uint256, IMevEthRouter.Swap)); + assertGt(amountOut, 5.89 ether); } } From 77b1f83a4f0f8472ff66963de470fbf54375fc10 Mon Sep 17 00:00:00 2001 From: sandybradley Date: Fri, 12 Jan 2024 00:57:56 +0200 Subject: [PATCH 6/6] chore: removed univ3 quoter as it is querky in tests and expensive in gas --- src/MevEthRouter.sol | 63 ------------------------------------- test/MevEthRouter2.t.sol | 67 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 63 deletions(-) create mode 100644 test/MevEthRouter2.t.sol diff --git a/src/MevEthRouter.sol b/src/MevEthRouter.sol index f15fb3f..bbc5b1a 100644 --- a/src/MevEthRouter.sol +++ b/src/MevEthRouter.sol @@ -8,7 +8,6 @@ import "./interfaces/IGyro.sol"; import "./interfaces/ICurveV2Pool.sol"; import "./interfaces/IMevEth.sol"; import "./interfaces/IRateProvider.sol"; -import "./interfaces/IQuoterV2.sol"; import "./interfaces/IGyroECLPMath.sol"; import "./interfaces/IMevEthRouter.sol"; import "./interfaces/IUniswapV3SwapCallback.sol"; @@ -43,8 +42,6 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { IVault internal constant BAL = IVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8); /// @dev Gyro ECLP Math lib IGyroECLPMath internal constant gyroMath = IGyroECLPMath(0xF89A1713998593A441cdA571780F0900Dbef20f9); - /// @dev IQuoterV2 Uniswap swap quoter - IQuoterV2 internal constant quoter = IQuoterV2(0x61fFE014bA17989E743c5F6cB21bF9697530B21e); /// @dev Sushiswap factory address address internal constant SUSHI_FACTORY = 0xC0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac; /// @dev UniswapV2 factory address @@ -445,20 +442,14 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { view returns (uint256 amountOut) { - // uint256 scalingFactorTokenIn = _scalingFactor(!isDeposit); - // uint256 scalingFactorTokenOut = _scalingFactor(isDeposit); uint256[] memory balances = new uint256[](2); balances[0] = isDeposit ? reserveOut : reserveIn; balances[1] = isDeposit ? reserveIn : reserveOut; - // uint256 feeAmount = amountIn * MevEthLibrary.getFee(5) / 1_000_000; - // amountIn = (amountIn - feeAmount) * _scalingFactor(!isDeposit) / 1 ether; amountIn = (amountIn - amountIn * MevEthLibrary.getFee(5) / 1_000_000) * _scalingFactor(!isDeposit) / 1 ether; // same selector workaround here - // bytes memory data = abi.encodeWithSelector(0x61ff4236, balances, amountIn, !isDeposit, params, derived, inv); (, bytes memory returnData) = address(gyroMath).staticcall(abi.encodeWithSelector(0x61ff4236, balances, amountIn, !isDeposit, params, derived, inv)); - // amountOut = gyroMath.calcOutGivenIn(balances, amountIn, !isDeposit, params, derived, inv) * 1 ether / scalingFactorTokenOut; amountOut = abi.decode(returnData, (uint256)) * 1 ether / _scalingFactor(isDeposit); } @@ -549,23 +540,6 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { ); } - // UniV3 adjustment - // Expensive in gas, so only used for high values and preferably called off-chain - if (index2[i] < 5 && index2[i] > 1 && amountsIn[index2[i]] > uniV3Caps[index2[i] - 2] / 2) { - // address tokenIn = isDeposit ? address(WETH09) : address(MEVETH); - // address tokenOut = isDeposit ? address(MEVETH) : address(WETH09); - amountsOut[index2[i]] = _swapUniV3Call( - isDeposit, - uint24(MevEthLibrary.getFee(index2[i])), - isDeposit ? address(WETH09) : address(MEVETH), - isDeposit ? address(MEVETH) : address(WETH09), - amountsIn[index2[i]] - ); - if (amountsOut[index2[i]] == 0) { - amountsIn[index2[i]] = 0; - } - } - cumulativeAmount = cumulativeAmount + amountsIn[index2[i]]; if (cumulativeAmount == amountIn) break; if (_isZero(amountsOutSingleEth[index2[_dec(i)]])) break; @@ -706,9 +680,6 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { virtual returns (uint256 amountInActual, uint256 amountOut) { - // bytes memory data = abi.encodePacked(tokenIn, tokenOut, fee); - // uint160 sqrtPriceLimitX96 = isReverse ? MAX_SQRT_RATIO - 1 : MIN_SQRT_RATIO + 1; - try IUniswapV3Pool(pair).swap( to, !isReverse, int256(amountIn), isReverse ? MAX_SQRT_RATIO - 1 : MIN_SQRT_RATIO + 1, abi.encodePacked(tokenIn, tokenOut, fee) ) returns (int256 amount0, int256 amount1) { @@ -719,36 +690,6 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { } } - 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: isReverse ? MAX_SQRT_RATIO - 1 : MIN_SQRT_RATIO + 1 - }); - // bytes memory input = abi.encodeWithSelector(quoter.quoteExactInputSingle.selector, quoterParams); - // (bool success, bytes memory data) = address(quoter).staticcall(input); - (bool success, bytes memory data) = address(quoter).staticcall(abi.encodeWithSelector(quoter.quoteExactInputSingle.selector, quoterParams)); - if (!success) { - amountOut = 0; - } else { - (amountOut) = abi.decode(data, (uint256)); - } - } - /// @dev Internal core swap. Requires the initial amount to have already been sent to the first pair (for v2 pairs). /// @param useQueue Use queue or not for withdrawals /// @param to Address of receiver @@ -765,10 +706,8 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { for (uint256 j; j < 2; j = _inc(j)) { amountIn = swaps.pools[j].amountIn; if (_isNonZero(amountIn)) { - // uint256 balBefore = ERC20(swaps.tokenOut).balanceOf(to); _swapSingle(swaps.isDeposit, to, swaps.pools[j].pair, swaps.pools[j].amountOut); // single v2 swap amounts[1] = amounts[1] + swaps.pools[j].amountOut; - // amounts[1] = amounts[1] + ERC20(swaps.tokenOut).balanceOf(to) - balBefore; } } // V3 swaps @@ -799,9 +738,7 @@ contract MevEthRouter is IUniswapV3SwapCallback, IMevEthRouter { amountIn = swaps.pools[5].amountIn; if (_isNonZero(amountIn)) { IVault.SingleSwap memory singleSwap = IVault.SingleSwap(poolId, IVault.SwapKind.GIVEN_IN, swaps.tokenIn, swaps.tokenOut, amountIn, new bytes(0)); - IVault.FundManagement memory fund = IVault.FundManagement(address(this), false, payable(to), false); - // todo: calc limit if (swaps.tokenIn == address(WETH09)) { WETH09.approve(address(BAL), amountIn); } else { diff --git a/test/MevEthRouter2.t.sol b/test/MevEthRouter2.t.sol new file mode 100644 index 0000000..c9d957d --- /dev/null +++ b/test/MevEthRouter2.t.sol @@ -0,0 +1,67 @@ +/// SPDX-License-Identifier: UNLICENSED + +pragma solidity >=0.8.13 <0.9.0; + +import "forge-std/Test.sol"; +import { Vm } from "forge-std/Vm.sol"; +import { MevEthRouter } from "../src/MevEthRouter.sol"; +import { IUniswapV2Pair } from "../src/interfaces/IUniswapV2Pair.sol"; +import { IWETH } from "../src/interfaces/IWETH.sol"; +import { IMevEth } from "../src/interfaces/IMevEth.sol"; +import { IMevEthRouter } from "../src/interfaces/IMevEthRouter.sol"; +import { ERC20 } from "solmate/tokens/ERC20.sol"; + +/// @title MevEthRouter2Test use different fork for cross check +contract MevEthRouter2Test is DSTest { + using stdStorage for StdStorage; + + string RPC_ETH_MAINNET = vm.envString("RPC_MAINNET"); + uint256 FORK_ID; + StdStorage stdstore; + Vm internal constant vm = Vm(HEVM_ADDRESS); + MevEthRouter router; + address WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + IMevEth internal constant MEVETH = IMevEth(0x24Ae2dA0f361AA4BE46b48EB19C91e02c5e4f27E); + + IWETH weth = IWETH(WETH); + uint256 minLiquidity = uint256(1000); + + function setUp() public { + FORK_ID = vm.createSelectFork(RPC_ETH_MAINNET); + router = new MevEthRouter(0x617c8dE5BdE54ffbb8d92716CC947858cA38f582); + } + + function writeTokenBalance(address who, address token, uint256 amt) internal { + stdstore.target(token).sig(ERC20(token).balanceOf.selector).with_key(who).checked_write(amt); + } + + receive() external payable { } + + /// @dev Fuzz test amountOut, Stake and redeem + function testStakeAndRedeem(uint80 amountIn) external { + vm.assume(amountIn > 0.1 ether); + vm.assume(amountIn < 100_000 ether); + vm.deal(address(this), amountIn); + // test getting swap route + bytes memory input = abi.encodeWithSelector(router.amountOutStake.selector, amountIn); + (, bytes memory data) = address(router).staticcall(input); + (uint256 amountOut, IMevEthRouter.Swap memory swaps) = abi.decode(data, (uint256, IMevEthRouter.Swap)); + assertGt(amountOut, MEVETH.previewDeposit(amountIn) * 999 / 1000); + // test stake + uint256 shares = router.stakeEthForMevEth{ value: amountIn }(address(this), amountIn, amountOut * 99 / 100, block.timestamp, swaps); + assertGt(shares, amountOut * 99 / 100); + // test redeem route + bool useQueue; + if (amountIn > 15 ether) { + useQueue = true; + } + input = abi.encodeWithSelector(router.amountOutRedeem.selector, useQueue, shares); + (, data) = address(router).staticcall(input); + (amountOut, swaps) = abi.decode(data, (uint256, IMevEthRouter.Swap)); + assertGt(amountOut, MEVETH.previewRedeem(shares) * 95 / 100); + // test redeem + ERC20(address(MEVETH)).approve(address(router), shares); + uint256 assets = router.redeemMevEthForEth(useQueue, address(this), shares, amountOut * 99 / 100, block.timestamp, swaps); + assertGt(assets, 0); + } +}