// SPDX-License-Identifier: MIT pragma solidity =0.8.18; import "@openzeppelin/contracts/utils/math/SafeMath.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "../../utils/metatx/Forwarder.sol"; import "../core/interfaces/ISomaSwapFactory.sol"; import "../libraries/TransferHelper.sol"; import "./ISomaSwapRouter.sol"; import "./libraries/SomaSwapLibrary.sol"; import "./interfaces/IWETH.sol"; import "hardhat/console.sol"; /** * @notice Implementation of the {ISomaSwapRouter} interface. */ contract SomaSwapRouter is ISomaSwapRouter { using Forwarder for address; using SafeMath for uint256; /** * @notice The name of the contract. */ string public constant name = "SomaSwapRouter"; /** * @inheritdoc ISomaSwapRouter */ address public immutable override factory; /** * @inheritdoc ISomaSwapRouter */ address public immutable override WETH; /** * @notice Modifier to ensure function calls occur before `deadline`. */ modifier ensure(uint256 deadline) { require(deadline >= block.timestamp, "SomaSwapRouter: EXPIRED"); _; } /** * @notice Constructor of the contract. * @param _factory The address of the SomaSwapFactory. * @param _WETH The address of WETH. */ constructor(address _factory, address _WETH) { factory = _factory; WETH = _WETH; } receive() external payable { assert(msg.sender == WETH); // only accept ETH via fallback from the WETH contract } // **** ADD LIQUIDITY **** 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 if (ISomaSwapFactory(factory).getPair(tokenA, tokenB) == address(0)) { // Here we revert. Only admins can create pairs revert("SomaSwapPair: INVALID_PAIR"); } (uint256 reserveA, uint256 reserveB) = SomaSwapLibrary.getReserves(factory, tokenA, tokenB); if (reserveA == 0 && reserveB == 0) { (amountA, amountB) = (amountADesired, amountBDesired); } else { uint256 amountBOptimal = SomaSwapLibrary.quote(amountADesired, reserveA, reserveB); if (amountBOptimal <= amountBDesired) { require(amountBOptimal >= amountBMin, "SomaSwapRouter: INSUFFICIENT_B_AMOUNT"); (amountA, amountB) = (amountADesired, amountBOptimal); } else { uint256 amountAOptimal = SomaSwapLibrary.quote(amountBDesired, reserveB, reserveA); assert(amountAOptimal <= amountADesired); require(amountAOptimal >= amountAMin, "SomaSwapRouter: INSUFFICIENT_A_AMOUNT"); (amountA, amountB) = (amountAOptimal, amountBDesired); } } } /** * @inheritdoc ISomaSwapRouter */ function addLiquidity( address tokenA, address tokenB, uint256 amountADesired, uint256 amountBDesired, uint256 amountAMin, uint256 amountBMin, address to, uint256 deadline ) external virtual override ensure(deadline) returns (uint256 amountA, uint256 amountB, uint256 liquidity) { (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin); address pair = SomaSwapLibrary.pairFor(factory, tokenA, tokenB); TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA); TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB); liquidity = ISomaSwapPair(pair).mint(to); } /** * @inheritdoc ISomaSwapRouter */ function addLiquidityETH( address token, uint256 amountTokenDesired, uint256 amountTokenMin, uint256 amountETHMin, address to, uint256 deadline ) external payable virtual override ensure(deadline) returns (uint256 amountToken, uint256 amountETH, uint256 liquidity) { (amountToken, amountETH) = _addLiquidity(token, WETH, amountTokenDesired, msg.value, amountTokenMin, amountETHMin); address pair = SomaSwapLibrary.pairFor(factory, token, WETH); TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken); IWETH(WETH).deposit{value: amountETH}(); assert(IWETH(WETH).transfer(pair, amountETH)); liquidity = ISomaSwapPair(pair).mint(to); // refund dust eth, if any if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH); } /** * @inheritdoc ISomaSwapRouter */ function removeLiquidity( address tokenA, address tokenB, uint256 liquidity, uint256 amountAMin, uint256 amountBMin, address to, uint256 deadline ) public virtual override ensure(deadline) returns (uint256 amountA, uint256 amountB) { address pair = SomaSwapLibrary.pairFor(factory, tokenA, tokenB); _transferToPair(pair, liquidity); // send liquidity to pair (uint256 amount0, uint256 amount1) = ISomaSwapPair(pair).burn(to); (address token0,) = SomaSwapLibrary.sortTokens(tokenA, tokenB); (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0); require(amountA >= amountAMin, "SomaSwapRouter: INSUFFICIENT_A_AMOUNT"); require(amountB >= amountBMin, "SomaSwapRouter: INSUFFICIENT_B_AMOUNT"); } /** * @inheritdoc ISomaSwapRouter */ function removeLiquidityETH( address token, uint256 liquidity, uint256 amountTokenMin, uint256 amountETHMin, address to, uint256 deadline ) public virtual override ensure(deadline) returns (uint256 amountToken, uint256 amountETH) { (amountToken, amountETH) = removeLiquidity(token, WETH, liquidity, amountTokenMin, amountETHMin, address(this), deadline); TransferHelper.safeTransfer(token, to, amountToken); IWETH(WETH).withdraw(amountETH); TransferHelper.safeTransferETH(to, amountETH); } /** * @inheritdoc ISomaSwapRouter */ 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 override returns (uint256 amountA, uint256 amountB) { address pair = SomaSwapLibrary.pairFor(factory, tokenA, tokenB); uint256 value = approveMax ? type(uint256).max : liquidity; ISomaSwapPair(pair).permit(msg.sender, address(this), value, deadline, v, r, s); (amountA, amountB) = removeLiquidity(tokenA, tokenB, liquidity, amountAMin, amountBMin, to, deadline); } /** * @inheritdoc ISomaSwapRouter */ function removeLiquidityETHWithPermit( address token, uint256 liquidity, uint256 amountTokenMin, uint256 amountETHMin, address to, uint256 deadline, bool approveMax, uint8 v, bytes32 r, bytes32 s ) external virtual override returns (uint256 amountToken, uint256 amountETH) { address pair = SomaSwapLibrary.pairFor(factory, token, WETH); uint256 value = approveMax ? type(uint256).max : liquidity; ISomaSwapPair(pair).permit(msg.sender, address(this), value, deadline, v, r, s); (amountToken, amountETH) = removeLiquidityETH(token, liquidity, amountTokenMin, amountETHMin, to, deadline); } /** * @inheritdoc ISomaSwapRouter */ // **** REMOVE LIQUIDITY (supporting fee-on-transfer tokens) **** function removeLiquidityETHSupportingFeeOnTransferTokens( address token, uint256 liquidity, uint256 amountTokenMin, uint256 amountETHMin, address to, uint256 deadline ) public virtual override ensure(deadline) returns (uint256 amountETH) { (, amountETH) = removeLiquidity(token, WETH, liquidity, amountTokenMin, amountETHMin, address(this), deadline); TransferHelper.safeTransfer(token, to, IERC20(token).balanceOf(address(this))); IWETH(WETH).withdraw(amountETH); TransferHelper.safeTransferETH(to, amountETH); } /** * @inheritdoc ISomaSwapRouter */ function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens( address token, uint256 liquidity, uint256 amountTokenMin, uint256 amountETHMin, address to, uint256 deadline, bool approveMax, uint8 v, bytes32 r, bytes32 s ) external virtual override returns (uint256 amountETH) { address pair = SomaSwapLibrary.pairFor(factory, token, WETH); uint256 value = approveMax ? type(uint256).max : liquidity; ISomaSwapPair(pair).permit(msg.sender, address(this), value, deadline, v, r, s); amountETH = removeLiquidityETHSupportingFeeOnTransferTokens( token, liquidity, amountTokenMin, amountETHMin, to, deadline ); } // **** SWAP **** // requires the initial amount to have already been sent to the first pair function _swap(uint256[] memory amounts, address[] memory path, address _to) internal virtual { for (uint256 i; i < path.length - 1; ++i) { (address input, address output) = (path[i], path[i + 1]); (address token0,) = SomaSwapLibrary.sortTokens(input, output); uint256 amountOut = amounts[i + 1]; (uint256 amount0Out, uint256 amount1Out) = input == token0 ? (uint256(0), amountOut) : (amountOut, uint256(0)); address to = i < path.length - 2 ? SomaSwapLibrary.pairFor(factory, output, path[i + 2]) : _to; _pairSwap(SomaSwapLibrary.pairFor(factory, input, output), amount0Out, amount1Out, to, new bytes(0)); } } /** * @inheritdoc ISomaSwapRouter */ function swapExactTokensForTokens( uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline ) external virtual override ensure(deadline) returns (uint256[] memory amounts) { amounts = SomaSwapLibrary.getAmountsOut(factory, amountIn, path); require(amounts[amounts.length - 1] >= amountOutMin, "SomaSwapRouter: INSUFFICIENT_OUTPUT_AMOUNT"); TransferHelper.safeTransferFrom( path[0], msg.sender, SomaSwapLibrary.pairFor(factory, path[0], path[1]), amounts[0] ); _swap(amounts, path, to); } /** * @inheritdoc ISomaSwapRouter */ function swapTokensForExactTokens( uint256 amountOut, uint256 amountInMax, address[] calldata path, address to, uint256 deadline ) external virtual override ensure(deadline) returns (uint256[] memory amounts) { amounts = SomaSwapLibrary.getAmountsIn(factory, amountOut, path); require(amounts[0] <= amountInMax, "SomaSwapRouter: EXCESSIVE_INPUT_AMOUNT"); TransferHelper.safeTransferFrom( path[0], msg.sender, SomaSwapLibrary.pairFor(factory, path[0], path[1]), amounts[0] ); _swap(amounts, path, to); } /** * @inheritdoc ISomaSwapRouter */ function swapExactETHForTokens(uint256 amountOutMin, address[] calldata path, address to, uint256 deadline) external payable virtual override ensure(deadline) returns (uint256[] memory amounts) { require(path[0] == WETH, "SomaSwapRouter: INVALID_PATH"); amounts = SomaSwapLibrary.getAmountsOut(factory, msg.value, path); require(amounts[amounts.length - 1] >= amountOutMin, "SomaSwapRouter: INSUFFICIENT_OUTPUT_AMOUNT"); IWETH(WETH).deposit{value: amounts[0]}(); assert(IWETH(WETH).transfer(SomaSwapLibrary.pairFor(factory, path[0], path[1]), amounts[0])); _swap(amounts, path, to); } /** * @inheritdoc ISomaSwapRouter */ function swapTokensForExactETH( uint256 amountOut, uint256 amountInMax, address[] calldata path, address to, uint256 deadline ) external virtual override ensure(deadline) returns (uint256[] memory amounts) { require(path[path.length - 1] == WETH, "SomaSwapRouter: INVALID_PATH"); amounts = SomaSwapLibrary.getAmountsIn(factory, amountOut, path); require(amounts[0] <= amountInMax, "SomaSwapRouter: EXCESSIVE_INPUT_AMOUNT"); TransferHelper.safeTransferFrom( path[0], msg.sender, SomaSwapLibrary.pairFor(factory, path[0], path[1]), amounts[0] ); _swap(amounts, path, address(this)); IWETH(WETH).withdraw(amounts[amounts.length - 1]); TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]); } /** * @inheritdoc ISomaSwapRouter */ function swapExactTokensForETH( uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline ) external virtual override ensure(deadline) returns (uint256[] memory amounts) { require(path[path.length - 1] == WETH, "SomaSwapRouter: INVALID_PATH"); amounts = SomaSwapLibrary.getAmountsOut(factory, amountIn, path); require(amounts[amounts.length - 1] >= amountOutMin, "SomaSwapRouter: INSUFFICIENT_OUTPUT_AMOUNT"); TransferHelper.safeTransferFrom( path[0], msg.sender, SomaSwapLibrary.pairFor(factory, path[0], path[1]), amounts[0] ); _swap(amounts, path, address(this)); IWETH(WETH).withdraw(amounts[amounts.length - 1]); TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]); } /** * @inheritdoc ISomaSwapRouter */ function swapETHForExactTokens(uint256 amountOut, address[] calldata path, address to, uint256 deadline) external payable virtual override ensure(deadline) returns (uint256[] memory amounts) { require(path[0] == WETH, "SomaSwapRouter: INVALID_PATH"); amounts = SomaSwapLibrary.getAmountsIn(factory, amountOut, path); require(amounts[0] <= msg.value, "SomaSwapRouter: EXCESSIVE_INPUT_AMOUNT"); IWETH(WETH).deposit{value: amounts[0]}(); assert(IWETH(WETH).transfer(SomaSwapLibrary.pairFor(factory, path[0], path[1]), amounts[0])); _swap(amounts, path, to); // refund dust eth, if any if (msg.value > amounts[0]) TransferHelper.safeTransferETH(msg.sender, msg.value - amounts[0]); } // **** SWAP (supporting fee-on-transfer tokens) **** // requires the initial amount to have already been sent to the first pair function _swapSupportingFeeOnTransferTokens(address[] memory path, address _to) internal virtual { for (uint256 i; i < path.length - 1; ++i) { (address input, address output) = (path[i], path[i + 1]); (address token0,) = SomaSwapLibrary.sortTokens(input, output); ISomaSwapPair pair = ISomaSwapPair(SomaSwapLibrary.pairFor(factory, input, output)); uint256 amountInput; uint256 amountOutput; { // scope to avoid stack too deep errors (uint256 reserve0, uint256 reserve1,) = pair.getReserves(); (uint256 reserveInput, uint256 reserveOutput) = input == token0 ? (reserve0, reserve1) : (reserve1, reserve0); amountInput = IERC20(input).balanceOf(address(pair)).sub(reserveInput); amountOutput = SomaSwapLibrary.getAmountOut(amountInput, reserveInput, reserveOutput); } (uint256 amount0Out, uint256 amount1Out) = input == token0 ? (uint256(0), amountOutput) : (amountOutput, uint256(0)); address to = i < path.length - 2 ? SomaSwapLibrary.pairFor(factory, output, path[i + 2]) : _to; _pairSwap(address(pair), amount0Out, amount1Out, to, new bytes(0)); } } /** * @inheritdoc ISomaSwapRouter */ function swapExactTokensForTokensSupportingFeeOnTransferTokens( uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline ) external virtual override ensure(deadline) { TransferHelper.safeTransferFrom( path[0], msg.sender, SomaSwapLibrary.pairFor(factory, path[0], path[1]), amountIn ); uint256 balanceBefore = IERC20(path[path.length - 1]).balanceOf(to); _swapSupportingFeeOnTransferTokens(path, to); require( IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin, "SomaSwapRouter: INSUFFICIENT_OUTPUT_AMOUNT" ); } /** * @inheritdoc ISomaSwapRouter */ function swapExactETHForTokensSupportingFeeOnTransferTokens( uint256 amountOutMin, address[] calldata path, address to, uint256 deadline ) external payable virtual override ensure(deadline) { require(path[0] == WETH, "SomaSwapRouter: INVALID_PATH"); uint256 amountIn = msg.value; IWETH(WETH).deposit{value: amountIn}(); assert(IWETH(WETH).transfer(SomaSwapLibrary.pairFor(factory, path[0], path[1]), amountIn)); uint256 balanceBefore = IERC20(path[path.length - 1]).balanceOf(to); _swapSupportingFeeOnTransferTokens(path, to); require( IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin, "SomaSwapRouter: INSUFFICIENT_OUTPUT_AMOUNT" ); } /** * @inheritdoc ISomaSwapRouter */ function swapExactTokensForETHSupportingFeeOnTransferTokens( uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline ) external virtual override ensure(deadline) { require(path[path.length - 1] == WETH, "SomaSwapRouter: INVALID_PATH"); TransferHelper.safeTransferFrom( path[0], msg.sender, SomaSwapLibrary.pairFor(factory, path[0], path[1]), amountIn ); _swapSupportingFeeOnTransferTokens(path, address(this)); uint256 amountOut = IERC20(WETH).balanceOf(address(this)); require(amountOut >= amountOutMin, "SomaSwapRouter: INSUFFICIENT_OUTPUT_AMOUNT"); IWETH(WETH).withdraw(amountOut); TransferHelper.safeTransferETH(to, amountOut); } /** * @inheritdoc ISomaSwapRouter */ // **** LIBRARY FUNCTIONS **** function quote(uint256 amountA, uint256 reserveA, uint256 reserveB) external pure virtual override returns (uint256 amountB) { return SomaSwapLibrary.quote(amountA, reserveA, reserveB); } /** * @inheritdoc ISomaSwapRouter */ function getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut) external pure virtual override returns (uint256 amountOut) { return SomaSwapLibrary.getAmountOut(amountIn, reserveIn, reserveOut); } /** * @inheritdoc ISomaSwapRouter */ function getAmountIn(uint256 amountOut, uint256 reserveIn, uint256 reserveOut) external pure virtual override returns (uint256 amountIn) { return SomaSwapLibrary.getAmountIn(amountOut, reserveIn, reserveOut); } /** * @inheritdoc ISomaSwapRouter */ function getAmountsOut(uint256 amountIn, address[] memory path) external view virtual override returns (uint256[] memory amounts) { return SomaSwapLibrary.getAmountsOut(factory, amountIn, path); } /** * @inheritdoc ISomaSwapRouter */ function getAmountsIn(uint256 amountOut, address[] memory path) external view virtual override returns (uint256[] memory amounts) { return SomaSwapLibrary.getAmountsIn(factory, amountOut, path); } function _pairSwap(address pair, uint256 amount0Out, uint256 amount1Out, address to, bytes memory data) private { pair.forwardCall( "swap(uint256,uint256,address,bytes)", abi.encode(amount0Out, amount1Out, to, data), msg.sender ); } function _transferToPair(address pair, uint256 amount) private { pair.forwardCall("transferFrom(address,address,uint256)", abi.encode(msg.sender, pair, amount), address(this)); } }