Skip to content

Commit

Permalink
Merge pull request #7 from manifoldfinance/fix/amountOut-edge
Browse files Browse the repository at this point in the history
Fix/amount out edge
  • Loading branch information
jeremyHD committed Jan 12, 2024
2 parents 3acab58 + f7b4f89 commit 7ae7f0b
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 94 deletions.
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ stackAllocation = true
mainnet = { key = "${ETHERSCAN_API}" }

[rpc_endpoints]
mainnet = ""
mainnet = "${RPC_MAINNET}"
optimism = ""
arbitrum = ""
polygon = ""
Expand Down
108 changes: 20 additions & 88 deletions src/MevEthRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
}
}

Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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;
}
}
}
Expand Down Expand Up @@ -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);
Expand All @@ -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;
}

Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -773,18 +711,16 @@ 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
for (uint256 j = 2; j < 5; j = _inc(j)) {
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) {
Expand All @@ -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 {
Expand Down Expand Up @@ -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));
}
}
}
Expand Down
1 change: 0 additions & 1 deletion src/interfaces/IRateProvider.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
59 changes: 55 additions & 4 deletions test/MevEthRouter.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand All @@ -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) {
Expand All @@ -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);
}
}
67 changes: 67 additions & 0 deletions test/MevEthRouter2.t.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}

0 comments on commit 7ae7f0b

Please sign in to comment.