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 343f193..2baf482 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 @@ -221,6 +218,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); @@ -395,15 +393,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 } } @@ -453,20 +442,16 @@ 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); } @@ -538,42 +523,29 @@ 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 - ); - - // 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) { - // 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]] + // 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 ); - 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; } } } @@ -612,10 +584,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); @@ -642,7 +610,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; } @@ -715,8 +683,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) @@ -728,34 +694,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(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 @@ -773,10 +711,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 @@ -784,7 +720,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) { @@ -807,9 +743,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 { @@ -840,8 +774,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 d8992c2..f0cba6a 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); } @@ -46,10 +46,11 @@ 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, 0); + assertGt(shares, amountOut * 99 / 100); + // test redeem route bool useQueue; if (amountIn > 15 ether) { @@ -58,10 +59,60 @@ 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); assertGt(assets, 0); } + + function testStakePools() external { + 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)); + 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, amountOut * 99 / 100); + + bool useQueue; + 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(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, amountOut * 99 / 100); + } + + 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.89 ether); + } } 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); + } +}