// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.13 <0.9.0;
/**
Optimal MEV router contract (IUniswapV2Router compatible)
For the User it aims to offer:
1. Better order routing for minimal slippage
2. At source MEV
3. Lower gas costs for swaps and liquidity changes
For the Liquidity providers:
1. Inclusive rewards
2. Reduced impermanent loss
For the Exchange providers:
1. Inclusive rewards
2. Increased adoption
For the Ethereum environment:
1. Reduced MEV attacks and fee spikes
2. Healthy growth in MEV space with inclusive incentives
Version 1 MEV Strategies
- cross-dex backruns for swaps
- reduced slippage fallback router
The contract leverages and depends on 3 external protocols:
1. Uniswap V2 (or equivalent on another network) for backrun completion and fallback swaps
2. BentoBox for flashloan backruns
3. Aave V2 for flashloan backruns that require more liquidity than BentoBox has
Business logic
- Profits from backruns are retained on the contract to improve efficiency (gas cost and profit) of future backruns
- Contract should therefore be deployed or ownership transfered to a multisig address
- Harvest function can be called from the multisig owners to distribute profits by consensus
Resources
- IUniswapV2Router: https://github.com/Uniswap/v2-periphery/blob/master/contracts/interfaces/IUniswapV2Router02.sol
- Contract size reduction techniques: https://ethereum.org/en/developers/tutorials/downsizing-contracts-to-fight-the-contract-size-limit/
- Contract gas reduction techniques: https://gist.github.com/hrkrshnn/ee8fabd532058307229d65dcd5836ddc
Dev Notes
- Normal sushiswap router functions. Swaps have 2 material changes:
1) slippage fallback router (uniswap v2)
2) backruns after user swap
- For gas and size efficiency, requires are modified to reverts with custom errors
- Factory hash is now passed to library functions because we are working with 2 factories
- Other changes are trade-offs for reducing contract size and / or gas usage and stack too deep errors
*/ |
|||||||||||||
============ Internal Imports ============ |
|||||||||||||
@title OpenMevRouter |
contract OpenMevRouter is IFlashBorrower, IOpenMevRouter, TwoStepOwnable {
using SafeTransferLib for ERC20;
// Custom errors save gas, encoding to 4 bytes
error Expired();
error NoTokens();
error NotPercent();
error NoReceivers();
error InvalidPath();
error TransferFailed();
error InsufficientBAmount();
error InsufficientAAmount();
error TokenIsFeeOnTransfer();
error ExcessiveInputAmount();
error ExecuteNotAuthorized();
error InsufficientAllowance();
error InsufficientLiquidity();
error InsufficientOutputAmount();
event MEV(address indexed user, address indexed token, uint256 value);
event LoanError(address indexed token, uint256 amountIn);
bytes4 internal constant SWAP_SELECTOR = bytes4(keccak256("swap(uint256,uint256,address,bytes)"));
address internal constant WETH09 = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address internal constant BACKUP_FACTORY = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; // uniswap v2 factory
address internal constant LENDING_POOL_ADDRESS = 0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9; // aave lending pool address
address internal constant BENTO = 0xF5BCE5077908a1b7370B9ae04AdC565EBd643966;
address internal constant AAVE_DATA_PROVIDER = 0x057835Ad21a177dbdd3090bB1CAE03EaCF78Fc6d;
IBentoBoxV1 internal constant bento = IBentoBoxV1(0xF5BCE5077908a1b7370B9ae04AdC565EBd643966); // BENTO vault contract
mapping(address => bool) internal IS_AAVE_ASSET; // boolean address mapping for flagging aave assets
/**
* |
||||||||||||
@notice Ensures deadline is not passed, otherwise revert. (0 = no deadline) |
function ensure(uint256 deadline) internal view {
if (deadline < block.timestamp) revert Expired();
} |
||||||||||||
@notice Checks amounts for token A and token B are balanced for pool. Creates a pair if none exists |
function _addLiquidity(
address tokenA,
address tokenB,
uint256 amountADesired,
uint256 amountBDesired,
uint256 amountAMin,
uint256 amountBMin
) internal virtual returns (uint256 amountA, uint256 amountB) {
// create the pair if it doesn't exist yet
address factory = OpenMevLibrary.SUSHI_FACTORY;
if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
IUniswapV2Factory(factory).createPair(tokenA, tokenB);
}
(uint256 reserveA, uint256 reserveB) = OpenMevLibrary.getReserves(factory, tokenA, tokenB);
if (_isZero(reserveA) && _isZero(reserveB)) {
(amountA, amountB) = (amountADesired, amountBDesired);
} else {
uint256 amountBOptimal = OpenMevLibrary.quote(amountADesired, reserveA, reserveB);
if (amountBOptimal > amountBDesired) {
uint256 amountAOptimal = OpenMevLibrary.quote(amountBDesired, reserveB, reserveA);
if (amountAOptimal > amountADesired) revert InsufficientAAmount();
if (amountAOptimal < amountAMin) revert InsufficientAAmount();
(amountA, amountB) = (amountAOptimal, amountBDesired);
} else {
if (amountBOptimal < amountBMin) revert InsufficientBAmount();
(amountA, amountB) = (amountADesired, amountBOptimal);
}
}
} |
||||||||||||
@notice Adds liquidity to an ERC-20⇄ERC-20 pool. msg.sender should have already given the router an allowance of at least amountADesired/amountBDesired on tokenA/tokenB |
function addLiquidity(
address tokenA,
address tokenB,
uint256 amountADesired,
uint256 amountBDesired,
uint256 amountAMin,
uint256 amountBMin,
address to,
uint256 deadline
)
external
virtual
returns (
uint256 amountA,
uint256 amountB,
uint256 liquidity
)
{
ensure(deadline);
(amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
address pair = OpenMevLibrary.pairFor(OpenMevLibrary.SUSHI_FACTORY, tokenA, tokenB);
ERC20(tokenA).safeTransferFrom(msg.sender, pair, amountA);
ERC20(tokenB).safeTransferFrom(msg.sender, pair, amountB);
liquidity = IUniswapV2Pair(pair).mint(to);
} |
||||||||||||
@notice Adds liquidity to an ERC-20⇄WETH pool with ETH. msg.sender should have already given the router an allowance of at least amountTokenDesired on token. msg.value is treated as a amountETHDesired. Leftover ETH, if any, is returned to msg.sender |
function addLiquidityETH(
address token,
uint256 amountTokenDesired,
uint256 amountTokenMin,
uint256 amountETHMin,
address to,
uint256 deadline
)
external
payable
virtual
returns (
uint256 amountToken,
uint256 amountETH,
uint256 liquidity
)
{
ensure(deadline);
address weth = WETH09;
(amountToken, amountETH) = _addLiquidity(
token,
weth,
amountTokenDesired,
msg.value,
amountTokenMin,
amountETHMin
);
address pair = OpenMevLibrary.pairFor(OpenMevLibrary.SUSHI_FACTORY, token, weth);
ERC20(token).safeTransferFrom(msg.sender, pair, amountToken);
IWETH(weth).deposit{ value: amountETH }();
ERC20(weth).safeTransfer(pair, amountETH);
liquidity = IUniswapV2Pair(pair).mint(to);
// refund dust eth, if any
if (msg.value > amountETH) SafeTransferLib.safeTransferETH(msg.sender, msg.value - amountETH);
} |
||||||||||||
**** REMOVE LIQUIDITY ****
@notice Removes liquidity from an ERC-20⇄ERC-20 pool. msg.sender should have already given the router an allowance of at least liquidity on the pool. |
function removeLiquidity(
address tokenA,
address tokenB,
uint256 liquidity,
uint256 amountAMin,
uint256 amountBMin,
address to,
uint256 deadline
) public virtual returns (uint256 amountA, uint256 amountB) {
ensure(deadline);
address pair = OpenMevLibrary.pairFor(OpenMevLibrary.SUSHI_FACTORY, tokenA, tokenB);
ERC20(pair).safeTransferFrom(msg.sender, pair, liquidity); // send liquidity to pair
(uint256 amount0, uint256 amount1) = IUniswapV2Pair(pair).burn(to);
(address token0, ) = OpenMevLibrary.sortTokens(tokenA, tokenB);
(amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);
if (amountA < amountAMin) revert InsufficientAAmount();
if (amountB < amountBMin) revert InsufficientBAmount();
} |
||||||||||||
@notice Removes liquidity from an ERC-20⇄WETH pool and receive ETH. msg.sender should have already given the router an allowance of at least liquidity on the pool. |
function removeLiquidityETH(
address token,
uint256 liquidity,
uint256 amountTokenMin,
uint256 amountETHMin,
address to,
uint256 deadline
) public virtual returns (uint256 amountToken, uint256 amountETH) {
address weth = WETH09;
uint256 balanceBefore = ERC20(token).balanceOf(address(this));
(amountToken, amountETH) = removeLiquidity(
token,
weth,
liquidity,
amountTokenMin,
amountETHMin,
address(this),
deadline
);
// exploit check from fee-on-transfer tokens
if (amountToken != ERC20(token).balanceOf(address(this)) - balanceBefore) revert TokenIsFeeOnTransfer();
ERC20(token).safeTransfer(to, amountToken);
IWETH(weth).withdraw(amountETH);
SafeTransferLib.safeTransferETH(to, amountETH);
} |
||||||||||||
@notice Removes liquidity from an ERC-20⇄ERC-20 pool without pre-approval, thanks to permit. |
function removeLiquidityWithPermit(
address tokenA,
address tokenB,
uint256 liquidity,
uint256 amountAMin,
uint256 amountBMin,
address to,
uint256 deadline,
bool approveMax,
uint8 v,
bytes32 r,
bytes32 s
) external virtual returns (uint256 amountA, uint256 amountB) {
IUniswapV2Pair(OpenMevLibrary.pairFor(OpenMevLibrary.SUSHI_FACTORY, tokenA, tokenB)).permit(
msg.sender,
address(this),
approveMax ? type(uint256).max : liquidity,
deadline,
v,
r,
s
);
(amountA, amountB) = removeLiquidity(tokenA, tokenB, liquidity, amountAMin, amountBMin, to, deadline);
} |
||||||||||||
@notice Removes liquidity from an ERC-20⇄WETTH pool and receive ETH without pre-approval, thanks to permit |
function removeLiquidityETHWithPermit(
address token,
uint256 liquidity,
uint256 amountTokenMin,
uint256 amountETHMin,
address to,
uint256 deadline,
bool approveMax,
uint8 v,
bytes32 r,
bytes32 s
) external virtual returns (uint256 amountToken, uint256 amountETH) {
IUniswapV2Pair(OpenMevLibrary.pairFor(OpenMevLibrary.SUSHI_FACTORY, token, WETH09)).permit(
msg.sender,
address(this),
approveMax ? type(uint256).max : liquidity,
deadline,
v,
r,
s
);
(amountToken, amountETH) = removeLiquidityETH(token, liquidity, amountTokenMin, amountETHMin, to, deadline);
} |
||||||||||||
@notice Identical to removeLiquidityETH, but succeeds for tokens that take a fee on transfer. msg.sender should have already given the router an allowance of at least liquidity on the pool. |
function removeLiquidityETHSupportingFeeOnTransferTokens(
address token,
uint256 liquidity,
uint256 amountTokenMin,
uint256 amountETHMin,
address to,
uint256 deadline
) public virtual returns (uint256 amountETH) {
address weth = WETH09;
uint256 balanceBefore = ERC20(token).balanceOf(address(this));
(, amountETH) = removeLiquidity(token, weth, liquidity, amountTokenMin, amountETHMin, address(this), deadline);
ERC20(token).safeTransfer(to, ERC20(token).balanceOf(address(this)) - balanceBefore);
IWETH(weth).withdraw(amountETH);
SafeTransferLib.safeTransferETH(to, amountETH);
} |
||||||||||||
@notice Identical to removeLiquidityETHWithPermit, but succeeds for tokens that take a fee on transfer. |
function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens(
address token,
uint256 liquidity,
uint256 amountTokenMin,
uint256 amountETHMin,
address to,
uint256 deadline,
bool approveMax,
uint8 v,
bytes32 r,
bytes32 s
) external virtual returns (uint256 amountETH) {
IUniswapV2Pair(OpenMevLibrary.pairFor(OpenMevLibrary.SUSHI_FACTORY, token, WETH09)).permit(
msg.sender,
address(this),
approveMax ? type(uint256).max : liquidity,
deadline,
v,
r,
s
);
amountETH = removeLiquidityETHSupportingFeeOnTransferTokens(
token,
liquidity,
amountTokenMin,
amountETHMin,
to,
deadline
);
} |
||||||||||||
@notice Internal core swap. Requires the initial amount to have already been sent to the first pair. |
function _swap(address _to, OpenMevLibrary.Swap[] memory swaps)
internal
virtual
returns (uint256[] memory amounts)
{
uint256 length = swaps.length;
amounts = new uint256[](_inc(length));
amounts[0] = swaps[0].amountIn;
for (uint256 i; i < length; i = _inc(i)) {
uint256 amountOut = swaps[i].amountOut;
{
(uint256 amount0Out, uint256 amount1Out) = swaps[i].isReverse
? (amountOut, uint256(0))
: (uint256(0), amountOut);
address to = i < _dec(length) ? swaps[_inc(i)].pair : _to;
address pair = swaps[i].pair;
_asmSwap(pair, amount0Out, amount1Out, to);
}
amounts[_inc(i)] = amountOut;
}
} |
||||||||||||
@notice Swaps an exact amount of input tokens for as many output tokens as possible, along the route determined by the path. The first element of path is the input token, the last is the output token, and any intermediate elements represent intermediate pairs to trade through. msg.sender should have already given the router an allowance of at least amountIn on the input token. |
function swapExactTokensForTokens(
uint256 amountIn,
uint256 amountOutMin,
address[] calldata path,
address to,
uint256 deadline
) external virtual returns (uint256[] memory amounts) {
ensure(deadline);
address factory = OpenMevLibrary.SUSHI_FACTORY;
OpenMevLibrary.Swap[] memory swaps = OpenMevLibrary.getSwapsOut(factory, amountIn, path);
uint256 length = swaps.length;
if (swaps[_dec(length)].amountOut < amountOutMin) {
// Change 1 -> fallback for insufficient output amount, check backup router
factory = BACKUP_FACTORY;
swaps = OpenMevLibrary.getSwapsOut(factory, amountIn, path);
if (swaps[_dec(length)].amountOut < amountOutMin) revert InsufficientOutputAmount();
}
ERC20(path[0]).safeTransferFrom(msg.sender, swaps[0].pair, amountIn);
amounts = _swap(to, swaps);
// Change 2 -> back-run swaps
_backrunSwaps(factory, swaps);
} |
||||||||||||
@notice Receive an exact amount of output tokens for as few input tokens as possible, along the route determined by the path. msg.sender should have already given the router an allowance of at least amountInMax on the input token. |
function swapTokensForExactTokens(
uint256 amountOut,
uint256 amountInMax,
address[] calldata path,
address to,
uint256 deadline
) external virtual returns (uint256[] memory amounts) {
ensure(deadline);
address factory = OpenMevLibrary.SUSHI_FACTORY;
// amounts = OpenMevLibrary.getAmountsIn(factory, amountOut, path);
OpenMevLibrary.Swap[] memory swaps = OpenMevLibrary.getSwapsIn(factory, amountOut, path);
if (swaps[0].amountIn > amountInMax) {
factory = BACKUP_FACTORY;
// Change 1 -> fallback for insufficient output amount, check backup router
swaps = OpenMevLibrary.getSwapsIn(factory, amountOut, path);
if (swaps[0].amountIn > amountInMax) revert ExcessiveInputAmount();
}
|