Skip to content

Instantly share code, notes, and snippets.

@pyk
Last active March 27, 2022 06:08
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save pyk/416b607cde9b0a53525628f74fffbeda to your computer and use it in GitHub Desktop.
// 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);
}
}
// 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);
}
}
}
// 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);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment