Skip to content

Instantly share code, notes, and snippets.

@pyk
Last active March 27, 2022 06:08
Show Gist options
  • Save pyk/416b607cde9b0a53525628f74fffbeda to your computer and use it in GitHub Desktop.
Save pyk/416b607cde9b0a53525628f74fffbeda to your computer and use it in GitHub Desktop.

Revisions

  1. pyk revised this gist Mar 27, 2022. 1 changed file with 217 additions and 0 deletions.
    217 changes: 217 additions & 0 deletions UniswapV2Adapter.sol
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,217 @@
    // SPDX-License-Identifier: GPL-3.0-or-later
    pragma solidity 0.8.11;
    pragma experimental ABIEncoderV2;

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

    import { IUniswapV2Router02 } from "../interfaces/IUniswapV2Router02.sol";
    import { IUniswapV2Factory } from "../interfaces/IUniswapV2Factory.sol";
    import { IUniswapV2Pair } from "../interfaces/IUniswapV2Pair.sol";
    // import { IFlashSwapper } from "../interfaces/IFlashSwapper.sol";

    /**
    * @title Uniswap V2 Adapter
    * @author bayu (github.com/pyk)
    * @notice Standarize Uniswap V2 interaction (swap & flashswap) as IUniswapAdapter
    */
    contract UniswapV2Adapter {
    /// ███ Libraries ██████████████████████████████████████████████████████████

    using SafeERC20 for IERC20;


    /// ███ Storages ███████████████████████████████████████████████████████████

    /// @notice Uniswap V2 router address
    address public immutable router;

    /// @notice WETH address
    address public immutable weth;

    /// @notice The flashswap types
    enum FlashSwapType {FlashSwapExactTokensForTokensViaETH}


    /// ███ Errors █████████████████████████████████████████████████████████████

    error FlashSwapAmountCannotBeZero();
    error FlashSwapPairNotFound(address token0, address token1);
    error FlashSwapNotAuthorized();

    /// @notice Error is raised when flash swap amount out is too low
    error FlashSwapAmountOutTooLow(uint256 min, uint256 got);


    /// ███ Constuctors ████████████████████████████████████████████████████████

    constructor(address _router) {
    router = _router;
    weth = IUniswapV2Router02(_router).WETH();
    }

    /// ███ Internal functions █████████████████████████████████████████████████

    /**
    * @notice This function is executed when flashSwapExactTokensForTokensViaETH is triggered
    */
    function onFlashSwapExactTokensForTokensViaETH(uint256 _wethAmount, uint256 _amountIn, uint256 _amountOut, address[2] memory _tokens, address _flasher, bytes memory _data) internal {
    /// ███ Interactions

    // Get token pairs
    address tokenInPair = IUniswapV2Factory(IUniswapV2Router02(router).factory()).getPair(_tokens[0], weth);
    address tokenOutPair = IUniswapV2Factory(IUniswapV2Router02(router).factory()).getPair(_tokens[1], weth);

    // Step 4:
    // Swap WETH to tokenOut
    address token0 = IUniswapV2Pair(tokenOutPair).token0();
    address token1 = IUniswapV2Pair(tokenOutPair).token1();
    uint256 amount0Out = _tokens[1] == token0 ? _amountOut : 0;
    uint256 amount1Out = _tokens[1] == token1 ? _amountOut : 0;
    IERC20(weth).safeTransfer(tokenOutPair, _wethAmount);
    IUniswapV2Pair(tokenOutPair).swap(amount0Out, amount1Out, address(this), bytes(""));

    // Step 5:
    // Transfer tokenOut to flasher
    IERC20(_tokens[1]).safeTransfer(_flasher, _amountOut);

    // Step 6:
    // Call the flasher
    // IFlashSwapper(_flasher).onFlashSwapExactTokensForTokensViaETH(_amountOut, _data);

    // Step 8:
    // Repay the flashswap
    IERC20(_tokens[0]).safeTransfer(tokenInPair, _amountIn);
    }

    /**
    * @notice Gets the amount of tokenOut if swap is routed through WETH
    * @param _tokens _tokens[0] is tokenIn, _tokens[1] is tokenOut
    * @param _amountIn The amount of tokenIn
    * @return _amountOut The amount of tokenOut
    */
    function getAmountOutViaETH(address[2] memory _tokens, uint256 _amountIn) public view returns (uint256 _amountOut) {
    address[] memory path = new address[](3);
    path[0] = _tokens[0];
    path[1] = weth;
    path[2] = _tokens[1];
    _amountOut = IUniswapV2Router02(router).getAmountsOut(_amountIn, path)[2];
    }

    /**
    * @notice Gets the amount of tokenIn if swap is routed through WETH
    * @param _tokens _tokens[0] is tokenIn, _tokens[1] is tokenOut
    * @param _amountOut The amount of tokenOut
    * @return _amountIn The amount of tokenIn
    */
    function getAmountInViaETH(address[2] memory _tokens, uint256 _amountOut) public view returns (uint256 _amountIn) {
    address[] memory path = new address[](3);
    path[0] = _tokens[0];
    path[1] = weth;
    path[2] = _tokens[1];
    _amountIn = IUniswapV2Router02(router).getAmountsIn(_amountOut, path)[0];
    }


    /// ███ Callbacks ██████████████████████████████████████████████████████████

    /// @notice Function is called by the Uniswap V2 pair's when swap function is executed
    function uniswapV2Call(address _sender, uint256 _amount0, uint256 _amount1, bytes memory _data) external {
    /// ███ Checks

    // Check caller
    address token0 = IUniswapV2Pair(msg.sender).token0();
    address token1 = IUniswapV2Pair(msg.sender).token1();
    if (msg.sender != IUniswapV2Factory(IUniswapV2Router02(router).factory()).getPair(token0, token1)) revert FlashSwapNotAuthorized();
    if (_sender != address(this)) revert FlashSwapNotAuthorized();

    // Get the data
    (FlashSwapType flashSwapType, bytes memory data) = abi.decode(_data, (FlashSwapType, bytes));

    // Continue execute the function based on the flash swap type
    if (flashSwapType == FlashSwapType.FlashSwapExactTokensForTokensViaETH) {
    // Get WETH amount
    uint256 wethAmount = _amount0 == 0 ? _amount1 : _amount0;
    (uint256 amountIn, uint256 amountOut, address tokenIn, address tokenOut, address flasher, bytes memory callData) = abi.decode(data, (uint256,uint256,address,address,address,bytes));
    onFlashSwapExactTokensForTokensViaETH(wethAmount, amountIn, amountOut, [tokenIn, tokenOut], flasher, callData);
    return;
    }
    }

    /// ███ Adapters ███████████████████████████████████████████████████████████

    /**
    * @notice Flash swaps an exact amount of input tokens for as many output
    * tokens as possible via tokenIn/WETH and tokenOut/WETH pairs.
    * @param _amountIn The amount of tokenIn that used to repay the flash swap
    * @param _amountOutMin The minimum amount of tokenOut that will received by the flash swap executor
    * @param _tokens _tokens[0] is the tokenIn and _tokens[1] is the tokenOut
    * @param _data Bytes data transfered to callback
    */
    function flashSwapExactTokensForTokensViaETH(uint256 _amountIn, uint256 _amountOutMin, address[2] calldata _tokens, bytes calldata _data) public {
    /// ███ Checks

    // Check amount
    if (_amountIn == 0) revert FlashSwapAmountCannotBeZero();

    // Check pairs
    address tokenInPair = IUniswapV2Factory(IUniswapV2Router02(router).factory()).getPair(_tokens[0], weth);
    address tokenOutPair = IUniswapV2Factory(IUniswapV2Router02(router).factory()).getPair(_tokens[1], weth);
    if (tokenInPair == address(0)) revert FlashSwapPairNotFound(_tokens[0], weth);
    if (tokenOutPair == address(0)) revert FlashSwapPairNotFound(_tokens[1], weth);

    // Check the amount of tokenOut
    uint256 amountOut = getAmountOutViaETH(_tokens, _amountIn);
    if (amountOut < _amountOutMin) revert FlashSwapAmountOutTooLow(_amountOutMin, amountOut);

    /// ███ Effects

    /// ███ Interactions

    // Step 1:
    // Calculate how much WETH we need to borrow from tokenIn/WETH pair
    address[] memory wethToTokenOut = new address[](2);
    wethToTokenOut[0] = weth;
    wethToTokenOut[1] = _tokens[1];
    uint256 wethAmount = IUniswapV2Router02(router).getAmountsIn(amountOut, wethToTokenOut)[0];

    // Step 2:
    // Borrow WETH from tokenIn/WETH liquidity pair (e.g. USDC/WETH)
    uint256 amount0Out = weth == IUniswapV2Pair(tokenInPair).token0() ? wethAmount : 0;
    uint256 amount1Out = weth == IUniswapV2Pair(tokenInPair).token1() ? wethAmount : 0;

    // Step 3:
    // Perform the flashswap to Uniswap V2; Step 4 in onFlashSwapExactTokensForTokensViaETH
    bytes memory data = abi.encode(FlashSwapType.FlashSwapExactTokensForTokensViaETH, abi.encode(_amountIn, amountOut, _tokens[0], _tokens[1], msg.sender, _data));
    IUniswapV2Pair(tokenInPair).swap(amount0Out, amount1Out, address(this), data);
    }

    /**
    * @notice Swaps an exact amount of output tokens for as few input
    * tokens as possible via tokenIn/WETH and tokenOut/WETH pairs.
    * @param _amountOut The amount of tokenOut
    * @param _amountInMax The maximum amount of tokenIn
    * @param _tokens _tokens[0] is tokenIn, _tokens[1] is tokenOut
    * @return _amountIn The amount of tokenIn used to get _amountOut
    */
    function swapTokensForExactTokensViaETH(uint256 _amountOut, uint256 _amountInMax, address[2] calldata _tokens) external returns (uint256 _amountIn) {
    // Tranfer the tokenIn
    IERC20(_tokens[0]).safeTransferFrom(msg.sender, address(this), _amountInMax);

    // Swap the token
    address[] memory path = new address[](3);
    path[0] = _tokens[0];
    path[1] = weth;
    path[2] = _tokens[1];

    IERC20(_tokens[0]).approve(router, _amountInMax);
    _amountIn = IUniswapV2Router02(router).swapTokensForExactTokens(_amountOut, _amountInMax, path, msg.sender, block.timestamp)[0];
    IERC20(_tokens[0]).approve(router, 0);

    // Transfer the leftover
    uint256 leftover = _amountInMax - _amountIn;
    if (leftover > 0) {
    IERC20(_tokens[0]).safeTransfer(msg.sender, leftover);
    }
    }
    }
  2. pyk revised this gist Mar 27, 2022. 1 changed file with 80 additions and 0 deletions.
    80 changes: 80 additions & 0 deletions Flasher.sol
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,80 @@
    // SPDX-License-Identifier: GPL-3.0-or-later
    pragma solidity 0.8.11;
    pragma experimental ABIEncoderV2;

    import "lib/ds-test/src/test.sol";
    import { IERC20 } from "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
    import { SafeERC20 } from "lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";

    import { IUniswapAdapter } from "../../interfaces/IUniswapAdapter.sol";

    /**
    * @title Flasher
    * @author bayu (github.com/pyk)
    * @notice Contract to simulate the flash swap user of UniswapV2Adapter.
    * This contract implements IFlashSwapper.
    */
    contract Flasher {
    /// ███ Libraries ██████████████████████████████████████████████████████████
    using SafeERC20 for IERC20;


    /// ███ Storages ███████████████████████████████████████████████████████████

    /// @notice Uniswap V2 Adapter
    address private uniswapAdapter;

    /// @notice The tokenIn
    address private tokenIn;

    /// @notice The tokenOut
    address private tokenOut;

    /// @notice The amount of tokenIn
    uint256 private amountIn;


    /// ███ Events █████████████████████████████████████████████████████████████

    event FlashSwap(uint256 amount, bytes data);


    /// ███ Errors █████████████████████████████████████████████████████████████

    /// @notice Error raised when onFlashSwap caller is not the UniswapAdapter
    error NotUniswapAdapter();


    /// ███ Constructors ███████████████████████████████████████████████████████

    constructor(address _uniswapAdapter) {
    uniswapAdapter = _uniswapAdapter;
    }


    /// ███ External functions █████████████████████████████████████████████████

    /// @notice Trigger the flash swap
    function flashSwapExactTokensForTokensViaETH(uint256 _amountIn, uint256 _amountOutMin, address[2] calldata _tokens, bytes calldata _data) external {
    tokenIn = _tokens[0];
    tokenOut = _tokens[1];
    amountIn = _amountIn;
    IUniswapAdapter(uniswapAdapter).flashSwapExactTokensForTokensViaETH(_amountIn, _amountOutMin, _tokens, _data);
    }

    /// @notice Executed by the adapter
    function onFlashSwapExactTokensForTokensViaETH(uint256 _amountOut, bytes calldata _data) external {
    /// ███ Checks

    // Check the caller; Make sure it's Uniswap Adapter
    if (msg.sender != uniswapAdapter) revert NotUniswapAdapter();

    /// ███ Effects

    /// ███ Interactions

    IERC20(tokenIn).safeTransfer(uniswapAdapter, amountIn);

    emit FlashSwap(_amountOut, _data);
    }
    }
  3. pyk created this gist Mar 27, 2022.
    127 changes: 127 additions & 0 deletions UniswapV2Adapter.t.sol
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,127 @@
    // SPDX-License-Identifier: GPL-3.0-or-later
    pragma solidity 0.8.11;
    pragma experimental ABIEncoderV2;

    import "lib/ds-test/src/test.sol";
    import { IERC20 } from "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
    import { SafeERC20 } from "lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";

    import { UniswapV2Adapter } from "../../uniswap/UniswapV2Adapter.sol";
    import { Flasher } from "./Flasher.sol";
    import { gohm, usdc, sushiRouter, weth } from "../Arbitrum.sol";
    import { HEVM } from "../HEVM.sol";
    import { IUniswapV2Router02 } from "../../interfaces/IUniswapV2Router02.sol";

    /**
    * @title Uniswap V2 Adapter Test
    * @author bayu (github.com/pyk)
    * @notice Unit testing for UniswapV2Adapter implementation
    */
    contract UniswapV2AdapterTest is DSTest {
    HEVM private hevm;

    function setUp() public {
    hevm = new HEVM();
    }

    /// @notice Flasher cannot flashSwapExactTokensForTokensViaETH with zero amount
    function testFailFlasherCannotFlashSwapExactTokensForTokensViaETHWithZeroAmountBorrowToken() public {
    // Create new adapter
    UniswapV2Adapter adapter = new UniswapV2Adapter(sushiRouter);

    // Create new Flasher
    Flasher flasher = new Flasher(address(adapter));

    // Trigger the flash swap; this should be failed
    flasher.flashSwapExactTokensForTokensViaETH(0, 0, [usdc, gohm], bytes(""));
    }

    /// @notice Flasher cannot flashSwapExactTokensForTokensViaETH with invalid tokenIn
    function testFailFlasherCannotFlashSwapExactTokensForTokensViaETHWithInvalidTokenIn() public {
    // Create new adapter
    UniswapV2Adapter adapter = new UniswapV2Adapter(sushiRouter);

    // Create new Flasher
    Flasher flasher = new Flasher(address(adapter));

    // Trigger the flash swap; this should be failed
    address randomToken = hevm.addr(1);
    flasher.flashSwapExactTokensForTokensViaETH(1 ether, 0, [randomToken, gohm], bytes(""));
    }

    /// @notice Flasher cannot flashSwapExactTokensForTokensViaETH with invalid tokenOut
    function testFailFlasherCannotFlashSwapExactTokensForTokensViaETHWithInvalidTokenOut() public {
    // Create new adapter
    UniswapV2Adapter adapter = new UniswapV2Adapter(sushiRouter);

    // Create new Flasher
    Flasher flasher = new Flasher(address(adapter));

    // Trigger the flash swap; this should be failed
    address randomToken = hevm.addr(1);
    flasher.flashSwapExactTokensForTokensViaETH(1 ether, 0, [usdc, randomToken], bytes(""));
    }

    /// @notice When flasher flashSwapExactTokensForTokensViaETH, make sure it receive the tokenOut
    function testFlasherCanFlashSwapExactTokensForTokensViaETHAndReceiveTokenOut() public {
    // Create new adapter
    UniswapV2Adapter adapter = new UniswapV2Adapter(sushiRouter);

    // Create new Flasher
    Flasher flasher = new Flasher(address(adapter));

    // Top up the flasher to repay the borrow
    hevm.setUSDCBalance(address(flasher), 10_000 * 1e6); // 10K USDC

    // Trigger the flash swap; borrow gOHM pay with USDC
    uint256 amountIn = 5_000 * 1e6; // 5K USDC
    flasher.flashSwapExactTokensForTokensViaETH(amountIn, 0, [usdc, gohm], bytes(""));

    // Get the amount out
    address[] memory tokenInToTokenOut = new address[](3);
    tokenInToTokenOut[0] = usdc;
    tokenInToTokenOut[1] = weth;
    tokenInToTokenOut[2] = gohm;
    uint256 amountOut = IUniswapV2Router02(sushiRouter).getAmountsOut(amountIn, tokenInToTokenOut)[2];

    // Check
    uint256 balance = IERC20(gohm).balanceOf(address(flasher));
    // Tolerance +-2%
    uint256 minBalance = amountOut - ((0.02 ether * balance) / 1 ether);
    uint256 maxBalance = amountOut + ((0.02 ether * balance) / 1 ether);
    assertGt(balance, minBalance);
    assertLt(balance, maxBalance);
    }

    /// @notice Make sure the uniswapV2Callback cannot be called by random dude
    function testFailUniswapV2CallCannotBeCalledByRandomDude() public {
    // Create new adapter
    UniswapV2Adapter adapter = new UniswapV2Adapter(sushiRouter);

    // Random dude try to execute the UniswapV2Callback; should be failed
    adapter.uniswapV2Call(address(this), 0, 0, bytes(""));
    }

    /// @notice Make sure the swapTokensForExactTokensViaETH is working
    function testSwapTokensForExactTokensViaETH() public {
    // Create new adapter
    UniswapV2Adapter adapter = new UniswapV2Adapter(sushiRouter);

    // Topup balance
    hevm.setGOHMBalance(address(this), 1 ether);

    // Swap gOHM to USDC
    uint256 amountOut = 500 * 1e6; // 500 USDC
    uint256 amountInMax = adapter.getAmountInViaETH([gohm, usdc], amountOut);
    IERC20(gohm).approve(address(adapter), amountInMax);
    uint256 amountIn = adapter.swapTokensForExactTokensViaETH(amountOut, amountInMax, [gohm, usdc]);
    IERC20(gohm).approve(address(adapter), 0);

    // Check the amountIn
    assertLe(amountIn, amountInMax);

    // Check the user balance
    assertEq(IERC20(gohm).balanceOf(address(this)), 1 ether - amountIn);
    assertEq(IERC20(usdc).balanceOf(address(this)), amountOut);
    }
    }