Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
sobolev-igor committed Mar 27, 2024
1 parent 54ebef6 commit 881a91e
Show file tree
Hide file tree
Showing 6 changed files with 755 additions and 68 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,14 @@ import { IUniswapV2Pair } from "../interfaces/IUniswapV2Pair.sol";
import { IWETH9 } from "../interfaces/IWETH9.sol";
import { Base } from "../shared/Base.sol";
import { SwapType } from "../shared/Enums.sol";
import {
BadToken,
InconsistentPairsAndDirectionsLengths,
InputSlippage,
LowReserve,
ZeroAmountIn,
ZeroAmountOut,
ZeroLength
} from "../shared/Errors.sol";
import { BadToken, InconsistentPairsAndDirectionsLengths, InsufficientBalance, LowReserve, ZeroAmountIn, ZeroAmountOut, ZeroLength } from "../shared/Errors.sol";
import { TokensHandler } from "../shared/TokensHandler.sol";
import { Weth } from "../shared/Weth.sol";

/**
* @title Uniswap caller that executes swaps on UniswapV2-like pools
*/
contract UniswapCaller is ICaller, TokensHandler, Weth {
contract UniswapV2Caller is ICaller, TokensHandler, Weth {
address internal constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;

/**
Expand All @@ -61,7 +53,6 @@ contract UniswapCaller is ICaller, TokensHandler, Weth {
* - directions Array of exchange directions (`true` means `token0` -> `token1`)
* - swapType Whether input or output amount is fixed
* - fixedSideAmount Amount of the token which is fixed (see `swapType`)
* - unwrap Bool indicating whether Wrapped Ether should be unwrapped to Ether
* @dev Implementation of Caller interface function
*/
function callBytes(bytes calldata callerCallData) external override {
Expand All @@ -85,18 +76,16 @@ contract UniswapCaller is ICaller, TokensHandler, Weth {

// Take input tokens and transfer to the first pair
{
address token = directions[0]
? IUniswapV2Pair(pairs[0]).token0()
: IUniswapV2Pair(pairs[0]).token1();

address inputTokenERC20 = inputToken;
if (inputToken == ETH) {
depositEth(amounts[0]);
inputTokenERC20 = getWeth();
}

uint256 balance = IERC20(token).balanceOf(address(this));
if (amounts[0] > balance) revert InputSlippage(balance, amounts[0]);
uint256 balance = IERC20(inputTokenERC20).balanceOf(address(this));
if (balance < amounts[0]) revert InsufficientBalance(balance, amounts[0]);

SafeERC20.safeTransfer(IERC20(token), pairs[0], amounts[0]);
SafeERC20.safeTransfer(IERC20(inputTokenERC20), pairs[0], amounts[0]);
}

// Do the swaps via the given pairs
Expand Down Expand Up @@ -252,11 +241,10 @@ contract UniswapCaller is ICaller, TokensHandler, Weth {
* @return reserveIn Pool reserve for input token
* @return reserveOut Pool reserve for output token
*/
function getReserves(address pair, bool direction)
internal
view
returns (uint256 reserveIn, uint256 reserveOut)
{
function getReserves(
address pair,
bool direction
) internal view returns (uint256 reserveIn, uint256 reserveOut) {
(uint256 reserve0, uint256 reserve1, ) = IUniswapV2Pair(pair).getReserves();
(reserveIn, reserveOut) = direction ? (reserve0, reserve1) : (reserve1, reserve0);

Expand Down
184 changes: 184 additions & 0 deletions contracts/callers/UniswapV2SinglePoolCaller.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// Copyright (C) 2020 Zerion Inc. <https://zerion.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: LGPL-3.0-only

pragma solidity 0.8.12;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

import { ICaller } from "../interfaces/ICaller.sol";
import { IUniswapV2Pair } from "../interfaces/IUniswapV2Pair.sol";
import { IWETH9 } from "../interfaces/IWETH9.sol";
import { Base } from "../shared/Base.sol";
import { SwapType } from "../shared/Enums.sol";
import { BadToken, InconsistentPairsAndDirectionsLengths, InsufficientBalance, LowReserve, ZeroAmountIn, ZeroAmountOut, ZeroLength } from "../shared/Errors.sol";
import { TokensHandler } from "../shared/TokensHandler.sol";
import { Weth } from "../shared/Weth.sol";

/**
* @title Uniswap caller that executes swaps on UniswapV2-like pools
*/
contract UniswapV2SinglePoolCaller is ICaller, TokensHandler, Weth {
address internal constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;

/**
* @notice Sets Wrapped Ether address for the current chain
* @param weth Wrapped Ether address
*/
constructor(address weth) Weth(weth) {
// solhint-disable-previous-line no-empty-blocks
}

/**
* @notice Main external function:
* executes swap using Uniswap-like pools and returns tokens to the account
* @param callerCallData ABI-encoded parameters:
* - inputToken Address of the token that should be spent
* - outputToken Address of the token that should be returned
* - pair Uniswap V2 like pair
* - swapType Whether input or output amount is fixed
* - fixedSideAmount Amount of the token which is fixed (see `swapType`)
* @dev Implementation of Caller interface function
*/
function callBytes(bytes calldata callerCallData) external override {
(
address inputToken,
address outputToken,
address pair,
SwapType swapType,
uint256 fixedSideAmount
) = abi.decode(callerCallData, (address, address, address, SwapType, uint256));

address inputTokenERC20 = (inputToken == ETH) ? getWeth() : inputToken;
// Positive direction means `inputTokenERC20 == token0`
bool direction = (inputTokenERC20 == IUniswapV2Pair(pair).token0());

(uint256 inputTokenAmount, uint256 outputTokenAmount) = swapType == SwapType.FixedInputs
? (fixedSideAmount, getAmountOut(fixedSideAmount, pair, direction))
: (getAmountIn(fixedSideAmount, pair, direction), fixedSideAmount);

// Transfer input tokens to the pair
{
uint256 balance = Base.getBalance(inputToken);
if (balance < inputTokenAmount) revert InsufficientBalance(balance, inputTokenAmount);

if (inputToken == ETH) depositEth(inputTokenAmount);
SafeERC20.safeTransfer(IERC20(inputTokenERC20), pair, inputTokenAmount);
}

// Do the swap via the given pair
IUniswapV2Pair(pair).swap(
direction ? uint256(0) : outputTokenAmount,
direction ? outputTokenAmount : uint256(0),
(outputToken == ETH) ? address(this) : msg.sender,
bytes("")
);

// Unwrap weth if necessary
if (outputToken == ETH) withdrawEth();

// In case of non-zero input token, transfer the remaining amount back to `msg.sender`
Base.transfer(inputToken, msg.sender, Base.getBalance(inputToken));

// In case of non-zero output token, transfer the total balance to `msg.sender`
Base.transfer(outputToken, msg.sender, Base.getBalance(outputToken));
}

/**
* @notice Wraps Ether
*/
function depositEth(uint256 amount) internal {
address weth = getWeth();

if (amount > 0) IWETH9(weth).deposit{ value: amount }();
}

/**
* @notice Unwraps Wrapped Ether (if necessary)
*/
function withdrawEth() internal {
address weth = getWeth();
uint256 wethBalance = IERC20(weth).balanceOf(address(this));
// The check always passes, however, left for unusual cases
if (wethBalance > uint256(0)) IWETH9(weth).withdraw(wethBalance);
}

/**
* @notice Calculates the required amount for one swap in case of fixed output amount
* @param amountOut Amount of the token returned after the swap
* @param pair Uniswap-like pair
* @param direction Exchange direction (`true` means token0 -> token1)
* @return amountIn Amount required for the swap
* @dev Repeats Uniswap's getAmountIn calculations
*/
function getAmountIn(
uint256 amountOut,
address pair,
bool direction
) internal view returns (uint256 amountIn) {
if (amountOut == uint256(0)) revert ZeroAmountOut();

(uint256 reserveIn, uint256 reserveOut) = getReserves(pair, direction);
if (reserveOut < amountOut) revert LowReserve(reserveOut, amountOut);

uint256 numerator = reserveIn * amountOut * 1000;
uint256 denominator = (reserveOut - amountOut) * 997;

return (numerator / denominator) + 1;
}

/**
* @notice Calculates the returned amount for one swap in case of fixed input amount
* @param amountIn Amount of the token provided for the swap
* @param pair Uniswap-like pair
* @param direction Exchange direction (`true` means token0 -> token1)
* @return amountOut Amount returned after the swap
* @dev Repeats Uniswap's getAmountIn calculations
*/
function getAmountOut(
uint256 amountIn,
address pair,
bool direction
) internal view returns (uint256 amountOut) {
if (amountIn == uint256(0)) revert ZeroAmountIn();

(uint256 reserveIn, uint256 reserveOut) = getReserves(pair, direction);

uint256 amountInWithFee = amountIn * 997;
uint256 numerator = amountInWithFee * reserveOut;
uint256 denominator = (reserveIn * 1000) + amountInWithFee;

return numerator / denominator;
}

/**
* @notice Returns pool's reserves in 'correct' order (input token, output token)
* @param pair Uniswap-like pair
* @param direction Exchange direction (`true` means token0 -> token1)
* @return reserveIn Pool reserve for input token
* @return reserveOut Pool reserve for output token
*/
function getReserves(
address pair,
bool direction
) internal view returns (uint256 reserveIn, uint256 reserveOut) {
(uint256 reserve0, uint256 reserve1, ) = IUniswapV2Pair(pair).getReserves();
(reserveIn, reserveOut) = direction ? (reserve0, reserve1) : (reserve1, reserve0);

return (reserveIn, reserveOut);
}
}
57 changes: 14 additions & 43 deletions contracts/router/Router.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,37 +30,9 @@ import { IRouter } from "../interfaces/IRouter.sol";
import { IYearnPermit } from "../interfaces/IYearnPermit.sol";
import { Base } from "../shared/Base.sol";
import { AmountType, PermitType, SwapType } from "../shared/Enums.sol";
import {
BadAmount,
BadAccount,
BadAccountSignature,
BadAmountType,
BadFeeAmount,
BadFeeBeneficiary,
BadFeeShare,
BadFeeSignature,
ExceedingDelimiterAmount,
ExceedingLimitFee,
HighInputBalanceChange,
InsufficientAllowance,
InsufficientMsgValue,
LowActualOutputAmount,
NoneAmountType,
NonePermitType,
NoneSwapType,
PassedDeadline
} from "../shared/Errors.sol";
import { BadAmount, BadAccount, BadAccountSignature, BadAmountType, BadFeeAmount, BadFeeBeneficiary, BadFeeShare, BadFeeSignature, ExceedingDelimiterAmount, ExceedingLimitFee, HighInputBalanceChange, InsufficientAllowance, InsufficientMsgValue, LowActualOutputAmount, NoneAmountType, NonePermitType, NoneSwapType, PassedDeadline } from "../shared/Errors.sol";
import { Ownable } from "../shared/Ownable.sol";
import {
AbsoluteTokenAmount,
AccountSignature,
Fee,
ProtocolFeeSignature,
Input,
Permit,
SwapDescription,
TokenAmount
} from "../shared/Structs.sol";
import { AbsoluteTokenAmount, AccountSignature, Fee, ProtocolFeeSignature, Input, Permit, SwapDescription, TokenAmount } from "../shared/Structs.sol";
import { TokensHandler } from "../shared/TokensHandler.sol";

import { ProtocolFee } from "./ProtocolFee.sol";
Expand Down Expand Up @@ -102,7 +74,10 @@ contract Router is
AccountSignature calldata accountSignature,
ProtocolFeeSignature calldata protocolFeeSignature
)
external payable override nonReentrant
external
payable
override
nonReentrant
returns (
uint256 inputBalanceChange,
uint256 actualOutputAmount,
Expand Down Expand Up @@ -150,7 +125,7 @@ contract Router is
uint256 initialOutputBalance = Base.getBalance(output.token);

// Transfer tokens to the caller
Base.transfer(inputToken, swapDescription.caller, absoluteInputAmount);
Base.transfer(inputToken, swapDescription.caller, absoluteInputAmount); // In order to support FoT, fix `absoluteInputAmount` here

// Call caller's `callBytes()` function with the provided calldata
Address.functionCall(
Expand Down Expand Up @@ -277,6 +252,7 @@ contract Router is
);
}

// if (balance < amount) revert InsufficientBalance(balance, amount);
SafeERC20.safeTransferFrom(IERC20(token), account, address(this), amount);
}

Expand Down Expand Up @@ -405,11 +381,10 @@ contract Router is
* @param account Address of the account to transfer token from
* @return absoluteTokenAmount Absolute token amount
*/
function getAbsoluteInputAmount(TokenAmount calldata tokenAmount, address account)
internal
view
returns (uint256 absoluteTokenAmount)
{
function getAbsoluteInputAmount(
TokenAmount calldata tokenAmount,
address account
) internal view returns (uint256 absoluteTokenAmount) {
AmountType amountType = tokenAmount.amountType;
address token = tokenAmount.token;
uint256 amount = tokenAmount.amount;
Expand Down Expand Up @@ -456,11 +431,7 @@ contract Router is
)
internal
pure
returns (
uint256 returnedAmount,
uint256 protocolFeeAmount,
uint256 marketplaceFeeAmount
)
returns (uint256 returnedAmount, uint256 protocolFeeAmount, uint256 marketplaceFeeAmount)
{
if (swapType == SwapType.None) revert NoneSwapType();

Expand All @@ -487,7 +458,7 @@ contract Router is
? output.absoluteAmount
: ((outputBalanceChange * DELIMITER) / (DELIMITER + totalFeeShare)) + uint256(1);

uint256 totalFeeAmount = outputBalanceChange - returnedAmount;
uint256 totalFeeAmount = outputBalanceChange - returnedAmount; //! not safe to distract
// This check is important in fixed outputs case as we never actually check that
// total fee amount is not too large and should always just pass in fixed inputs case
if (totalFeeAmount * DELIMITER > totalFeeShare * returnedAmount)
Expand Down
1 change: 1 addition & 0 deletions contracts/shared/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ error HighInputBalanceChange(uint256 inputBalanceChange, uint256 requiredInputBa
error InconsistentPairsAndDirectionsLengths(uint256 pairsLength, uint256 directionsLength);
error InputSlippage(uint256 amount, uint256 requiredAmount);
error InsufficientAllowance(uint256 allowance, uint256 requiredAllowance);
error InsufficientBalance(uint256 balance, uint256 requiredBalance);
error InsufficientMsgValue(uint256 msgValue, uint256 requiredMsgValue);
error LowActualOutputAmount(uint256 actualOutputAmount, uint256 requiredActualOutputAmount);
error LowReserve(uint256 reserve, uint256 requiredReserve);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const uniDaiWethAddress = '0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11';
const zeroPermit = ['0', EMPTY_BYTES];
const zeroSignature = ['0', EMPTY_BYTES];

describe('UniswapCaller', () => {
describe('UniswapV2Caller', () => {
let owner;
let notOwner;
let caller;
Expand All @@ -40,7 +40,7 @@ describe('UniswapCaller', () => {
// ],
// });

Caller = await ethers.getContractFactory('UniswapCaller');
Caller = await ethers.getContractFactory('UniswapV2Caller');
Router = await ethers.getContractFactory('Router');

[owner, notOwner] = await ethers.getSigners();
Expand Down
Loading

0 comments on commit 881a91e

Please sign in to comment.