Last active
March 27, 2022 06:08
-
-
Save pyk/416b607cde9b0a53525628f74fffbeda to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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